← back to Commercialrealestate

public/valley-pools.html

353 lines

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='light'?'light':'dark');}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();</script>
<!-- Self-heal: nav-agent grid controls can persist a "removed"/"collapsed" state that hides the whole grid. Clear it before nav-agent runs so a grid can never stay hidden. -->
<script>(function(){try{for(var i=localStorage.length-1;i>=0;i--){var k=localStorage.key(i);if(k&&k.indexOf('navagent:')===0){var v=localStorage.getItem(k);if(v&&v.indexOf('"removed":true')>=0){localStorage.removeItem(k);}}}}catch(e){}})();</script>
<script>/* credential-safe fetch (2026-08-18): resolve EVERY relative fetch against a credential-free base so creds in the page URL (http://user:pass@host/...) don't poison document.baseURI and throw. */(function(){var of=window.fetch,O=location.origin;window.fetch=function(u,o){try{if(typeof u==='string'){if(/^[a-z][a-z0-9+.-]*:/i.test(u)){var x=new URL(u);if(x.username||x.password){x.username='';x.password='';u=x.href;}}else{u=new URL(u,O+location.pathname+location.search).href;}}else if(u&&typeof u.url==='string'){var y=new URL(u.url,O+location.pathname+location.search);if(y.username||y.password){y.username='';y.password='';u=new Request(y.href,u);}}}catch(e){}return of.call(this,u,o);};try{if(location.href.indexOf('@')>=0){history.replaceState(null,'',location.pathname+location.search+location.hash);}}catch(e){}})();</script>
<title>San Fernando Valley · Pool Homes under $1.2M — CRCP</title>
<link rel="stylesheet" href="/vendor/leaflet/leaflet.css">
<style>
  :root{--acc-b:#79b8ff;--cols:4;
    --t-best:#ff5c8a;--t-value:#4bd268;--t-cut:#e3b341;--t-nego:#79b8ff;--t-mkt:#7d8896;}
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg,#0d1117);color:var(--ink,#f2f6fb);font-family:-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;font-size:13px;-webkit-font-smoothing:antialiased}
  a{color:var(--blue,#79b8ff);text-decoration:none}
  header{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;padding:11px 16px;border-bottom:1px solid var(--line);background:var(--headbg,rgba(13,17,23,.98));position:sticky;top:0;z-index:1200}
  header h1{font-size:15px;margin:0;font-weight:700;letter-spacing:.01em}
  header .wm{font-weight:800;letter-spacing:.14em;font-size:14px}
  header .wm .dot{color:var(--t-value)}
  .badge{font-size:11px;color:var(--mut);border:1px solid var(--line);border-radius:20px;padding:2px 9px}
  .badge b{color:var(--gold)}
  header .nav{margin-left:auto;display:flex;gap:12px}
  header .nav a{font-size:13px}
  .shell{display:flex;align-items:flex-start}
  .rail{width:238px;flex:0 0 238px;border-right:1px solid var(--line);padding:12px;height:calc(100vh - 50px);overflow:auto;position:sticky;top:50px}
  .rsec{margin-bottom:15px}
  .rsec h4{margin:0 0 7px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);font-weight:700}
  .chips{display:flex;flex-wrap:wrap;gap:6px}
  .chip{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:16px;padding:4px 10px;font-size:12px;cursor:pointer;user-select:none;display:inline-flex;align-items:center;gap:5px}
  .chip:hover{border-color:var(--blue)}
  .chip.active{background:var(--blue);color:var(--onacc);border-color:var(--blue);font-weight:600}
  .chip .ct{font-size:10px;opacity:.7}
  .chip[data-deal].active{color:var(--onacc)}
  .chip.c-best.active{background:var(--t-best);border-color:var(--t-best)}
  .chip.c-value.active{background:var(--t-value);border-color:var(--t-value)}
  .chip.c-cut.active{background:var(--t-cut);border-color:var(--t-cut)}
  .chip.c-nego.active{background:var(--t-nego);border-color:var(--t-nego)}
  .rsec input,.rsec select{width:100%;background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 9px;font-size:13px;font-family:inherit}
  .rsec .two{display:flex;gap:8px}
  .rsec .two input{width:50%}
  .legend{display:flex;flex-direction:column;gap:5px;font-size:11px;color:var(--mut)}
  .legend .lg{display:flex;align-items:center;gap:6px}
  .legend .sw{width:11px;height:11px;border-radius:3px;flex:0 0 11px}
  .main{flex:1;min-width:0;padding:12px 16px 60px}
  .prov{color:var(--mut);font-size:11px;line-height:1.5;margin-bottom:10px}
  .prov b{color:var(--ink)}
  .stats{display:flex;gap:18px;flex-wrap:wrap;margin-bottom:10px}
  .stat{display:flex;flex-direction:column}.stat b{font-size:19px;line-height:1}.stat span{font-size:11px;color:var(--mut)}
  .stat.best b{color:var(--t-best)}.stat.value b{color:var(--t-value)}.stat.cut b{color:var(--t-cut)}
  .toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
  .toolbar .search{flex:1;min-width:170px;background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font-size:13px}
  .toolbar select,.toolbar input[type=number]{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:6px 9px;font-size:13px}
  .toolbar label{font-size:11px;color:var(--mut);display:flex;align-items:center;gap:5px}
  .count{font-size:12px;color:var(--mut)}
  #mapBtn{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:8px;padding:6px 11px;font-size:12px;cursor:pointer}
  #mapBtn.on{background:var(--t-nego);color:var(--onacc);font-weight:600;border-color:var(--t-nego)}
  /* grid cards */
  #gridView{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:13px}
  .card{background:var(--card);border:1px solid var(--line);border-left:4px solid var(--t-mkt);border-radius:11px;overflow:hidden;display:flex;flex-direction:column;transition:transform .12s,box-shadow .12s}
  .card:hover{transform:translateY(-2px)}
  .card.t-best{border-left-color:var(--t-best)}.card.t-best:hover{box-shadow:0 0 0 1px var(--t-best)}
  .card.t-value{border-left-color:var(--t-value)}.card.t-value:hover{box-shadow:0 0 0 1px var(--t-value)}
  .card.t-cut{border-left-color:var(--t-cut)}.card.t-cut:hover{box-shadow:0 0 0 1px var(--t-cut)}
  .card.t-nego{border-left-color:var(--t-nego)}
  .card .top{padding:11px 12px;display:flex;flex-direction:column;gap:5px;flex:1}
  .price{font-size:18px;font-weight:800;display:flex;align-items:baseline;gap:7px}
  .wasp{font-size:11px;font-weight:600;color:var(--mut);text-decoration:line-through}
  .addr{font-size:13px;font-weight:600;line-height:1.3}
  .loc{color:var(--mut);font-size:12px}
  .facts{display:flex;gap:9px;flex-wrap:wrap;color:var(--mut);font-size:12px}
  .facts b{color:var(--ink)}
  .pay{font-size:12px;color:var(--mut)}.pay b{color:var(--blue)}
  .badges{display:flex;gap:5px;flex-wrap:wrap}
  .pill{font-size:10px;padding:2px 7px;border-radius:16px;font-weight:700;border:1px solid transparent}
  .p-best{background:rgba(255,92,138,.16);color:var(--t-best);border-color:rgba(255,92,138,.4)}
  .p-value{background:rgba(75,210,104,.16);color:var(--t-value);border-color:rgba(75,210,104,.4)}
  .p-cut{background:rgba(227,179,65,.16);color:var(--t-cut);border-color:rgba(227,179,65,.4)}
  .p-nego{background:rgba(121,184,255,.14);color:var(--t-nego);border-color:rgba(121,184,255,.35)}
  .p-pool{background:rgba(75,210,104,.10);color:var(--t-value);border-color:rgba(75,210,104,.25)}
  .p-reg{background:rgba(125,136,150,.14);color:var(--mut);border-color:rgba(125,136,150,.3)}
  .card .foot{border-top:1px solid var(--line);padding:8px 12px;display:flex;justify-content:space-between;font-size:12px}
  .card .foot .dom{color:var(--mut)}
  .notes{border-top:1px solid var(--line);padding:7px 12px}
  .note-btn{background:none;border:0;color:var(--mut);font-size:12px;cursor:pointer;padding:0;font-family:inherit}
  .note-btn:hover{color:var(--blue)}
  .card.has-note .note-btn{color:var(--gold)}
  .note-edit{margin-top:6px}
  .note-edit textarea{width:100%;min-height:54px;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:6px;font:inherit;font-size:12px;resize:vertical}
  .note-row{display:flex;align-items:center;gap:8px;margin-top:5px}
  .note-save{background:var(--blue);color:var(--onacc);border:0;border-radius:6px;padding:4px 10px;font-size:12px;font-weight:600;cursor:pointer}
  .note-meta{font-size:11px;color:var(--mut)}
  /* table */
  #tableView table{width:100%;border-collapse:collapse}
  #tableView th{position:sticky;top:0;background:var(--card);text-align:left;padding:7px 9px;font-size:11px;color:var(--mut);border-bottom:1px solid var(--line);cursor:pointer;white-space:nowrap}
  #tableView td{padding:6px 9px;border-bottom:1px solid var(--line);white-space:nowrap}
  #tableView td.mono{font-variant-numeric:tabular-nums}
  #tableView tr.r-best td:first-child{box-shadow:inset 3px 0 var(--t-best)}
  #tableView tr.r-value td:first-child{box-shadow:inset 3px 0 var(--t-value)}
  #tableView tr.r-cut td:first-child{box-shadow:inset 3px 0 var(--t-cut)}
  #tableView tr.r-nego td:first-child{box-shadow:inset 3px 0 var(--t-nego)}
  #map{height:calc(100vh - 170px);min-height:420px;border:1px solid var(--line);border-radius:11px;display:none}
  body.mapmode #tableView,body.mapmode #gridView,body.mapmode #listView{display:none!important}
  body.mapmode #map{display:block}
  .price-lbl{background:rgba(13,17,23,.82);border:1px solid var(--line);border-radius:5px;padding:0 4px;font-size:10.5px;font-weight:800;line-height:1.5;color:var(--ink);box-shadow:none;white-space:nowrap}
  .price-lbl::before{display:none}
  html[data-theme="light"] .price-lbl{background:rgba(255,255,255,.9)}
  .price-lbl.t-best{color:var(--t-best);border-color:var(--t-best)}
  .price-lbl.t-value{color:var(--t-value);border-color:var(--t-value)}
  .price-lbl.t-cut{color:var(--t-cut);border-color:var(--t-cut)}
  .price-lbl.t-nego{color:var(--t-nego)}
  @media(max-width:900px){.rail{display:none}}
</style>
<link rel="stylesheet" href="/three-view.css">
<link rel="stylesheet" href="/nav-agent/nav-agent.css">
  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
  <span class="wm">CRCP<span class="dot">.</span></span>
  <h1>San Fernando Valley — Pool Homes</h1>
  <span class="badge">Single-family · pool · SFV · under <b>$1.2M</b></span>
  <div class="nav"><a href="/mls.html">← Listings</a><a href="/">Explorer</a></div>
</header>
<div class="shell">
  <aside class="rail">
    <div class="rsec"><h4>Max price</h4>
      <select id="maxPrice"><option value="1200000">$1,200,000</option><option value="1000000">$1,000,000</option><option value="900000">$900,000</option><option value="800000">$800,000</option><option value="700000">$700,000</option><option value="600000">$600,000</option></select></div>
    <div class="rsec"><h4>Beds / baths (min)</h4><div class="two"><input type="number" id="minBeds" placeholder="beds" min="0" step="1"><input type="number" id="minBaths" placeholder="baths" min="0" step="1"></div></div>
    <div class="rsec"><h4>Deal tier</h4><div class="chips" id="fDeal">
      <span class="chip c-best" data-deal="best">🔥 Best</span>
      <span class="chip c-value" data-deal="value">🟢 Value</span>
      <span class="chip c-cut" data-deal="cut">🟠 Cut</span>
      <span class="chip c-nego" data-deal="nego">🔵 Nego</span></div></div>
    <div class="rsec"><h4>Region</h4><div class="chips" id="fRegion"></div></div>
    <div class="rsec"><h4>Mortgage estimate</h4><div class="two"><input type="number" id="rate" value="6.75" step="0.125" title="rate %"><input type="number" id="down" value="20" step="5" title="down %"></div>
      <div style="font-size:10px;color:var(--mut);margin-top:4px">rate % · down %</div></div>
    <div class="rsec"><h4>Deal colors</h4><div class="legend">
      <span class="lg"><span class="sw" style="background:var(--t-best)"></span>🔥 Best — below-market & cut</span>
      <span class="lg"><span class="sw" style="background:var(--t-value)"></span>🟢 Value — ≥12% under area $/sf</span>
      <span class="lg"><span class="sw" style="background:var(--t-cut)"></span>🟠 Price cut</span>
      <span class="lg"><span class="sw" style="background:var(--t-nego)"></span>🔵 Negotiable — 30+ days</span>
      <span class="lg"><span class="sw" style="background:var(--t-mkt)"></span>⚪ Market-priced</span></div></div>
  </aside>
  <div class="main">
    <div class="stats" id="stats"></div>
    <div class="prov" id="prov">Active single-family homes with a pool in the San Fernando Valley under $1,200,000. Feed-first ($0) from Redfin gis-csv (&pool=1 verified has-pool filter); price-history + $/sqft deal-score joined from the CRE sweep table. Payment shown is an estimate only.</div>
    <div class="toolbar">
      <input class="search" id="q" placeholder="search address, city, zip, region…">
      <span class="viewseg tv-seg" id="viewseg"></span>
      <button id="mapBtn" title="Map with price tags">◱ Map</button>
      <select id="sort"></select>
      <label>density <input type="range" id="dens" min="1" max="8" step="1"></label>
      <span class="count" id="count"></span>
    </div>
    <div id="tableView"><table><thead id="head"></thead><tbody id="body"></tbody></table></div>
    <div id="gridView"></div>
    <div id="listView"></div>
    <div id="map"></div>
  </div>
</div>
<link rel="stylesheet" href="/crcp-theme.css">
<script src="/vendor/leaflet/leaflet.js"></script>
<script src="/three-view.js"></script>
<script>
const $=s=>document.querySelector(s);
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const fmt=n=>n==null?'—':'$'+Number(n).toLocaleString();
const num=n=>n==null?'—':Number(n).toLocaleString();

let DATA=[],META={},CURRENT=[],TV=null,map=null,layer=null,q='',NOTES={};
const fmtDate=x=>{try{return new Date(x).toLocaleString(undefined,{dateStyle:'medium',timeStyle:'short'})}catch(e){return ''}};
let sortKey='deal',sortDir=1;
let dens=(function(){const v=parseInt(localStorage.getItem('vpDens'),10);return(v>=1&&v<=8)?v:4;})();
const F={maxPrice:1200000,minBeds:null,minBaths:null,region:new Set(),deal:new Set(),rate:6.75,down:20};

// ---- deal tier (priority cascade) ----
function tier(h){const cut=h.price_cut>0,val=h.value_buy,nego=(h.days_on_market!=null&&h.days_on_market>=30);
  return (cut&&val)?'best':val?'value':cut?'cut':nego?'nego':'mkt';}
const TIERRANK={best:0,value:1,cut:2,nego:3,mkt:4};
const TIERCOLOR={best:'#ff5c8a',value:'#4bd268',cut:'#e3b341',nego:'#79b8ff',mkt:'#7d8896'};
function pmt(h){const rate=F.rate/100,down=F.down/100,P=h.price*(1-down),r=rate/12,n=360;
  const pi=r>0?P*r*Math.pow(1+r,n)/(Math.pow(1+r,n)-1):P/n;
  return Math.round(pi+h.price*0.011/12+(h.sqft?h.sqft*0.30:2400)/12);}

// ---- columns (drive table + list + nav-agent field toggles + sort menu) ----
const COLS=[
  {k:'deal',l:'Deal',t:'deal',calc:r=>tier(r)},
  {k:'price',l:'Price',t:'money'},
  {k:'prev_price',l:'Was',t:'money'},
  {k:'price_cut',l:'Cut',t:'money'},
  {k:'ppsf',l:'$/sf',t:'money'},
  {k:'ppsf_vs_region',l:'vs area',t:'pct'},
  {k:'pmt',l:'Est /mo',t:'money',calc:r=>pmt(r)},
  {k:'beds',l:'Bd',t:'n'},{k:'baths',l:'Ba',t:'n'},{k:'sqft',l:'SqFt',t:'n'},{k:'year_built',l:'Built',t:'y'},
  {k:'days_on_market',l:'DOM',t:'n'},
  {k:'address',l:'Address',t:'s'},{k:'city',l:'City',t:'s'},{k:'zip',l:'Zip',t:'s'},{k:'region',l:'Region',t:'s'}
];
const cval=(r,c)=>c.calc?c.calc(r):r[c.k];
function cell(r,c){const v=cval(r,c);
  if(c.k==='deal'){const t=v,lbl={best:'🔥 Best',value:'🟢 Value',cut:'🟠 Cut',nego:'🔵 Nego',mkt:'⚪ Market'}[t];return `<span class="pill p-${t==='mkt'?'reg':t}">${lbl}</span>`;}
  if(c.t==='money')return v==null||v===0&&c.k==='price_cut'?'—':(v==null?'—':'$'+Number(v).toLocaleString());
  if(c.t==='pct')return v==null?'—':`<span style="color:${v<0?'var(--t-value)':'var(--mut)'}">${v>0?'+':''}${v}%</span>`;
  if(c.t==='n')return v==null?'—':(+v).toLocaleString();
  if(c.t==='y')return v==null?'—':v;
  return esc(v==null||v===''?'—':v);
}

function pass(r){
  if(r.price>F.maxPrice)return false;
  if(F.minBeds!=null&&!((r.beds||0)>=F.minBeds))return false;
  if(F.minBaths!=null&&!((r.baths||0)>=F.minBaths))return false;
  if(F.region.size&&!F.region.has(r.region))return false;
  if(F.deal.size&&!F.deal.has(tier(r)))return false;
  if(q){const t=q.toLowerCase();return (r.address+' '+r.city+' '+r.zip+' '+r.region).toLowerCase().includes(t);}
  return true;
}
function sorted(rows){
  if(sortKey==='deal') return rows.slice().sort((a,b)=>TIERRANK[tier(a)]-TIERRANK[tier(b)]||(a.ppsf_vs_region??0)-(b.ppsf_vs_region??0)||a.price-b.price);
  const c=COLS.find(x=>x.k===sortKey)||COLS[1];
  return rows.slice().sort((a,b)=>{let x=cval(a,c),y=cval(b,c);const xm=(x==null||x===''),ym=(y==null||y==='');
    if(xm&&ym)return 0;if(xm)return 1;if(ym)return -1;
    if(!isNaN(+x)&&!isNaN(+y)){x=+x;y=+y;}else{x=String(x).toLowerCase();y=String(y).toLowerCase();}
    return x<y?-1*sortDir:x>y?sortDir:0;});
}

function cardHTML(h){
  const t=tier(h),pp=h.ppsf?('$'+h.ppsf.toLocaleString()+'/sf'):'';
  const vs=h.ppsf_vs_region!=null?` <span style="color:${h.ppsf_vs_region<0?'var(--t-value)':'var(--mut)'}">(${h.ppsf_vs_region>0?'+':''}${h.ppsf_vs_region}% vs area)</span>`:'';
  const b=[];
  if(t==='best')b.push('<span class="pill p-best">🔥 Best deal</span>');
  else if(t==='value')b.push('<span class="pill p-value">🟢 Value buy</span>');
  else if(t==='cut')b.push('<span class="pill p-cut">🟠 Price cut</span>');
  else if(t==='nego')b.push('<span class="pill p-nego">🔵 Negotiable</span>');
  if(h.price_cut>0&&t!=='cut'&&t!=='best')b.push('<span class="pill p-cut">↓ '+fmt(h.price_cut)+'</span>');
  if(h.has_pool)b.push('<span class="pill p-pool">◔ Pool</span>');
  b.push('<span class="pill p-reg">'+esc(h.region)+'</span>');
  return `<div class="top">
    <div class="price">${fmt(h.price)}${h.price_cut>0?`<span class="wasp">${fmt(h.prev_price)}</span>`:''}</div>
    <div class="addr">${esc(h.address)}</div><div class="loc">${esc(h.city)} ${esc(h.zip)}</div>
    <div class="facts"><span><b>${num(h.beds)}</b> bd</span><span><b>${num(h.baths)}</b> ba</span><span><b>${num(h.sqft)}</b> sf</span>${h.year_built?`<span>built <b>${h.year_built}</b></span>`:''}${pp?`<span><b>${pp}</b></span>${vs}`:''}</div>
    <div class="pay">est <b>$${pmt(h).toLocaleString()}/mo</b> · ${F.down}% down @ ${F.rate}%</div>
    <div class="badges">${b.join('')}</div></div>
    <div class="foot"><span class="dom">${h.days_on_market!=null?h.days_on_market+(h.days_on_market===1?' day':' days')+' on market':'—'}</span>${h.source?`<a href="${h.source}" target="_blank" rel="noopener noreferrer">Redfin ↗</a>`:''}</div>
    <div class="notes" data-noteid="${esc(h.id)}">
      <button class="note-btn">📝 ${NOTES[h.id]?'Note':'Add note'}</button>
      <div class="note-edit" hidden><textarea placeholder="Private note for this listing…">${esc(NOTES[h.id]?NOTES[h.id].note:'')}</textarea>
        <div class="note-row"><button class="note-save">Save</button><span class="note-meta">${NOTES[h.id]?'saved '+fmtDate(NOTES[h.id].updated_at):''}</span></div></div>
    </div>`;
}
function renderGrid(rows){const gv=$('#gridView');gv.style.setProperty('--cols',dens);
  gv.innerHTML=rows.map(h=>`<div class="card t-${tier(h)}${NOTES[h.id]?' has-note':''}">${cardHTML(h)}</div>`).join('')||'<div style="color:var(--mut);padding:30px">No matches for these filters.</div>';}
// per-card notes (delegated) — save to /api/sfr-notes/:id (per-user via the gate:<user> fallback)
$('#gridView').addEventListener('click',async e=>{
  const box=e.target.closest('.notes'); if(!box)return; const id=box.dataset.noteid; const card=box.closest('.card');
  if(e.target.closest('.note-btn')){const ed=box.querySelector('.note-edit');ed.hidden=!ed.hidden;if(!ed.hidden)box.querySelector('textarea').focus();return;}
  if(e.target.closest('.note-save')){
    const note=box.querySelector('textarea').value, meta=box.querySelector('.note-meta'); meta.textContent='saving…';
    try{const r=await fetch('/api/sfr-notes/'+encodeURIComponent(id),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({note})});
      const j=await r.json();
      if(j.ok){ if(j.note)NOTES[id]=j.note; else delete NOTES[id];
        meta.textContent=j.note?'saved '+fmtDate(j.note.updated_at):'cleared';
        box.querySelector('.note-btn').textContent='📝 '+(NOTES[id]?'Note':'Add note');
        card.classList.toggle('has-note',!!NOTES[id]); }
      else meta.textContent=j.error||(j.signed_in===false?'sign in to save':'error');
    }catch(err){meta.textContent='error saving';}
  }
});
function renderTable(rows){
  $('#head').innerHTML='<tr>'+COLS.map(c=>`<th data-k="${c.k}">${esc(c.l)}${sortKey===c.k?(sortDir<0?' ↓':' ↑'):''}</th>`).join('')+'</tr>';
  const fs=Math.max(10,15-dens);
  $('#body').innerHTML=rows.map(h=>`<tr class="r-${tier(h)}" style="font-size:${fs}px">`+COLS.map(c=>`<td class="${['money','n','pct','y'].includes(c.t)?'mono':''}">${cell(h,c)}</td>`).join('')+'</tr>').join('');
  $('#head').querySelectorAll('th').forEach(th=>th.addEventListener('click',()=>{const k=th.dataset.k;if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=(['address','city','region','zip','deal'].includes(k))?1:-1;}$('#sort').value=k;render();}));
}

function renderMap(rows){
  if(!map){map=L.map('map',{scrollWheelZoom:true}).setView([34.19,-118.45],11);
    const dark=document.documentElement.getAttribute('data-theme')!=='light';
    L.tileLayer(dark?'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png':'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{attribution:'© OpenStreetMap © CARTO',maxZoom:19}).addTo(map);}
  if(layer)layer.remove();layer=L.layerGroup().addTo(map);
  const pts=[],shortP=n=>n>=1e6?('$'+(n/1e6).toFixed(n%1e6?2:1).replace(/\.0$/,'')+'M'):('$'+Math.round(n/1000)+'k');
  for(const h of rows){if(h.lat==null||h.lng==null)continue;const t=tier(h),c=TIERCOLOR[t];
    const m=L.circleMarker([h.lat,h.lng],{radius:t==='mkt'?5:7,color:c,weight:2,fillColor:c,fillOpacity:t==='mkt'?.35:.75});
    m.bindTooltip(shortP(h.price),{permanent:true,direction:'right',offset:[7,0],className:'price-lbl t-'+t});
    m.bindPopup(`<b>${fmt(h.price)}</b>${h.price_cut>0?` <s style="color:#888">${fmt(h.prev_price)}</s>`:''}<br>${esc(h.address)}, ${esc(h.city)}<br>${num(h.beds)}bd/${num(h.baths)}ba · ${num(h.sqft)}sf${h.ppsf?' · $'+h.ppsf+'/sf':''}<br>est $${pmt(h).toLocaleString()}/mo${h.source?`<br><a href="${h.source}" target="_blank" rel="noopener">View on Redfin ↗</a>`:''}`);
    m.addTo(layer);pts.push([h.lat,h.lng]);}
  if(pts.length){try{map.fitBounds(pts,{padding:[30,30],maxZoom:13})}catch(e){}}
  setTimeout(()=>map.invalidateSize(),60);
}

function render(){
  CURRENT=sorted(DATA.filter(pass));
  const rows=CURRENT;
  const nb=rows.filter(r=>tier(r)==='best').length,nv=rows.filter(r=>r.value_buy).length,nc=rows.filter(r=>r.price_cut>0).length;
  const prices=rows.map(r=>r.price).sort((a,b)=>a-b),med=prices.length?prices[Math.floor(prices.length/2)]:0;
  $('#stats').innerHTML=`<div class="stat"><b>${rows.length}</b><span>homes shown</span></div>`+
    `<div class="stat"><b>${prices.length?fmt(prices[0]).replace('$','$'):'—'}</b><span>lowest</span></div>`+
    `<div class="stat"><b>${fmt(med)}</b><span>median</span></div>`+
    `<div class="stat best"><b>${nb}</b><span>🔥 best</span></div>`+
    `<div class="stat value"><b>${nv}</b><span>🟢 value</span></div>`+
    `<div class="stat cut"><b>${nc}</b><span>🟠 cut</span></div>`;
  $('#count').textContent=rows.length+' of '+DATA.length;
  if(TV)TV.render();else{renderGrid(rows);renderTable(rows);}
  if(document.body.classList.contains('mapmode'))renderMap(rows);
}

// ---- rail: region chips ----
function buildRail(){
  const rc={};DATA.forEach(r=>{if(r.region)rc[r.region]=(rc[r.region]||0)+1;});
  $('#fRegion').innerHTML=Object.entries(rc).sort((a,b)=>b[1]-a[1]).map(([k,c])=>`<span class="chip" data-reg="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
}
function tog(set,v){set.has(v)?set.delete(v):set.add(v);}
$('#fRegion').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.region,c.dataset.reg);c.classList.toggle('active');render();});
$('#fDeal').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.deal,c.dataset.deal);c.classList.toggle('active');render();});
$('#maxPrice').addEventListener('change',e=>{F.maxPrice=+e.target.value;render();});
$('#minBeds').addEventListener('input',e=>{F.minBeds=e.target.value?+e.target.value:null;render();});
$('#minBaths').addEventListener('input',e=>{F.minBaths=e.target.value?+e.target.value:null;render();});
$('#rate').addEventListener('input',e=>{F.rate=+e.target.value||6.75;render();});
$('#down').addEventListener('input',e=>{F.down=+e.target.value||0;render();});
$('#q').addEventListener('input',e=>{q=e.target.value.trim();render();});
$('#sort').addEventListener('change',e=>{sortKey=e.target.value;sortDir=(['address','city','region','zip','deal'].includes(sortKey))?1:-1;render();});
$('#dens').addEventListener('input',e=>{dens=+e.target.value;localStorage.setItem('vpDens',dens);render();});
$('#mapBtn').addEventListener('click',()=>{const on=document.body.classList.toggle('mapmode');$('#mapBtn').classList.toggle('on',on);if(on){renderMap(CURRENT);setTimeout(()=>map&&map.invalidateSize(),80);}});

// sort menu
$('#sort').innerHTML=`<option value="deal">Sort: Best deals</option>`+COLS.filter(c=>c.k!=='deal').map(c=>`<option value="${c.k}">Sort: ${esc(c.l)}</option>`).join('');
$('#dens').value=dens;

Promise.all([
  fetch('/data/valley-pools.json').then(r=>r.json()),
  fetch('/api/sfr-notes').then(r=>r.json()).catch(()=>({notes:{}}))
]).then(([j,n])=>{
  DATA=j.homes||[];META=j;NOTES=(n&&n.notes)||{};buildRail();
  // three-view (grid/list/table) — sort owned by the page (#sort + header clicks); TV adds view modes + compact list + windowing
  TV=ThreeView.mount({seg:'#viewseg',gridMount:'#gridView',listMount:'#listView',tableMount:'#tableView',
    getRows:()=>CURRENT,
    fields:COLS.map(c=>({k:c.k,l:c.l,html:true,calc:r=>cell(r,c)})),
    title:r=>({name:(r.address||'—'),sub:(r.city||'')+' '+(r.zip||'')}),
    renderGrid:renderGrid,renderTable:renderTable,
    storageKey:'vpools',defaultView:'grid',views:['grid','list','table']});
  render();
  $('#prov').innerHTML+=`<br>Generated ${new Date(META.generated).toLocaleString(undefined,{dateStyle:'medium',timeStyle:'short'})} · ${DATA.length} homes · ${META.price_cuts||0} price cuts · ${META.value_buys||0} value buys · ${META.regions_ok||0} Valley regions · $0 feed-first.`;
}).catch(e=>{$('#count').textContent='failed to load /data/valley-pools.json — '+e.message;});
</script>
<script src="/corner-nav.js" defer></script>
<script src="/nav-agent/nav-agent.js" defer></script>
<script src="/filter-modal.js" defer></script>
<script src="/crcp-theme.js" defer></script>
  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>