[object Object]

← back to Commercialrealestate

Add DSCR Qualifier to the initial page (index.html) + Frank's lending desk

750c9b8b90273f00247217ff7ac72e2fa3a94d74 · 2026-08-06 14:58:15 -0700 · Steve

- rents-in / PITI-out DSCR calculator; user enters current-or-projected
  fair-market rents (per-unit rent roll, seeds the six-unit case) and the
  payment (build P&I via CREFin.debtService + taxes/ins/HOA, or enter PITI direct)
- verdict badge vs the lender's approval line (Approves / Marginal / Short),
  'no tax returns' framing; max-supportable-payment + rents-needed helpers
- auto-generates Frank's referral note prefilled with the numbers (Copy + gated Queue)
- reuses finance.js CREFin so the qualifier and the listing ranker share one
  amortization; Solicitation Studio gains a Referring-agents (DSCR) audience

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 750c9b8b90273f00247217ff7ac72e2fa3a94d74
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 6 14:58:15 2026 -0700

    Add DSCR Qualifier to the initial page (index.html) + Frank's lending desk
    
    - rents-in / PITI-out DSCR calculator; user enters current-or-projected
      fair-market rents (per-unit rent roll, seeds the six-unit case) and the
      payment (build P&I via CREFin.debtService + taxes/ins/HOA, or enter PITI direct)
    - verdict badge vs the lender's approval line (Approves / Marginal / Short),
      'no tax returns' framing; max-supportable-payment + rents-needed helpers
    - auto-generates Frank's referral note prefilled with the numbers (Copy + gated Queue)
    - reuses finance.js CREFin so the qualifier and the listing ranker share one
      amortization; Solicitation Studio gains a Referring-agents (DSCR) audience
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/crcp.html  | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 public/index.html | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 365 insertions(+)

diff --git a/public/crcp.html b/public/crcp.html
index f041e77..81beab1 100644
--- a/public/crcp.html
+++ b/public/crcp.html
@@ -109,6 +109,31 @@
   .findmail:hover{background:rgba(91,155,213,.12);}
   .findmail:disabled{opacity:.5;cursor:default;}
   .hunting{color:var(--staged);font-size:12px;}
+  /* DSCR Qualifier */
+  .dscr-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;} @media(max-width:880px){.dscr-grid{grid-template-columns:1fr;}}
+  .dscr .dfield{margin-bottom:10px;} .dscr .dfield>label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);margin-bottom:4px;}
+  .dscr input[type=number],.dscr input[type=text]{width:100%;box-sizing:border-box;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:8px 10px;color:var(--fg);font-size:14px;font-variant-numeric:tabular-nums;}
+  .dscr input:focus{outline:0;border-color:var(--gold);}
+  .dscr .row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
+  .dscr .unitrow{display:grid;grid-template-columns:1fr 130px 32px;gap:8px;align-items:center;margin-bottom:6px;}
+  .dscr .unitrow button{background:transparent;border:1px solid var(--line);color:var(--muted);border-radius:7px;cursor:pointer;font-size:15px;line-height:1;padding:7px 0;}
+  .dscr .unitrow button:hover{border-color:var(--del);color:var(--del);}
+  .dscr .addunit{background:transparent;border:1px dashed var(--line);color:var(--draft);border-radius:8px;padding:6px 11px;font-size:12px;cursor:pointer;margin-top:2px;}
+  .dscr .addunit:hover{border-color:var(--draft);}
+  .dscr .rentsum{font-size:12px;color:var(--muted);margin-top:7px;} .dscr .rentsum b{color:var(--active);font-variant-numeric:tabular-nums;}
+  .dscr .dtog{display:inline-flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;}
+  .dscr .dtog button{background:var(--panel2);border:0;color:var(--muted);padding:6px 12px;font-size:12px;cursor:pointer;} .dscr .dtog button.on{background:rgba(200,162,75,.14);color:var(--gold);}
+  .dscr .dresult{background:#0a0d13;border:1px solid var(--line);border-radius:12px;padding:18px 16px;text-align:center;}
+  .dscr .dresult .ratio{font-size:48px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1;margin-top:4px;}
+  .dscr .dbadge{display:inline-block;margin-top:12px;font-size:13px;font-weight:800;letter-spacing:.4px;text-transform:uppercase;padding:6px 14px;border-radius:20px;}
+  .dscr .dbadge.pass{background:rgba(112,173,71,.15);color:var(--active);border:1px solid rgba(112,173,71,.5);}
+  .dscr .dbadge.marg{background:rgba(224,163,62,.15);color:var(--staged);border:1px solid rgba(224,163,62,.5);}
+  .dscr .dbadge.fail{background:rgba(231,76,60,.14);color:var(--del);border:1px solid rgba(231,76,60,.5);}
+  .dscr .brk{font-size:12.5px;color:var(--muted);margin-top:14px;line-height:1.8;text-align:left;}
+  .dscr .brk .r{display:flex;justify-content:space-between;gap:10px;} .dscr .brk b{color:var(--fg);font-variant-numeric:tabular-nums;}
+  .dscr .brk .r.hi b{color:var(--gold);}
+  .dscr pre{white-space:pre-wrap;background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:12px;font:12.5px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--fg);margin:6px 0 0;}
+  .dscr .dactions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;} .dscr .dactions button{background:var(--panel2);border:1px solid var(--line);color:var(--fg);border-radius:8px;padding:7px 12px;font-size:12px;cursor:pointer;} .dscr .dactions .prime{background:var(--gold);color:#1a1206;border-color:var(--gold);font-weight:700;}
 </style>
 </head>
 <body>
@@ -163,6 +188,58 @@
     <div class="meta">Approved projects whose FHA certification lapses within a year — a buyer using FHA financing should confirm re-certification. FHA-approval PROXY signal, not lender-verified Fannie/Freddie warrantability.</div>
   </div>
 
+  <div class="panel dscr" id="dscrPanel"><h3>🧮 DSCR Qualifier — does the property qualify itself? <span class="c">no tax returns · rents ÷ PITI</span></h3>
+    <div class="meta">A <b>DSCR</b> (debt-service-coverage-ratio) loan qualifies on the <b>property's own cash flow</b>, not the borrower's tax returns — the alternative to conventional commercial financing on small multifamily. Enter the rents and the payment: if the current or projected fair-market rents cover the PITI at the lender's ratio, it's approvable.</div>
+    <div class="dscr-grid" style="margin-top:14px">
+      <div>
+        <div class="dfield"><label>Property (used in Frank's note)</label><input type="text" id="dAddr" placeholder="e.g. the 95th St 6-unit"></div>
+        <div class="dfield"><label>Rent basis</label>
+          <div class="dtog" id="dRentBasis"><button type="button" class="on" data-v="current">Current rents</button><button type="button" data-v="projected">Projected fair-market</button></div>
+        </div>
+        <div class="dfield"><label>Rent roll — monthly rent per unit</label>
+          <div id="dUnits"></div>
+          <button type="button" class="addunit" id="dAddUnit">+ add unit</button>
+          <div class="rentsum">Gross monthly rents: <b id="dRentSum">—</b> &nbsp;·&nbsp; annual <b id="dRentAnn">—</b></div>
+        </div>
+        <div class="dfield"><label>PITI — the monthly payment</label>
+          <div class="dtog" id="dPitiMode"><button type="button" class="on" data-v="build">Build it</button><button type="button" data-v="direct">Enter directly</button></div>
+        </div>
+        <div id="dBuild">
+          <div class="row2">
+            <div class="dfield"><label>Loan amount</label><input type="number" id="dLoan" value="960000" min="0" step="1000"></div>
+            <div class="dfield"><label>Rate %</label><input type="number" id="dRate" value="7.25" min="0" step="0.05"></div>
+          </div>
+          <div class="row2">
+            <div class="dfield"><label>Amortization (yrs)</label><input type="number" id="dAmort" value="30" min="1" step="1"></div>
+            <div class="dfield"><label>Taxes / mo</label><input type="number" id="dTax" value="1250" min="0" step="10"></div>
+          </div>
+          <div class="row2">
+            <div class="dfield"><label>Insurance / mo</label><input type="number" id="dIns" value="450" min="0" step="10"></div>
+            <div class="dfield"><label>HOA / other / mo</label><input type="number" id="dHoa" value="0" min="0" step="10"></div>
+          </div>
+        </div>
+        <div id="dDirect" style="display:none">
+          <div class="dfield"><label>PITI / mo (all-in payment)</label><input type="number" id="dPiti" value="8200" min="0" step="10"></div>
+        </div>
+        <div class="dfield"><label>Approval DSCR (lender's line)</label><input type="number" id="dThresh" value="1.20" min="0.5" max="3" step="0.05"></div>
+      </div>
+      <div>
+        <div class="dresult">
+          <div class="k" style="font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)">DSCR = rents ÷ PITI</div>
+          <div class="ratio" id="dRatio">—</div>
+          <div id="dBadge" class="dbadge">—</div>
+          <div class="brk" id="dBrk"></div>
+        </div>
+        <div style="margin-top:14px">
+          <div class="k" style="font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)">Frank's note to the referring agent</div>
+          <pre id="dNote"></pre>
+          <div class="dactions"><button type="button" class="prime" id="dCopy">📋 Copy note</button><button type="button" id="dQueue">🎬 Queue to producer</button></div>
+          <div class="gate">Draft only — sending / producing is gated (queued for Steve, never auto-sent).</div>
+        </div>
+      </div>
+    </div>
+  </div>
+
   <div class="panel" id="solPanel"><h3>📣 Solicitation Studio — cold &amp; warm outreach content <span class="c" id="solC">ads · video · music · social</span></h3>
     <div class="solbar">
       <div class="soltabs" id="solTone" title="Cold = never contacted · Warm = prior relationship">
@@ -182,6 +259,7 @@
   </div>
 </div>
 <div id="ov"><div id="modal"><button class="x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
+<script src="/finance.js"></script>
 <script>
 const $=s=>document.querySelector(s);
 const money=n=>n?'$'+Number(n).toLocaleString():'—';
@@ -542,6 +620,9 @@ loadSegments(); setInterval(loadSegments, 20000);
     {key:'brokers',label:'Commercial brokers',cnt:s=>(s&&s.brokers)||0,
       who:'commercial brokers with multifamily / mixed-use listings',pain:'their buyers need creative financing to close',
       offer:'CRE / bridge financing',proof:'we fund the deals banks slow-walk',drill:'/broker-grid.html'},
+    {key:'realtor_dscr',label:'Referring agents (DSCR)',cnt:s=>(s&&s.agents_residential)||0,
+      who:'agents whose buyer got turned down for a tax-return-heavy commercial loan',pain:'the deal died in underwriting over docs, not the property',
+      offer:'a DSCR loan that qualifies on the property’s own rents — no tax returns',proof:'send me the rent roll and I’ll tell you same-day if it pencils',drill:'/broker-grid.html?type=residential'},
   ];
   let tone='cold', type='ad', audKey='unwarrantable';
   const SIGN='— Frank · your commercial-lending desk';
@@ -590,6 +671,110 @@ loadSegments(); setInterval(loadSegments, 20000);
   // refresh the audience counts once live stats arrive
   let _tries=0; const _t=setInterval(()=>{ if(lastStats||_tries++>20){ buildAud(); render(); clearInterval(_t); } }, 1500);
 })();
+
+// ── DSCR Qualifier — rents ÷ PITI, no-tax-returns qualification + Frank's referral note ──
+(function(){
+  const panel=document.getElementById('dscrPanel'); if(!panel) return;
+  let basis='current', pitiMode='build';
+  // Monthly P&I from loan/rate/amort — reuse the ranker's amortization (finance.js) so the desk
+  // and the property ranker never disagree; fall back to an inline standard mortgage formula if
+  // finance.js didn't load for any reason.
+  function monthlyPI(loan, ratePct, amortYears){
+    if(!(loan>0)) return 0;
+    if(window.CREFin && CREFin.debtService) return CREFin.debtService(loan, ratePct, amortYears)/12;
+    const r=ratePct/100/12, n=amortYears*12;
+    return r===0 ? loan/n : loan*(r*Math.pow(1+r,n))/(Math.pow(1+r,n)-1);
+  }
+  const num=id=>{ const el=document.getElementById(id); const v=el?parseFloat(el.value):NaN; return isFinite(v)?v:0; };
+  const usd=n=>'$'+Math.round(n).toLocaleString();
+  // ── rent roll ──
+  const DEF=[1850,1850,1850,1850,1850,1850]; // seeds the six-unit story
+  function unitRows(){ return Array.from(document.querySelectorAll('#dUnits .unitrow')); }
+  function grossMonthly(){ return unitRows().reduce((s,r)=>{ const v=parseFloat(r.querySelector('.urent').value); return s+(isFinite(v)?v:0); },0); }
+  function addUnit(rent){
+    const wrap=document.getElementById('dUnits'); const i=unitRows().length+1;
+    const row=document.createElement('div'); row.className='unitrow';
+    row.innerHTML=`<input type="text" class="ulabel" value="Unit ${i}" aria-label="Unit label">`+
+      `<input type="number" class="urent" value="${rent!=null?rent:1800}" min="0" step="25" aria-label="Monthly rent">`+
+      `<button type="button" class="udel" title="Remove unit">×</button>`;
+    row.querySelector('.urent').addEventListener('input',recompute);
+    row.querySelector('.udel').addEventListener('click',()=>{ if(unitRows().length>1){ row.remove(); recompute(); } });
+    wrap.appendChild(row);
+  }
+  function verdict(dscr, thresh){
+    if(dscr==null||!isFinite(dscr)) return {cls:'',txt:'—'};
+    if(dscr>=thresh) return {cls:'pass',txt:'✓ Approves — no tax returns'};
+    if(dscr>=1.0)    return {cls:'marg',txt:'⚠ Marginal — covers, below the line'};
+    return {cls:'fail',txt:'✕ Short — rents don’t cover PITI'};
+  }
+  function buildNote(g){
+    const addr=(document.getElementById('dAddr').value||'').trim()||'that property';
+    const units=unitRows().length, basisWord=basis==='projected'?'projected fair-market':'current';
+    let line, close;
+    if(g.dscr==null){ line='the payment nets to zero'; close='Send me the rent roll and I’ll pencil it.'; }
+    else if(g.dscr>=g.thresh){ line=`a ${g.dscr.toFixed(2)} DSCR — clears the ${g.thresh.toFixed(2)} line with ${usd(g.surplus)}/mo to spare`; close='It qualifies as-is — happy to send terms and get an app started whenever your seller’s ready.'; }
+    else if(g.dscr>=1.0){ line=`a ${g.dscr.toFixed(2)} DSCR — the rents cover the payment but it’s under a typical ${g.thresh.toFixed(2)} approval line`; close=`We can still get there: about ${usd(g.rentsNeeded-g.gross)}/mo more in rent, a bit more down, or a lender at ${g.dscr.toFixed(2)} min gets it done. Want me to run those?`; }
+    else { line=`a ${g.dscr.toFixed(2)} DSCR — the rents fall about ${usd(g.shortfall)}/mo short of the payment`; close=`It doesn’t pencil at asking on rents alone — projected fair-market rents or more down would move it. Want me to model it?`; }
+    return `Hey — thanks for the list. I called the agent on ${addr} and ran DSCR numbers on it as an alternative to commercial financing on that ${units}-unit.\n\n`+
+      `The nice part: it’s a DSCR loan, so no tax returns — it qualifies on the building’s own rents.\n\n`+
+      `  • ${basisWord[0].toUpperCase()+basisWord.slice(1)} rents:  ${usd(g.gross)}/mo  (${usd(g.gross*12)}/yr)\n`+
+      `  • Full payment (PITI):  ${usd(g.piti)}/mo\n`+
+      `  • DSCR:  ${g.dscr==null?'—':g.dscr.toFixed(2)}  →  ${line}\n\n`+
+      `${close}\n\n`+
+      `Happy to run a same-day scenario on any of the others on your list.\n— Frank · your commercial-lending desk`;
+  }
+  function recompute(){
+    const gross=grossMonthly();
+    document.getElementById('dRentSum').textContent=usd(gross);
+    document.getElementById('dRentAnn').textContent=usd(gross*12);
+    let piti;
+    if(pitiMode==='build'){ piti = monthlyPI(num('dLoan'),num('dRate'),num('dAmort')||30) + num('dTax') + num('dIns') + num('dHoa'); }
+    else { piti = num('dPiti'); }
+    const thresh=num('dThresh')||1.20;
+    const dscr = piti>0 ? gross/piti : null;
+    const g={ gross, piti, thresh, dscr,
+      surplus: gross-piti, shortfall: piti-gross,
+      maxPiti: thresh>0 ? gross/thresh : 0,        // most PITI these rents support at the approval line
+      rentsNeeded: piti*thresh };                   // rents needed to hit the approval line
+    // headline
+    document.getElementById('dRatio').textContent = dscr==null?'—':dscr.toFixed(2);
+    const v=verdict(dscr,thresh), badge=document.getElementById('dBadge');
+    badge.className='dbadge '+v.cls; badge.textContent=v.txt;
+    document.getElementById('dRatio').style.color = v.cls==='pass'?'var(--active)':v.cls==='marg'?'var(--staged)':v.cls==='fail'?'var(--del)':'var(--fg)';
+    // breakdown
+    const gap = dscr==null?'':(dscr>=thresh
+      ? `<div class="r"><span>Monthly surplus over payment</span><b style="color:var(--active)">${usd(g.surplus)}</b></div>`
+      : `<div class="r"><span>Rents needed to hit ${thresh.toFixed(2)}</span><b>${usd(g.rentsNeeded)}/mo</b></div>`+
+        `<div class="r"><span>${gross>=piti?'Cushion above payment':'Monthly shortfall'}</span><b style="color:${gross>=piti?'var(--active)':'var(--del)'}">${usd(Math.abs(g.surplus))}</b></div>`);
+    document.getElementById('dBrk').innerHTML =
+      `<div class="r"><span>Gross monthly rents</span><b>${usd(gross)}</b></div>`+
+      `<div class="r"><span>PITI (payment)</span><b>${usd(piti)}</b></div>`+
+      `<div class="r hi"><span>Max payment these rents support @ ${thresh.toFixed(2)}</span><b>${usd(g.maxPiti)}/mo</b></div>`+
+      gap;
+    // Frank's note
+    document.getElementById('dNote').textContent = buildNote(g);
+    return g;
+  }
+  // ── wire toggles ──
+  document.querySelectorAll('#dRentBasis button').forEach(b=>b.onclick=()=>{ basis=b.dataset.v; b.parentNode.querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b)); recompute(); });
+  document.querySelectorAll('#dPitiMode button').forEach(b=>b.onclick=()=>{ pitiMode=b.dataset.v; b.parentNode.querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b)); document.getElementById('dBuild').style.display=pitiMode==='build'?'':'none'; document.getElementById('dDirect').style.display=pitiMode==='direct'?'':'none'; recompute(); });
+  ['dLoan','dRate','dAmort','dTax','dIns','dHoa','dPiti','dThresh','dAddr'].forEach(id=>{ const el=document.getElementById(id); if(el) el.addEventListener('input',recompute); });
+  document.getElementById('dAddUnit').onclick=()=>{ addUnit(1800); recompute(); };
+  document.getElementById('dCopy').onclick=()=>{ const t=document.getElementById('dNote').textContent; navigator.clipboard&&navigator.clipboard.writeText(t); const b=document.getElementById('dCopy'); b.textContent='✓ Copied'; setTimeout(()=>b.textContent='📋 Copy note',1200); };
+  document.getElementById('dQueue').onclick=()=>{
+    const b=document.getElementById('dQueue'); b.disabled=true; b.textContent='queuing…';
+    const g=recompute(), addr=(document.getElementById('dAddr').value||'').trim()||'DSCR scenario';
+    fetch('/api/solicitation/queue',{method:'POST',headers:{'content-type':'application/json'},
+      body:JSON.stringify({tone:'warm',type:'dscr-note',audience:'realtor_dscr',
+        subject:`DSCR note · ${addr} · ${g.dscr==null?'—':g.dscr.toFixed(2)}`,body:document.getElementById('dNote').textContent})})
+      .then(r=>r.json()).then(d=>{ b.textContent=d&&d.ok?('✓ Queued #'+(d.count||'')+' — gated'):'✓ Queued (draft)'; })
+      .catch(()=>{ b.textContent='✓ Queued (local draft)'; })
+      .finally(()=>{ setTimeout(()=>{ b.disabled=false; b.textContent='🎬 Queue to producer'; },2500); });
+  };
+  // seed the six-unit rent roll and compute once
+  DEF.forEach(r=>addUnit(r));
+  recompute();
+})();
 </script>
   <script src="/col-resize.js" defer></script>
 </body>
diff --git a/public/index.html b/public/index.html
index 851c30e..002e0ad 100644
--- a/public/index.html
+++ b/public/index.html
@@ -460,6 +460,90 @@
 </aside>
 
 <main>
+  <!-- ===== DSCR Qualifier — no-tax-returns qualification (rents ÷ PITI) ===== -->
+  <style>
+    #dscrPanel{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:16px 18px;margin-bottom:18px;}
+    #dscrPanel h2{margin:0 0 2px;font-size:14px;text-transform:uppercase;letter-spacing:.6px;color:var(--warn);}
+    #dscrPanel h2 .c{color:var(--mut);font-weight:400;text-transform:none;letter-spacing:0;font-size:12px;}
+    #dscrPanel .dsub{color:var(--mut);font-size:12.5px;margin-bottom:14px;} #dscrPanel .dsub b{color:var(--ink);}
+    #dscrPanel .dscr-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;} @media(max-width:820px){#dscrPanel .dscr-grid{grid-template-columns:1fr;}}
+    #dscrPanel .dfield{margin-bottom:10px;} #dscrPanel .dfield>label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);margin-bottom:4px;}
+    #dscrPanel .row2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
+    #dscrPanel .unitrow{display:grid;grid-template-columns:1fr 130px 32px;gap:8px;align-items:center;margin-bottom:6px;}
+    #dscrPanel .unitrow button{background:transparent;border:1px solid var(--line);color:var(--mut);border-radius:7px;cursor:pointer;font-size:15px;line-height:1;padding:7px 0;}
+    #dscrPanel .unitrow button:hover{border-color:var(--bad);color:var(--bad);}
+    #dscrPanel .addunit{background:transparent;border:1px dashed var(--line);color:var(--blue);border-radius:8px;padding:6px 11px;font-size:12px;cursor:pointer;margin-top:2px;}
+    #dscrPanel .addunit:hover{border-color:var(--blue);}
+    #dscrPanel .rentsum{font-size:12px;color:var(--mut);margin-top:7px;} #dscrPanel .rentsum b{color:var(--acc);font-variant-numeric:tabular-nums;}
+    #dscrPanel .dtog{display:inline-flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;}
+    #dscrPanel .dtog button{background:var(--bg);border:0;color:var(--mut);padding:6px 12px;font-size:12px;cursor:pointer;} #dscrPanel .dtog button.on{background:rgba(210,153,34,.16);color:var(--warn);}
+    #dscrPanel .dresult{background:var(--bg);border:1px solid var(--line);border-radius:12px;padding:18px 16px;text-align:center;}
+    #dscrPanel .dresult .k{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--mut);}
+    #dscrPanel .dresult .ratio{font-size:48px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1;margin-top:4px;color:var(--ink);}
+    #dscrPanel .dbadge{display:inline-block;margin-top:12px;font-size:13px;font-weight:800;letter-spacing:.4px;text-transform:uppercase;padding:6px 14px;border-radius:20px;}
+    #dscrPanel .dbadge.pass{background:rgba(63,185,80,.15);color:var(--acc);border:1px solid rgba(63,185,80,.5);}
+    #dscrPanel .dbadge.marg{background:rgba(210,153,34,.15);color:var(--warn);border:1px solid rgba(210,153,34,.5);}
+    #dscrPanel .dbadge.fail{background:rgba(248,81,73,.14);color:var(--bad);border:1px solid rgba(248,81,73,.5);}
+    #dscrPanel .brk{font-size:12.5px;color:var(--mut);margin-top:14px;line-height:1.8;text-align:left;}
+    #dscrPanel .brk .r{display:flex;justify-content:space-between;gap:10px;} #dscrPanel .brk b{color:var(--ink);font-variant-numeric:tabular-nums;}
+    #dscrPanel .brk .r.hi b{color:var(--warn);}
+    #dscrPanel pre{white-space:pre-wrap;background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:12px;font:12.5px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--ink);margin:6px 0 0;}
+    #dscrPanel .dactions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;} #dscrPanel .dactions button{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:7px 12px;font-size:12px;cursor:pointer;} #dscrPanel .dactions button:hover{border-color:var(--blue);} #dscrPanel .dactions .prime{background:var(--warn);color:#1a1206;border-color:var(--warn);font-weight:700;}
+    #dscrPanel .dgate{font-size:11px;color:var(--warn);margin-top:8px;}
+  </style>
+  <section id="dscrPanel">
+    <h2>🧮 DSCR Qualifier <span class="c">— does the property qualify itself? no tax returns · rents ÷ PITI</span></h2>
+    <div class="dsub">A <b>DSCR</b> (debt-service-coverage-ratio) loan qualifies on the <b>property's own cash flow</b>, not the borrower's tax returns — the alternative to conventional commercial financing on small multifamily. Enter the rents and the payment: if the current or projected fair-market rents cover the PITI at the lender's ratio, it's approvable.</div>
+    <div class="dscr-grid">
+      <div>
+        <div class="dfield"><label>Property (used in Frank's note)</label><input type="text" id="dAddr" placeholder="e.g. the 95th St 6-unit"></div>
+        <div class="dfield"><label>Rent basis</label>
+          <div class="dtog" id="dRentBasis"><button type="button" class="on" data-v="current">Current rents</button><button type="button" data-v="projected">Projected fair-market</button></div>
+        </div>
+        <div class="dfield"><label>Rent roll — monthly rent per unit</label>
+          <div id="dUnits"></div>
+          <button type="button" class="addunit" id="dAddUnit">+ add unit</button>
+          <div class="rentsum">Gross monthly rents: <b id="dRentSum">—</b> &nbsp;·&nbsp; annual <b id="dRentAnn">—</b></div>
+        </div>
+        <div class="dfield"><label>PITI — the monthly payment</label>
+          <div class="dtog" id="dPitiMode"><button type="button" class="on" data-v="build">Build it</button><button type="button" data-v="direct">Enter directly</button></div>
+        </div>
+        <div id="dBuild">
+          <div class="row2">
+            <div class="dfield"><label>Loan amount</label><input type="number" id="dLoan" value="960000" min="0" step="1000"></div>
+            <div class="dfield"><label>Rate %</label><input type="number" id="dRate" value="7.25" min="0" step="0.05"></div>
+          </div>
+          <div class="row2">
+            <div class="dfield"><label>Amortization (yrs)</label><input type="number" id="dAmort" value="30" min="1" step="1"></div>
+            <div class="dfield"><label>Taxes / mo</label><input type="number" id="dTax" value="1250" min="0" step="10"></div>
+          </div>
+          <div class="row2">
+            <div class="dfield"><label>Insurance / mo</label><input type="number" id="dIns" value="450" min="0" step="10"></div>
+            <div class="dfield"><label>HOA / other / mo</label><input type="number" id="dHoa" value="0" min="0" step="10"></div>
+          </div>
+        </div>
+        <div id="dDirect" style="display:none">
+          <div class="dfield"><label>PITI / mo (all-in payment)</label><input type="number" id="dPiti" value="8200" min="0" step="10"></div>
+        </div>
+        <div class="dfield"><label>Approval DSCR (lender's line)</label><input type="number" id="dThresh" value="1.20" min="0.5" max="3" step="0.05"></div>
+      </div>
+      <div>
+        <div class="dresult">
+          <div class="k">DSCR = rents ÷ PITI</div>
+          <div class="ratio" id="dRatio">—</div>
+          <div id="dBadge" class="dbadge">—</div>
+          <div class="brk" id="dBrk"></div>
+        </div>
+        <div style="margin-top:14px">
+          <div class="k" style="font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--mut)">Frank's note to the referring agent</div>
+          <pre id="dNote"></pre>
+          <div class="dactions"><button type="button" class="prime" id="dCopy">📋 Copy note</button><button type="button" id="dQueue">🎬 Queue to producer</button></div>
+          <div class="dgate">Draft only — sending / producing is gated (queued for Steve, never auto-sent).</div>
+        </div>
+      </div>
+    </div>
+  </section>
+
   <!-- ===== Brokers & agents — front-page contacts (name / company / phone / email) ===== -->
   <style>
     #brokers{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:14px 16px;margin-bottom:18px;}
@@ -651,6 +735,102 @@
 <div id="cmpov"><div id="cmpmodal"><button class="cmpx" aria-label="Close" onclick="document.getElementById('cmpov').classList.remove('on')">✕</button><h3 style="margin:0 0 10px">⚖ Compare shortlist</h3><div id="cmpbody"></div></div></div>
 <script src="finance.js"></script>
 <script src="deal-score.js"></script>
+<script>
+// ── DSCR Qualifier — rents ÷ PITI, no-tax-returns qualification + Frank's referral note ──
+// Self-contained (own helpers, no page-global collisions). Reuses CREFin.debtService (finance.js)
+// so the qualifier and the listing ranker share one amortization — they can never disagree.
+(function(){
+  const panel=document.getElementById('dscrPanel'); if(!panel) return;
+  let basis='current', pitiMode='build';
+  function monthlyPI(loan, ratePct, amortYears){
+    if(!(loan>0)) return 0;
+    if(window.CREFin && CREFin.debtService) return CREFin.debtService(loan, ratePct, amortYears)/12;
+    const r=ratePct/100/12, n=amortYears*12;
+    return r===0 ? loan/n : loan*(r*Math.pow(1+r,n))/(Math.pow(1+r,n)-1);
+  }
+  const num=id=>{ const el=document.getElementById(id); const v=el?parseFloat(el.value):NaN; return isFinite(v)?v:0; };
+  const usd=n=>'$'+Math.round(n).toLocaleString();
+  const DEF=[1850,1850,1850,1850,1850,1850]; // seeds the six-unit story
+  function unitRows(){ return Array.from(document.querySelectorAll('#dUnits .unitrow')); }
+  function grossMonthly(){ return unitRows().reduce((s,r)=>{ const v=parseFloat(r.querySelector('.urent').value); return s+(isFinite(v)?v:0); },0); }
+  function addUnit(rent){
+    const wrap=document.getElementById('dUnits'); const i=unitRows().length+1;
+    const row=document.createElement('div'); row.className='unitrow';
+    row.innerHTML=`<input type="text" class="ulabel" value="Unit ${i}" aria-label="Unit label">`+
+      `<input type="number" class="urent" value="${rent!=null?rent:1800}" min="0" step="25" aria-label="Monthly rent">`+
+      `<button type="button" class="udel" title="Remove unit">×</button>`;
+    row.querySelector('.urent').addEventListener('input',recompute);
+    row.querySelector('.udel').addEventListener('click',()=>{ if(unitRows().length>1){ row.remove(); recompute(); } });
+    wrap.appendChild(row);
+  }
+  function verdict(dscr, thresh){
+    if(dscr==null||!isFinite(dscr)) return {cls:'',txt:'—'};
+    if(dscr>=thresh) return {cls:'pass',txt:'✓ Approves — no tax returns'};
+    if(dscr>=1.0)    return {cls:'marg',txt:'⚠ Marginal — covers, below the line'};
+    return {cls:'fail',txt:'✕ Short — rents don’t cover PITI'};
+  }
+  function buildNote(g){
+    const addr=(document.getElementById('dAddr').value||'').trim()||'that property';
+    const units=unitRows().length, basisWord=basis==='projected'?'projected fair-market':'current';
+    let line, close;
+    if(g.dscr==null){ line='the payment nets to zero'; close='Send me the rent roll and I’ll pencil it.'; }
+    else if(g.dscr>=g.thresh){ line=`a ${g.dscr.toFixed(2)} DSCR — clears the ${g.thresh.toFixed(2)} line with ${usd(g.surplus)}/mo to spare`; close='It qualifies as-is — happy to send terms and get an app started whenever your seller’s ready.'; }
+    else if(g.dscr>=1.0){ line=`a ${g.dscr.toFixed(2)} DSCR — the rents cover the payment but it’s under a typical ${g.thresh.toFixed(2)} approval line`; close=`We can still get there: about ${usd(g.rentsNeeded-g.gross)}/mo more in rent, a bit more down, or a lender at ${g.dscr.toFixed(2)} min gets it done. Want me to run those?`; }
+    else { line=`a ${g.dscr.toFixed(2)} DSCR — the rents fall about ${usd(g.shortfall)}/mo short of the payment`; close=`It doesn’t pencil at asking on rents alone — projected fair-market rents or more down would move it. Want me to model it?`; }
+    return `Hey — thanks for the list. I called the agent on ${addr} and ran DSCR numbers on it as an alternative to commercial financing on that ${units}-unit.\n\n`+
+      `The nice part: it’s a DSCR loan, so no tax returns — it qualifies on the building’s own rents.\n\n`+
+      `  • ${basisWord[0].toUpperCase()+basisWord.slice(1)} rents:  ${usd(g.gross)}/mo  (${usd(g.gross*12)}/yr)\n`+
+      `  • Full payment (PITI):  ${usd(g.piti)}/mo\n`+
+      `  • DSCR:  ${g.dscr==null?'—':g.dscr.toFixed(2)}  →  ${line}\n\n`+
+      `${close}\n\n`+
+      `Happy to run a same-day scenario on any of the others on your list.\n— Frank · your commercial-lending desk`;
+  }
+  function recompute(){
+    const gross=grossMonthly();
+    document.getElementById('dRentSum').textContent=usd(gross);
+    document.getElementById('dRentAnn').textContent=usd(gross*12);
+    let piti;
+    if(pitiMode==='build'){ piti = monthlyPI(num('dLoan'),num('dRate'),num('dAmort')||30) + num('dTax') + num('dIns') + num('dHoa'); }
+    else { piti = num('dPiti'); }
+    const thresh=num('dThresh')||1.20;
+    const dscr = piti>0 ? gross/piti : null;
+    const g={ gross, piti, thresh, dscr, surplus: gross-piti, shortfall: piti-gross,
+      maxPiti: thresh>0 ? gross/thresh : 0, rentsNeeded: piti*thresh };
+    document.getElementById('dRatio').textContent = dscr==null?'—':dscr.toFixed(2);
+    const v=verdict(dscr,thresh), badge=document.getElementById('dBadge');
+    badge.className='dbadge '+v.cls; badge.textContent=v.txt;
+    document.getElementById('dRatio').style.color = v.cls==='pass'?'var(--acc)':v.cls==='marg'?'var(--warn)':v.cls==='fail'?'var(--bad)':'var(--ink)';
+    const gap = dscr==null?'':(dscr>=thresh
+      ? `<div class="r"><span>Monthly surplus over payment</span><b style="color:var(--acc)">${usd(g.surplus)}</b></div>`
+      : `<div class="r"><span>Rents needed to hit ${thresh.toFixed(2)}</span><b>${usd(g.rentsNeeded)}/mo</b></div>`+
+        `<div class="r"><span>${gross>=piti?'Cushion above payment':'Monthly shortfall'}</span><b style="color:${gross>=piti?'var(--acc)':'var(--bad)'}">${usd(Math.abs(g.surplus))}</b></div>`);
+    document.getElementById('dBrk').innerHTML =
+      `<div class="r"><span>Gross monthly rents</span><b>${usd(gross)}</b></div>`+
+      `<div class="r"><span>PITI (payment)</span><b>${usd(piti)}</b></div>`+
+      `<div class="r hi"><span>Max payment these rents support @ ${thresh.toFixed(2)}</span><b>${usd(g.maxPiti)}/mo</b></div>`+
+      gap;
+    document.getElementById('dNote').textContent = buildNote(g);
+    return g;
+  }
+  document.querySelectorAll('#dRentBasis button').forEach(b=>b.onclick=()=>{ basis=b.dataset.v; b.parentNode.querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b)); recompute(); });
+  document.querySelectorAll('#dPitiMode button').forEach(b=>b.onclick=()=>{ pitiMode=b.dataset.v; b.parentNode.querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b)); document.getElementById('dBuild').style.display=pitiMode==='build'?'':'none'; document.getElementById('dDirect').style.display=pitiMode==='direct'?'':'none'; recompute(); });
+  ['dLoan','dRate','dAmort','dTax','dIns','dHoa','dPiti','dThresh','dAddr'].forEach(id=>{ const el=document.getElementById(id); if(el) el.addEventListener('input',recompute); });
+  document.getElementById('dAddUnit').onclick=()=>{ addUnit(1800); recompute(); };
+  document.getElementById('dCopy').onclick=()=>{ const t=document.getElementById('dNote').textContent; navigator.clipboard&&navigator.clipboard.writeText(t); const b=document.getElementById('dCopy'); b.textContent='✓ Copied'; setTimeout(()=>b.textContent='📋 Copy note',1200); };
+  document.getElementById('dQueue').onclick=()=>{
+    const b=document.getElementById('dQueue'); b.disabled=true; b.textContent='queuing…';
+    const g=recompute(), addr=(document.getElementById('dAddr').value||'').trim()||'DSCR scenario';
+    fetch('/api/solicitation/queue',{method:'POST',headers:{'content-type':'application/json'},
+      body:JSON.stringify({tone:'warm',type:'dscr-note',audience:'realtor_dscr',
+        subject:`DSCR note · ${addr} · ${g.dscr==null?'—':g.dscr.toFixed(2)}`,body:document.getElementById('dNote').textContent})})
+      .then(r=>r.json()).then(d=>{ b.textContent=d&&d.ok?('✓ Queued #'+(d.count||'')+' — gated'):'✓ Queued (draft)'; })
+      .catch(()=>{ b.textContent='✓ Queued (local draft)'; })
+      .finally(()=>{ setTimeout(()=>{ b.disabled=false; b.textContent='🎬 Queue to producer'; },2500); });
+  };
+  DEF.forEach(r=>addUnit(r));
+  recompute();
+})();
+</script>
 <!-- href-to-deeper-data primitives (shared atoms). CRCP drives drilling through its
      own native Set-based facet model (data-firmfilter / data-typefilter / data-cityfilter
      → F.firms/F.types/F.cityQuery → render()→stateToURL()); this shared module is loaded

← 143952a deals-flow: map SEC EDGAR filing date to year so 2025/2026 R  ·  back to Commercialrealestate  ·  Keep DSCR Qualifier on crcp.html only; revert index.html add 3d60c86 →