← back to Commercialrealestate
public/deals.html
294 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230a0d13'/%3E%3Ctext x='8' y='12' font-size='11' text-anchor='middle' fill='%23c8a24b'%3E%24%3C/text%3E%3C/svg%3E">
<title>Deal Intelligence — LA County CRE</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="/deal-score.js"></script>
<style>
:root{
--gold:#e9c46a; --g2:#c8a24b; --violet:#8b7cf6; --teal:#2dd4bf; --blue:#60a5fa; --pink:#f472b6; --orange:#fb923c;
--ink:#eef1f7; --muted:#9aa3b4; --glass:rgba(255,255,255,.055); --glassb:rgba(255,255,255,.12);
}
*{box-sizing:border-box;margin:0;padding:0;}
html,body{min-height:100%;background:#070a10;color:var(--ink);font:15px/1.5 -apple-system,'SF Pro Display',Segoe UI,Roboto,sans-serif;-webkit-font-smoothing:antialiased;}
/* ---- 2026 liquid-glass background: drifting gradient-mesh orbs behind frosted glass ---- */
body::before{content:'';position:fixed;inset:-25%;z-index:-2;
background:
radial-gradient(38vw 38vw at 18% 22%, rgba(139,124,246,.40), transparent 60%),
radial-gradient(34vw 34vw at 82% 18%, rgba(45,212,191,.34), transparent 60%),
radial-gradient(40vw 40vw at 72% 86%, rgba(233,196,106,.30), transparent 62%),
radial-gradient(36vw 36vw at 24% 88%, rgba(96,165,250,.30), transparent 60%);
filter:saturate(1.25);animation:drift 26s ease-in-out infinite alternate;}
body::after{content:'';position:fixed;inset:0;z-index:-1;backdrop-filter:blur(70px);background:rgba(7,10,16,.34);}
@keyframes drift{0%{transform:translate3d(0,0,0) scale(1) rotate(0deg);}
50%{transform:translate3d(2%,-2%,0) scale(1.08) rotate(4deg);}100%{transform:translate3d(-2%,2%,0) scale(1.04) rotate(-3deg);}}
.wrap{max-width:1280px;margin:0 auto;padding:32px 32px 60px;}
/* ---- header ---- */
header{display:flex;align-items:center;gap:16px;margin-bottom:22px;flex-wrap:wrap;}
.title h1{font-size:26px;font-weight:800;letter-spacing:-.5px;background:linear-gradient(92deg,#fff,var(--gold) 60%,var(--teal));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
.title p{font-size:12.5px;color:var(--muted);margin-top:2px;}
nav{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap;}
nav a{font-size:12.5px;color:var(--muted);text-decoration:none;padding:7px 13px;border-radius:13px;
background:var(--glass);border:1px solid var(--glassb);backdrop-filter:blur(14px);transition:.2s;}
nav a:hover{color:#fff;border-color:rgba(233,196,106,.5);transform:translateY(-1px);}
/* ---- glass primitives ---- */
.glass{position:relative;background:var(--glass);border:1px solid var(--glassb);border-radius:22px;
backdrop-filter:blur(22px) saturate(1.5);-webkit-backdrop-filter:blur(22px) saturate(1.5);
box-shadow:0 12px 40px rgba(0,0,0,.40), inset 0 1px 0 rgba(255,255,255,.18), inset 0 -1px 0 rgba(0,0,0,.25);
overflow:hidden;opacity:0;transform:translateY(14px);animation:rise .7s cubic-bezier(.2,.7,.2,1) forwards;}
.glass::before{content:'';position:absolute;inset:0;border-radius:22px;pointer-events:none;
background:linear-gradient(120deg, rgba(255,255,255,.14), transparent 32%);}
@keyframes rise{to{opacity:1;transform:none;}}
/* ---- hero stat tiles ---- */
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:15px;margin-bottom:18px;}
.tile{padding:18px 19px;}
.tile .k{font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);}
.tile .v{font-size:33px;font-weight:800;letter-spacing:-.5px;margin-top:6px;font-variant-numeric:tabular-nums;line-height:1;}
.tile .s{font-size:11.5px;color:var(--muted);margin-top:6px;}
.tile .ic{position:absolute;top:15px;right:16px;font-size:19px;opacity:.85;filter:drop-shadow(0 2px 8px rgba(0,0,0,.4));}
.v.gold{background:linear-gradient(90deg,#fff,var(--gold));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
.v.teal{background:linear-gradient(90deg,#fff,var(--teal));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
.v.violet{background:linear-gradient(90deg,#fff,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
.v.blue{background:linear-gradient(90deg,#fff,var(--blue));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
/* ---- chart grid ---- */
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:22px;}
.card{padding:24px 25px 22px;border:2px solid rgba(255,255,255,.4);}
.card h3{font-size:13px;font-weight:700;letter-spacing:.2px;display:flex;align-items:center;gap:8px;margin-bottom:3px;}
.card .sub{font-size:11.5px;color:var(--muted);margin-bottom:12px;}
.card .cw{position:relative;height:260px;}
.c12{grid-column:span 12;} .c8{grid-column:span 8;} .c6{grid-column:span 6;} .c4{grid-column:span 4;} .c7{grid-column:span 7;} .c5{grid-column:span 5;}
@media(max-width:900px){.c8,.c6,.c4,.c7,.c5{grid-column:span 12;}}
.dot{width:9px;height:9px;border-radius:50%;box-shadow:0 0 10px currentColor;}
.foot{margin-top:22px;font-size:11px;color:var(--muted);text-align:center;line-height:1.7;}
.pill{display:inline-block;padding:2px 9px;border-radius:11px;background:var(--glass);border:1px solid var(--glassb);margin:0 3px;}
.ctrls{display:flex;align-items:center;gap:10px;}
.ctrl-btn{font-size:12.5px;color:var(--ink);background:var(--glass);border:1px solid var(--glassb);backdrop-filter:blur(14px);border-radius:13px;padding:7px 13px;cursor:pointer;transition:.2s;}
.ctrl-btn:hover{border-color:rgba(233,196,106,.5);transform:translateY(-1px);}
.ctrl-btn .ico{display:inline-block;}
.ctrl-btn.spin .ico{animation:spin .6s linear;}
@keyframes spin{to{transform:rotate(360deg);}}
.seg-toggle{display:inline-flex;background:var(--glass);border:1px solid var(--glassb);border-radius:13px;overflow:hidden;backdrop-filter:blur(14px);}
.seg-toggle button{background:none;border:0;color:var(--muted);font-size:12px;padding:7px 12px;cursor:pointer;transition:.2s;}
.seg-toggle button.on{background:linear-gradient(90deg,rgba(233,196,106,.28),rgba(45,212,191,.2));color:#fff;}
/* ---- Top deals by score ---- */
.topdeals{margin-top:15px;padding:18px 19px 16px;}
.topdeals h3{font-size:13px;font-weight:700;display:flex;align-items:center;gap:8px;margin-bottom:3px;}
.topdeals .sub{font-size:11.5px;color:var(--muted);margin-bottom:12px;}
.tdlist{display:flex;flex-direction:column;gap:7px;}
.tdrow{display:grid;grid-template-columns:auto 1fr auto auto;align-items:center;gap:12px;
padding:9px 12px;border-radius:13px;background:rgba(255,255,255,.04);border:1px solid var(--glassb);}
.tdrow .sc{font-size:19px;font-weight:800;font-variant-numeric:tabular-nums;width:46px;text-align:center;line-height:1;}
.tdrow .sc small{display:block;font-size:8.5px;font-weight:600;color:var(--muted);letter-spacing:.4px;}
.tdrow .ad{font-size:13px;font-weight:600;}
.tdrow .ad .cy{color:var(--muted);font-weight:400;font-size:11.5px;}
.tdrow .mt{font-size:11px;color:var(--muted);text-align:right;font-variant-numeric:tabular-nums;}
.tdrow .pr{font-size:13px;font-weight:700;color:var(--gold);text-align:right;font-variant-numeric:tabular-nums;}
.tdrow a{color:inherit;text-decoration:none;}
@media(max-width:620px){.tdrow{grid-template-columns:auto 1fr auto;}.tdrow .mt{display:none;}}
/* a11y: respect reduced-motion — kill the drifting gradient + card rise for motion-sensitive users */
@media(prefers-reduced-motion:reduce){
body::before{animation:none;}
.glass{animation:none;opacity:1;transform:none;}
.ctrl-btn.spin .ico{animation:none;}
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="title">
<h1>◆ Deal Intelligence</h1>
<p id="asof">LA County commercial + residential pipeline · live</p>
</div>
<div class="ctrls">
<button id="refresh" class="ctrl-btn btn" title="Re-pull live data + redraw all charts"><span class="ico">↻</span> Refresh</button>
<span class="seg-toggle seg" id="metric" title="Switch the pipeline chart metric">
<button data-m="count" class="on">Deals</button><button data-m="med">Median $</button>
</span>
</div>
<nav>
<a href="/">Properties</a><a href="/map.html">Map</a><a href="/crcp.html">CRCP</a><a href="/rent-rolls.html">Rent Rolls</a>
<a href="/brokers.html">Mind-map</a><a href="/sales.html">Sales</a><a href="/fha-loans.html">FHA Loans</a>
</nav>
</header>
<div class="tiles" id="tiles"></div>
<div class="grid">
<div class="glass card c7" style="animation-delay:.05s"><h3><span class="dot" style="color:var(--gold)"></span>Deal Pipeline by Loan Product</h3>
<div class="sub">How many screened properties fit each category</div><div class="cw"><canvas id="cPipe"></canvas></div></div>
<div class="glass card c5" style="animation-delay:.1s"><h3><span class="dot" style="color:var(--teal)"></span>Portfolio by Property Type</h3>
<div class="sub">Commercial listings by asset class</div><div class="cw"><canvas id="cType"></canvas></div></div>
<div class="glass card c5" style="animation-delay:.15s"><h3><span class="dot" style="color:var(--violet)"></span>Cap-Rate Distribution</h3>
<div class="sub">Where the yield lives across priced deals</div><div class="cw"><canvas id="cCap"></canvas></div></div>
<div class="glass card c7" style="animation-delay:.2s"><h3><span class="dot" style="color:var(--blue)"></span>Median Deal Size by Product</h3>
<div class="sub">Typical ticket per loan product — what you're financing</div><div class="cw"><canvas id="cMed"></canvas></div></div>
<div class="glass card c7" style="animation-delay:.25s"><h3><span class="dot" style="color:var(--orange)"></span>Geographic Concentration</h3>
<div class="sub">Top markets by property count (geocoded)</div><div class="cw"><canvas id="cGeo"></canvas></div></div>
<div class="glass card c5" style="animation-delay:.3s"><h3><span class="dot" style="color:var(--pink)"></span>Price Band by Product</h3>
<div class="sub">p10–median–p90 range — the deal-size spread</div><div class="cw"><canvas id="cBand"></canvas></div></div>
</div>
<div class="glass topdeals" id="topdeals" style="animation-delay:.35s">
<h3><span class="dot" style="color:var(--gold)"></span>Top Deals by Unified Score</h3>
<div class="sub">Highest 0–100 Deal Scores — blend of pro-forma rent yield + cap rate + DSCR-readiness + assumable bonus, re-normalized over present signals. A screen, not an appraisal.</div>
<div class="tdlist" id="tdlist"></div>
</div>
<div class="foot">
<span class="pill">Liquid-glass UI · 2026</span><span class="pill">Chart.js</span><span class="pill">live data</span><br>
Estimates labeled where used (assumable = heuristic; price bands = p10/p90; rent/ROI = Census proxy). Not an appraisal.
</div>
</div>
<script>
const F = { money:n=>(n==null||!isFinite(n))?'—':'$'+(n>=1e9?(n/1e9).toFixed(1)+'B':n>=1e6?(n/1e6).toFixed(2)+'M':n>=1e3?Math.round(n/1e3)+'k':n),
num:n=>{ n=Number(n); return isFinite(n)?n.toLocaleString():'—'; } };
const PAL = ['#e9c46a','#2dd4bf','#8b7cf6','#60a5fa','#f472b6','#fb923c','#34d399','#f87171','#a78bfa','#fbbf24'];
// translucent gradient fill for glass-matched bars
function grad(ctx, c, vertical=true){ if(typeof c!=='string'||!c) c='#888888'; const a=ctx.chart.chartArea; if(!a) return c+'cc';
const g=ctx.chart.ctx.createLinearGradient(vertical?0:a.left,vertical?a.bottom:0,vertical?0:a.right,vertical?a.top:0);
g.addColorStop(0,c+'33'); g.addColorStop(1,c+'ee'); return g; }
Chart.defaults.color='#9aa3b4'; Chart.defaults.borderColor='rgba(255,255,255,.07)';
Chart.defaults.font.family="-apple-system,'SF Pro Display',Segoe UI,Roboto,sans-serif";
const glassTip={backgroundColor:'rgba(16,20,28,.92)',borderColor:'rgba(255,255,255,.14)',borderWidth:1,padding:11,cornerRadius:12,titleColor:'#fff',bodyColor:'#cfd6e4',displayColors:true};
function countUp(el,to,fmt){ if(!isFinite(to))to=0; const dur=900,t0=performance.now();
(function step(t){ const k=Math.min(1,(t-t0)/dur), e=1-Math.pow(1-k,3); el.textContent=fmt(Math.round(to*e)); if(k<1)requestAnimationFrame(step); })(t0); }
let LAST=null, METRIC='count';
function pull(){
const btn=document.getElementById('refresh');
if(btn){ btn.classList.remove('spin'); void btn.offsetWidth; btn.classList.add('spin'); }
return Promise.all([
fetch('/api/segments').then(r=>r.json()).catch(()=>({segments:[]})),
fetch('/api/crcp/stats').then(r=>r.json()).catch(()=>({})),
fetch('/data/ranked.json').then(r=>r.json()).catch(()=>({ranked:[]})),
fetch('/data/map-points.json').then(r=>r.json()).catch(()=>({points:[]})),
fetch('/data/demographics.json').then(r=>r.json()).catch(()=>({byCity:{}})),
]).then(d=>{ LAST=d; draw(); }).catch(e=>{ document.getElementById('asof').textContent='data load error: '+e.message; });
}
function draw(){
const [seg, stats, ranked, mapd, demo]=LAST;
['cPipe','cType','cCap','cMed','cGeo','cBand'].forEach(id=>{ const c=Chart.getChart(id); if(c)c.destroy(); }); // no double-init on redraw
const segs=(seg.segments||[]).map(s=>({...s,props:+s.props||0,agents:+s.agents||0,med:+s.priceMed||0,lo:+s.priceMin||0,hi:+s.priceMax||0}))
.filter(s=>s.props>0).sort((a,b)=>b.props-a.props);
const rows=(ranked.ranked||ranked||[]);
const pts=(mapd.points||[]);
// ---- hero tiles ----
const totalProps=(stats.listings||0)+(stats.sfr||rows.length*0||0)+ (pts.filter(p=>p.kind==='condo').length||0) || pts.length;
const screened = pts.length || (segs.reduce((s,x)=>s+x.props,0));
const agents=(stats.brokers||stats.agents_commercial||0);
const priced = segs.filter(x=>x.med>0); // guard ÷0 → no "$NaN" tile
const medAll = priced.length ? Math.round(priced.reduce((s,x)=>s+x.med,0)/priced.length) : 0;
const tiles=[
{k:'Properties screened',v:screened,cls:'gold',ic:'🏙️',s:'commercial · condo · SFR, geocoded'},
{k:'Segments live',v:segs.length,cls:'teal',ic:'◆',s:'property call segments'},
{k:'Agents in rolodex',v:agents,cls:'violet',ic:'👤',s:(stats.brokers_phone||0)+' w/ phone · '+(stats.brokers_email||0)+' w/ email'},
{k:'Median deal size',v:medAll,cls:'blue',ic:'💰',s:'across all products',fmt:F.money},
];
document.getElementById('tiles').innerHTML=tiles.map((t,i)=>
`<div class="glass tile" style="animation-delay:${i*.05}s"><span class="ic">${t.ic}</span><div class="k">${t.k}</div>`+
`<div class="v ${t.cls}" id="tile${i}">0</div><div class="s">${t.s}</div></div>`).join('');
tiles.forEach((t,i)=>countUp(document.getElementById('tile'+i),t.v,t.fmt||F.num));
// ---- 1. Deal pipeline by loan product (horizontal bars) ----
const pseg=METRIC==='med'?[...segs].sort((a,b)=>b.med-a.med):segs; // toggle: deal count vs median ticket
new Chart(cPipe,{type:'bar',data:{labels:pseg.map(s=>s.label),datasets:[{data:pseg.map(s=>METRIC==='med'?s.med:s.props),
backgroundColor:c=>grad(c,PAL[c.dataIndex%PAL.length],false),borderRadius:9,borderSkipped:false,maxBarThickness:26}]},
options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{...glassTip,
callbacks:{label:c=>METRIC==='med'?` median ${F.money(c.raw)} · ${F.num(pseg[c.dataIndex].props)} props`:` ${F.num(c.raw)} properties · med ${F.money(pseg[c.dataIndex].med)}`}}},
scales:{x:{grid:{color:'rgba(255,255,255,.06)'},ticks:{callback:v=>METRIC==='med'?F.money(v):F.num(v)}},y:{grid:{display:false},ticks:{font:{size:11}}}},
animation:{duration:900,easing:'easeOutQuart'}}});
// ---- 2. Portfolio by property type (doughnut) ----
const bt=stats.byType||{}; const tl=Object.keys(bt), tv=tl.map(k=>bt[k]);
new Chart(cType,{type:'doughnut',data:{labels:tl,datasets:[{data:tv,backgroundColor:PAL.map(c=>c+'dd'),
borderColor:'rgba(10,14,20,.7)',borderWidth:2,hoverOffset:8}]},
options:{responsive:true,maintainAspectRatio:false,cutout:'62%',plugins:{legend:{position:'right',labels:{boxWidth:11,boxHeight:11,padding:12,font:{size:11.5}}},tooltip:{...glassTip,callbacks:{label:c=>` ${c.label}: ${F.num(c.raw)}`}}},animation:{animateRotate:true,duration:1100}}});
// ---- 3. Cap-rate distribution (histogram from ranked) ----
const caps=rows.map(p=>+p.cap_rate).filter(c=>c>0&&c<20);
const buckets=['0–2','2–4','4–6','6–8','8–10','10+']; const bc=[0,0,0,0,0,0];
caps.forEach(c=>{ const i=Math.min(5,Math.floor(c/2)); bc[i]++; });
new Chart(cCap,{type:'bar',data:{labels:buckets,datasets:[{data:bc,backgroundColor:c=>grad(c,'#8b7cf6'),borderRadius:9,borderSkipped:false}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{...glassTip,callbacks:{title:c=>`Cap rate ${c[0].label}%`,label:c=>` ${F.num(c.raw)} deals`}}},
scales:{x:{grid:{display:false},title:{display:true,text:'cap rate %',color:'#9aa3b4',font:{size:10}}},y:{grid:{color:'rgba(255,255,255,.06)'}}},animation:{duration:1000}}});
// ---- 4. Median deal size by product (area line) ----
const ms=[...segs].sort((a,b)=>a.med-b.med);
new Chart(cMed,{type:'line',data:{labels:ms.map(s=>s.label),datasets:[{data:ms.map(s=>s.med),
borderColor:'#60a5fa',backgroundColor:c=>grad(c,'#60a5fa'),fill:true,tension:.4,pointRadius:4,pointBackgroundColor:'#60a5fa',pointBorderColor:'#fff',pointBorderWidth:1.5}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{...glassTip,callbacks:{label:c=>' median '+F.money(c.raw)}}},
scales:{x:{grid:{display:false},ticks:{font:{size:9.5},maxRotation:32,minRotation:18}},y:{grid:{color:'rgba(255,255,255,.06)'},ticks:{callback:v=>F.money(v)}}},animation:{duration:1100}}});
// ---- 5. Geographic concentration (top cities) ----
const cc={}; pts.forEach(p=>{ if(p.city)cc[p.city]=(cc[p.city]||0)+1; });
const top=Object.entries(cc).sort((a,b)=>b[1]-a[1]).slice(0,10);
new Chart(cGeo,{type:'bar',data:{labels:top.map(t=>t[0]),datasets:[{data:top.map(t=>t[1]),
backgroundColor:c=>grad(c,'#fb923c'),borderRadius:8,borderSkipped:false,maxBarThickness:30}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{...glassTip,callbacks:{label:c=>` ${F.num(c.raw)} properties`}}},
scales:{x:{grid:{display:false},ticks:{font:{size:10},maxRotation:36,minRotation:24}},y:{grid:{color:'rgba(255,255,255,.06)'}}},animation:{duration:1000}}});
// ---- 6. Price band by product (floating bars: p10→p90, median marker) ----
const sb=segs.filter(s=>s.hi>s.lo);
new Chart(cBand,{type:'bar',data:{labels:sb.map(s=>s.label),datasets:[
{label:'p10–p90',data:sb.map(s=>[s.lo,s.hi]),backgroundColor:c=>grad(c,PAL[c.dataIndex%PAL.length],false),borderRadius:7,borderSkipped:false,maxBarThickness:20},
]},options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{...glassTip,
callbacks:{label:c=>{const s=sb[c.dataIndex];return [' p10 '+F.money(s.lo),' med '+F.money(s.med),' p90 '+F.money(s.hi)];}}}},
scales:{x:{grid:{color:'rgba(255,255,255,.06)'},ticks:{callback:v=>F.money(v)}},y:{grid:{display:false},ticks:{font:{size:10}}}},animation:{duration:1100}}});
// ---- Top deals by Unified Score (same DealScore.compute as cards + map) ----
renderTopDeals(rows, (demo&&demo.byCity)||{});
document.getElementById('asof').textContent=`LA County pipeline · ${F.num(screened)} properties · ${segs.length} loan products · updated just now`;
}
const TDSCEN={downPct:25,ratePct:6.75,amortYears:30};
function tdDebtSvc(loan){ if(!(loan>0)) return 0; const r=TDSCEN.ratePct/100/12, n=TDSCEN.amortYears*12;
return r===0 ? loan/TDSCEN.amortYears : loan*(r*Math.pow(1+r,n))/(Math.pow(1+r,n)-1)*12; }
function scoreRow(p, DEMO){
const d=DEMO[(p.city||'').toLowerCase()];
let gy=null, proYr=null;
if(d&&d.median_gross_rent&&p.price>0){ const pro=Math.round(d.median_gross_rent*(p.units||1)*1.12); proYr=pro*12; gy=+(100*proYr/p.price).toFixed(2); }
let dscr=p.finance?p.finance.dscr:null, px=null;
if(p.cap_rate==null&&proYr&&p.price>0){ const ads=tdDebtSvc(p.price*(1-TDSCEN.downPct/100)); if(ads>0) px=+((proYr*0.6)/ads).toFixed(2); }
return DealScore.compute({grossYieldPro:gy,cap_rate:p.cap_rate,verified:p.verified,dscr,proxyDscr:px,assum:p.assum,fhaApproved:p.fhaApproved});
}
function renderTopDeals(rows, DEMO){
const scored=rows.map(p=>({p,d:scoreRow(p,DEMO)})).filter(x=>x.d&&x.d.score!=null)
.sort((a,b)=>b.d.score-a.d.score).slice(0,12);
const el=document.getElementById('tdlist');
if(!scored.length){ el.innerHTML='<div style="color:var(--muted);font-size:12px;padding:8px">No scorable deals in the current dataset.</div>'; return; }
el.innerHTML=scored.map(({p,d})=>{
const col=DealScore.scoreColor(d.score);
const cap=p.cap_rate!=null?`${p.cap_rate}% cap`:'cap n/d';
const dscr=p.finance&&p.finance.dscr!=null?` · ${p.finance.dscr} DSCR`:'';
const conf=(d.confidence==='low'||d.confidence==='thin')?' ?':'';
const tip=d.parts.map(pt=>`${pt.label}: ${Math.round(pt.score)}/100 (${pt.conf})`).join(' · ');
return `<div class="tdrow" title="${tip.replace(/"/g,'"')}">`+
`<div class="sc" style="color:${col}">${d.score}${conf}<small>SCORE</small></div>`+
`<div class="ad"><a href="/index.html" title="open in the property explorer">${p.address||'(addr n/a)'}</a> <span class="cy">${p.city||''}</span></div>`+
`<div class="mt">${p.type||''} · ${p.units||'?'}u<br>${cap}${dscr}</div>`+
`<div class="pr">${F.money(p.price)}</div></div>`;
}).join('');
}
// ---- controls (real, auto-discoverable) ----
document.getElementById('refresh').addEventListener('click', pull);
document.getElementById('metric').addEventListener('click', e=>{
const b=e.target.closest('button'); if(!b) return;
document.querySelectorAll('#metric button').forEach(x=>x.classList.toggle('on', x===b));
METRIC=b.dataset.m; if(LAST) draw();
});
pull(); // initial load
</script>
<script src="/col-resize.js" defer></script>
</body>
</html>