← back to Commercialrealestate

public/mls.html

527 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='%230e1116'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%233fb950'%3EMLS%3C/text%3E%3C/svg%3E">
<title>LA County Listings — Residential + CRE</title>
<style>
  :root{ --bg:#0e1116; --card:#161b22; --line:#2a313c; --ink:#e6edf3; --mut:#8b949e; --acc:#3fb950; --warn:#d29922; --bad:#f85149; --blue:#58a6ff; }
  *{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
  header{display:flex;align-items:center;gap:12px;padding:11px 18px;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:40;background:rgba(14,17,22,.98);backdrop-filter:blur(8px);flex-wrap:wrap;}
  header h1{font-size:16px;margin:0;} header h1 b{color:var(--acc);}
  header .nav{display:flex;gap:12px;margin-left:6px;} header .nav a{color:var(--blue);text-decoration:none;font-size:13px;} header .nav a:hover{color:var(--warn);}
  .spacer{flex:1;}
  .search{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:8px 12px;font-size:14px;min-width:220px;}
  .toggle{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;}
  .toggle button{background:var(--card);border:0;color:var(--mut);padding:8px 13px;font-size:13px;cursor:pointer;}
  .toggle button.on{background:var(--blue);color:#0b0e14;font-weight:700;}
  .csvbtn{background:var(--card);border:1px solid var(--line);color:var(--acc);border-radius:9px;padding:8px 12px;font-size:13px;cursor:pointer;} .csvbtn:hover{border-color:var(--acc);}
  .count{color:var(--mut);font-size:12px;}
  .wrap{padding:24px 28px 60px;}
  /* TABLE (spreadsheet) */
  .tblwrap{overflow:auto;max-height:calc(100vh - 130px);border:1px solid var(--line);border-radius:10px;}
  table.mls{border-collapse:separate;border-spacing:0;width:100%;font-size:12.5px;white-space:nowrap;}
  table.mls thead th{position:sticky;top:0;background:#1b2230;color:var(--mut);text-transform:uppercase;letter-spacing:.3px;font-size:10.5px;padding:8px 10px;border-bottom:1px solid var(--line);cursor:pointer;user-select:none;z-index:2;}
  table.mls thead th:hover{color:var(--ink);}
  table.mls thead th.asc::after{content:' ▲';color:var(--acc);} table.mls thead th.desc::after{content:' ▼';color:var(--acc);}
  /* draggable column reorder (Steve rule 2026-07-31: move columns around) */
  table.mls thead th.dragcol{cursor:grab;}
  table.mls thead th.dragging{opacity:.45;}
  table.mls thead th.dropL{box-shadow:inset 3px 0 0 var(--acc);}
  table.mls thead th.dropR{box-shadow:inset -3px 0 0 var(--acc);}
  table.mls tbody td{padding:7px 10px;border-bottom:1px solid #20262f;}
  table.mls tbody tr:hover td{background:rgba(88,166,255,.07);}
  table.mls td.n{text-align:right;font-variant-numeric:tabular-nums;}
  table.mls a{color:var(--blue);text-decoration:none;} table.mls a:hover{text-decoration:underline;}
  .pill{font-size:10px;padding:1px 6px;border-radius:8px;font-weight:600;}
  .pill.active{background:rgba(63,185,80,.16);color:var(--acc);} .pill.other{background:rgba(210,153,34,.16);color:var(--warn);}
  .mut{color:var(--mut);}
  /* GRID */
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--card-min,260px),1fr));gap:20px;}
  #sortsel{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:5px 8px;font-size:12px}
  #denswrap{display:none;align-items:center;gap:6px;font-size:13px;color:var(--mut)}
  #denswrap input[type=range]{width:92px;accent-color:var(--acc)}
  .gc{background:var(--card);border:2px solid #fff;border-radius:11px;padding:18px 20px;}
  .gc .addr{font-weight:600;} .gc .sub{color:var(--mut);font-size:12px;margin-bottom:6px;}
  .gc .row{display:flex;justify-content:space-between;font-size:12.5px;padding:2px 0;border-top:1px solid #20262f;}
  .gc .row .k{color:var(--mut);} .gc .row .v{font-variant-numeric:tabular-nums;}
  .hide{display:none;}
  #ov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:60;align-items:flex-start;justify-content:center;padding:40px 14px;overflow-y:auto;}
  #ov.on{display:flex;}
  #modal{width:min(680px,96vw);background:var(--card);border:1px solid var(--line);border-radius:15px;padding:20px 22px;position:relative;}
  #modal h3{margin:0 0 2px;font-size:19px;} .ph-sub{color:var(--warn);font-size:13px;}
  .ph-x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
  .ph-ci{display:flex;gap:14px;flex-wrap:wrap;margin:8px 0;font-size:13.5px;}
  .ph-sec{margin-top:14px;} .ph-sec h4{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);}
  .ph-item{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid #20262f;font-size:13px;} .ph-mut{color:var(--mut);font-size:12.5px;}
  /* ---- Amazon-style left rail ---- */
  .shell{display:flex;align-items:flex-start;}
  .rail{flex:0 0 260px;background:#0b0e13;border-right:1px solid var(--line);align-self:stretch;position:sticky;top:53px;height:calc(100vh - 53px);overflow-y:auto;padding:0 0 40px;}
  .rail .rsec{padding:13px 16px;border-bottom:1px solid var(--line);}
  .rail h4{margin:0 0 9px;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--mut);}
  .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;}
  .chip:hover{color:var(--ink);} .chip.active{border-color:var(--blue);color:var(--blue);background:rgba(88,166,255,.08);}
  .chip .ct{color:var(--mut);font-size:10px;margin-left:4px;}
  .navchip{display:inline-flex;align-items:center;gap:5px;background:rgba(210,153,34,.10);border:1px solid rgba(210,153,34,.4);color:var(--warn);border-radius:16px;padding:5px 11px;font-size:12px;cursor:pointer;text-decoration:none;font-weight:600;}
  .row2{display:flex;gap:8px;} .fld{flex:1;} .fld label{display:block;font-size:10px;color:var(--mut);margin-bottom:3px;}
  .rail input[type=number]{width:100%;background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 9px;font-size:13px;}
  .fieldtoggles{display:flex;flex-direction:column;gap:1px;max-height:320px;overflow-y:auto;}
  .ftog{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink);cursor:pointer;padding:3px 4px;border-radius:6px;} .ftog:hover{background:rgba(88,166,255,.06);} .ftog input{accent-color:var(--blue);}
  .minibtn{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:7px;padding:4px 9px;font-size:11px;cursor:pointer;margin-right:5px;} .minibtn:hover{border-color:var(--blue);color:var(--ink);}
  .wrap{flex:1 1 auto;min-width:0;}
  @media(max-width:820px){ .rail{display:none;} }
  .rail .rsec > h4 { cursor:pointer; display:flex; align-items:center; justify-content:space-between; user-select:none; }
  .rail .rsec > h4::after { content:'▾'; color:var(--mut); font-size:11px; margin-left:8px; transition:transform .15s; }
  .rail .rsec.collapsed > h4::after { transform:rotate(-90deg); }
  .rail .rsec.collapsed > *:not(h4) { display:none !important; }
  .mlsempty { padding:60px 20px; text-align:center; color:var(--mut); }
  .starbtn { background:none; border:0; color:var(--mut); font-size:16px; line-height:1; cursor:pointer; padding:0; }
  .starbtn:hover, .starbtn.on { color:var(--warn); }
  #shortbtn.shorton { border-color:var(--warn); color:var(--warn); }
  .statsbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:9px 22px; border-bottom:1px solid var(--line); }
  .statsbar:empty { display:none; }
  .statpill { font-size:12px; color:var(--ink); background:var(--card); border:1px solid var(--line); border-radius:8px; padding:4px 10px; }
  .statpill .sl { color:var(--mut); } .statpill b { font-variant-numeric:tabular-nums; }
  table.mls td.startd, table.mls th.starth { width:46px; white-space:nowrap; text-align:center; padding:5px 4px; }
  .copybtn { background:none; border:0; color:var(--mut); font-size:14px; line-height:1; cursor:pointer; padding:0; }
  .copybtn:hover { color:var(--blue); }
  #heat { background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:9px; padding:8px 10px; font-size:13px; }
  .heatleg { display:none; align-items:center; gap:5px; font-size:11px; color:var(--mut); }
  .heatleg.on { display:inline-flex; }
  .heatleg .hb { width:52px; height:8px; border-radius:4px; background:linear-gradient(90deg,hsl(0,70%,45%),hsl(60,70%,45%),hsl(120,70%,45%)); }
</style>
</head>
<body>
<header>
  <h1><b>LA County Listings</b> · Residential + CRE <span style="font-size:11px;font-weight:400;color:#8b949e">· Redfin-sourced</span></h1>
  <div class="nav"><a href="/">← Explorer</a><a href="/deals-flow.html" title="Closed deal flow — public-record sales + FHA + SEC REIT">💰 Deal Flow</a><a href="/map.html">🗺️ Map</a><a href="/licensed-agents.html" title="Real-estate agents &amp; brokers verified against the CA DRE public license roll">🎫 Licensed Agents</a><a href="/crcp.html">🛰️ CRCP</a></div>
  <span class="spacer"></span>
  <input class="search" id="q" type="text" placeholder="search all fields — address, city, firm, type, agent, source, year… (space = AND)">
  <div class="toggle" id="view"><button data-v="table" class="on">▦ Table</button><button data-v="grid">▤ Grid</button></div>
  <select id="sortsel" title="Sort listings"></select>
  <label id="denswrap" title="Card density — grid view">⤢<input type="range" id="density" min="1" max="10" step="1"></label>
  <button class="csvbtn" id="shortbtn" title="Show only your shortlisted (starred) listings — shared with the Explorer">⭐ Shortlist (0)</button>
  <select id="heat" title="Color rows red (low) → green (high) by a metric"><option value="none">Heat: off</option><option value="cap">Heat: Cap rate</option><option value="score">Heat: Score</option></select>
  <span id="heatleg" class="heatleg">low <span class="hb"></span> high · <span class="hl"></span></span>
  <button class="csvbtn" id="csv" title="Export current rows to CSV">⬇ CSV</button>
  <span class="count" id="count">…</span>
</header>
<div class="shell">
<aside class="rail">
  <div class="rsec"><h4>Asset class</h4><div class="chips" id="fType"></div>
    <div style="margin-top:9px;display:flex;flex-wrap:wrap;gap:6px;align-items:center"><span class="chip" id="listedChip" style="border-color:rgba(63,185,80,.5);color:var(--acc)" title="Homes newly on the market within the day window">🆕 Just Listed</span><span class="chip" id="soldChip" style="border-color:rgba(88,166,255,.5);color:var(--blue)" title="Closed escrow — pulled listings matched to a recorded sale within the day window">✅ Just Sold</span><span style="display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--mut)">within <input type="number" id="recentDays" min="1" max="365" step="1" style="width:52px;background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:4px 6px;font-size:12px" title="Day window for Just Listed / Just Sold"> days</span><span class="chip" id="escrowChip" style="border-color:rgba(210,153,34,.5);color:var(--warn)" title="Currently in escrow — pending or contingent">🤝 In Escrow</span><span class="chip" id="fellChip" style="border-color:rgba(248,81,73,.45);color:var(--bad)" title="Back on the market — was pending/contingent, now active again">↩️ Fell Out of Escrow</span><span class="chip" id="cutChip" style="border-color:rgba(255,166,0,.5);color:#ffa600" title="Price reduced below its prior price within the day window">💸 Price Cut</span></div>
    <div style="margin-top:9px;display:flex;flex-wrap:wrap;gap:6px"><span class="chip" id="nqChip" data-warr="nonqm" style="border-color:rgba(188,140,255,.5);color:#bc8cff" title="Non-QM target: unwarrantable condos + small multifamily (1–9u) + mixed-use">◆ Non-QM</span><span class="chip" id="unwChip" data-warr="unwarrantable" style="border-color:rgba(248,81,73,.4);color:var(--bad)" title="Filter the table to non-FHA-approved condos (non-QM target)">⚑ Unwarrantable condos</span><a class="navchip" href="/condos" title="Full condo drill-down page">condos page →</a></div></div>
  <div class="rsec"><h4>Status</h4><div class="chips" id="fStatus"></div></div>
  <div class="rsec"><h4>Submarket / city</h4><div class="chips" id="fCity"></div></div>
  <div class="rsec"><h4>Source of truth</h4><div class="chips" id="fSource"></div><div style="font-size:10px;color:var(--mut);margin-top:5px;line-height:1.4">firm-direct = the brokerage's own listing · broker-of-record = real listing broker known · aggregator = CREXi/portal only</div></div>
  <div class="rsec"><h4>Brokerage <span style="font-size:10px;font-weight:400;color:var(--mut)">(listing broker)</span></h4><div class="chips" id="fFirm"></div></div>
  <div class="rsec"><h4>Price</h4><div class="row2"><div class="fld"><label>Min $</label><input type="number" id="pMin" placeholder="any" step="50000"></div><div class="fld"><label>Max $</label><input type="number" id="pMax" placeholder="any" step="50000"></div></div></div>
  <div class="rsec"><h4>Fields / columns — show / hide</h4>
    <div class="fieldtoggles" id="fFields"></div>
    <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fReset" title="Reset columns to the tight default — order, widths &amp; visibility">↺ Reset</button></div>
    <div class="mut" style="font-size:10.5px;margin-top:7px;line-height:1.35">Drag a column header to reorder · drag its right edge to resize · click to sort.</div>
  </div>
</aside>
<div class="wrap">
  <div class="statsbar" id="statsbar"></div>
  <div id="tableView" class="tblwrap"><table class="mls" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
  <div id="gridView" class="grid hide"></div>
  <div id="moreWrap" style="text-align:center;padding:16px 0"></div>
</div>
</div>
<div id="ov"><div id="modal"><button class="ph-x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
<script>
const $=s=>document.querySelector(s);
const esc=s=>(s==null?'':String(s)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
const money=n=>(n==null||n==='')?'—':'$'+Number(n).toLocaleString();
const num=n=>(n==null||n==='')?'—':Number(n).toLocaleString();
// Column spec: key, label, type (s=string,n=number,money,pct,link,status), accessor
const COLS=[
  {k:'rank',l:'#',t:'n'},
  {k:'address',l:'Address',t:'s'},
  {k:'city',l:'City',t:'s'},
  {k:'zip',l:'ZIP',t:'s'},
  {k:'type',l:'Type',t:'s'},
  {k:'price',l:'Price',t:'money'},
  {k:'units',l:'Units',t:'n'},
  {k:'sqft',l:'SqFt',t:'n'},
  {k:'ppu',l:'$/Unit',t:'money',calc:r=>r.units?Math.round(r.price/r.units):null},
  {k:'pps',l:'$/SqFt',t:'money',calc:r=>r.sqft?Math.round(r.price/r.sqft):null},
  {k:'cap_rate',l:'Cap %',t:'pct'},
  {k:'area_rent',l:'Area Rent/mo',t:'money'},
  {k:'area_yield',l:'Area GRY %',t:'pct'},
  {k:'year_built',l:'Year',t:'n'},
  {k:'rent_control',l:'Rent Ctrl',t:'s'},
  {k:'status',l:'Status',t:'status'},
  {k:'mkt',l:'Market',t:'s'},
  {k:'dom',l:'Days on Mkt',t:'n'},
  {k:'prev_price',l:'Prev $',t:'money'},
  {k:'off_date',l:'Pulled',t:'s'},
  {k:'sold_price',l:'Sold $',t:'money'},
  {k:'sold_date',l:'Sold',t:'s'},
  {k:'firm',l:'Firm',t:'s'},
  {k:'broker_agent',l:'Agent',t:'s'},
  {k:'source_of_truth',l:'Source',t:'s'},
  {k:'financeScore',l:'Finance',t:'n'},
  {k:'composite',l:'Score',t:'n'},
  {k:'source',l:'Source',t:'link'},
];
let DATA=[], view='table', sortKey='dom', sortDir=1, q='', lastColSig='';  // default landing: freshest listings first (days-on-market asc); localStorage overrides for returning users
const _rd=(function(){ const v=parseInt(localStorage.getItem('mlsRecentDays'),10); return (v>0&&v<=365)?v:7; })();
const F={types:new Set(),cities:new Set(),status:new Set(),source:new Set(),firms:new Set(),pmin:null,pmax:null,unwarrantable:false,nonqm:false,shortlist:false,justsold:false,justlisted:false,inescrow:false,fellout:false,pricecut:false,recentDays:_rd};
const daysSince=d=>{ if(!d) return Infinity; const t=Date.parse(d); return isNaN(t)?Infinity:(Date.now()-t)/86400000; };
// Real listing age: Redfin DAYS ON MARKET when present (gap-immune), else our first-seen date.
const listAge=r=>{ if(r.dom!=null&&isFinite(+r.dom)) return +r.dom; return daysSince(r.first_seen); };
// shared shortlist with the Explorer (same localStorage key + same listing ids)
const SHORT=new Set((function(){ try{ return JSON.parse(localStorage.getItem('cre_shortlist')||'[]'); }catch(e){ return []; } })());
function saveShort(){ try{ localStorage.setItem('cre_shortlist', JSON.stringify([...SHORT])); }catch(e){} }
function toggleStar(id){ SHORT.has(id)?SHORT.delete(id):SHORT.add(id); saveShort(); }
function updateShortBtn(){ const sb=$('#shortbtn'); if(sb){ sb.textContent='⭐ Shortlist ('+SHORT.size+')'; sb.classList.toggle('shorton',F.shortlist); } }
// pagination (perf, parity with the Explorer)
let _rows=[], _cols=[], _shown=0;
const PAGE=()=> view==='table'?200:60;
const star=r=>`<button class="starbtn ${SHORT.has(r.id)?'on':''}" data-star="${r.id}" aria-label="${SHORT.has(r.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(r.id)}" title="Shortlist">★</button><button class="copybtn" data-copy="${r.id}" aria-label="Copy summary" title="Copy summary">📋</button>`;
function paintBody(){
  const rows=_rows.slice(0,_shown), cols=_cols;
  if(view==='table'){
    $('#tableView').classList.remove('hide'); $('#gridView').classList.add('hide');
    $('#tbody').innerHTML=rows.map(r=>`<tr class="prow" data-addr="${esc(r.address||'')}" data-city="${esc(r.city||'')}" style="cursor:pointer"><td class="startd">${star(r)}</td>`+cols.map(c=>{
      const cls=(c.t==='money'||c.t==='n'||c.t==='pct')?' class="n"':''; return `<td${cls}>${cellHTML(r,c)}</td>`;
    }).join('')+'</tr>').join('');
    const csig=cols.map(c=>c.k).join(',');
    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
    else if(window.ColResize && window.ColResize.reattach){ window.ColResize.reattach(document.querySelector('#tableView table.mls')); } // resize handles survive sort/search re-render (TK-10089)
  } else {
    $('#tableView').classList.add('hide'); $('#gridView').classList.remove('hide');
    const bodyCols=cols.filter(c=>!['address','city','zip','type'].includes(c.k));
    $('#gridView').innerHTML=rows.map(r=>`<div class="gc prow" data-addr="${esc(r.address||'')}" data-city="${esc(r.city||'')}" style="cursor:pointer">
      <div style="display:flex;justify-content:space-between;gap:8px"><div class="addr">${esc(r.address||'—')}</div>${star(r)}</div><div class="sub">${esc(r.city||'')} ${esc(r.zip||'')} · ${esc(r.type||'')}</div>`
      +bodyCols.map(c=>`<div class="row"><span class="k">${esc(c.l)}</span><span class="v">${cellHTML(r,c)}</span></div>`).join('')
      +`</div>`).join('');
  }
  const mw=$('#moreWrap'); if(mw) mw.innerHTML=_shown<_rows.length?`<button class="csvbtn" id="showmore">Show ${Math.min(PAGE(),_rows.length-_shown)} more · showing ${_shown} of ${_rows.length}</button>`:'';
  applyHeat(rows);
}
// heat cues + copy summary (parity with the Explorer)
let heat=localStorage.getItem('cre_heat_mls')||'none';
function heatVal(r){ return heat==='cap'?(r.cap_rate!=null&&r.cap_rate!==''?+r.cap_rate:null):heat==='score'?(r.composite!=null?+r.composite:null):null; }
function heatColor(t){ t=Math.max(0,Math.min(1,t)); return `hsl(${Math.round(t*120)},70%,45%)`; }
function applyHeat(rows){
  const lg=$('#heatleg'); if(lg){ lg.classList.toggle('on',heat!=='none'); if(heat!=='none') lg.querySelector('.hl').textContent={cap:'Cap rate',score:'Score'}[heat]; }
  if(heat==='none') return;
  const vals=rows.map(heatVal).filter(v=>v!=null&&isFinite(v)); if(!vals.length) return;
  const min=Math.min(...vals),max=Math.max(...vals),span=(max-min)||1;
  rows.forEach(r=>{ const v=heatVal(r); const cell=document.querySelector(`.startd [data-star="${r.id}"]`)?.closest('td'); if(!cell) return; cell.style.boxShadow=(v==null||!isFinite(v))?'':'inset 5px 0 0 '+heatColor((v-min)/span); });
}
function rowSummary(r){ const L=[];
  L.push(`${r.address||''}, ${r.city||''} CA ${r.zip||''}`.trim());
  const l2=[r.type, r.units?`${r.units} unit${r.units>1?'s':''}`:null, r.price?money(r.price):null].filter(Boolean).join(' · '); if(l2)L.push(l2);
  const m=[]; if(r.cap_rate!=null&&r.cap_rate!=='') m.push('Cap '+r.cap_rate+'%'); if(r.units&&r.price) m.push('$'+Math.round(r.price/r.units).toLocaleString()+'/unit'); if(r.sqft&&r.price) m.push('$'+Math.round(r.price/r.sqft).toLocaleString()+'/SF'); if(m.length)L.push(m.join(' · '));
  const ex=[]; if(r.year_built) ex.push('Built '+r.year_built); if(r.beds!=null) ex.push((r.beds)+'bd/'+(r.baths??'—')+'ba'); if(r.warrantable_status) ex.push('Warrantability: '+r.warrantable_status); if(ex.length)L.push(ex.join(' · '));
  if(r.source) L.push(r.source); return L.join('\n');
}
function copySummary(id,btn){ const r=DATA.find(x=>x.id===id); if(!r) return; const done=()=>{ if(btn){ btn.textContent='✓'; setTimeout(()=>btn.textContent='📋',1200); } }; try{ navigator.clipboard.writeText(rowSummary(r)).then(done).catch(done); }catch(e){ done(); } }
function median(a){ if(!a.length) return null; const s=a.slice().sort((x,y)=>x-y); const m=Math.floor(s.length/2); return s.length%2?s[m]:(s[m-1]+s[m])/2; }
function renderStats(rows){ const sb=$('#statsbar'); if(!sb) return; if(!rows.length){ sb.innerHTML=''; return; }
  const prices=rows.map(r=>+r.price).filter(v=>v>0);
  const ppu=rows.map(r=>r.units?+r.price/r.units:null).filter(v=>v>0);
  const pps=rows.map(r=>r.sqft?+r.price/r.sqft:null).filter(v=>v>0);
  const caps=rows.map(r=>r.cap_rate).filter(v=>v!=null&&v!=='').map(Number).filter(v=>!isNaN(v));
  const yrs=rows.map(r=>+r.year_built).filter(v=>v>1800);
  const rents=rows.map(r=>+r.area_rent).filter(v=>v>0);
  const ylds=rows.map(r=>+r.area_yield).filter(v=>v>0);
  const pill=(l,v)=>v==null?'':`<span class="statpill"><span class="sl">${l}</span> <b>${v}</b></span>`;
  sb.innerHTML=pill('Count',rows.length)
    +pill('Median price',prices.length?'$'+Math.round(median(prices)).toLocaleString():null)
    +pill('Median $/unit',ppu.length?'$'+Math.round(median(ppu)).toLocaleString():null)
    +pill('Median $/SF',pps.length?'$'+Math.round(median(pps)).toLocaleString():null)
    +pill('Avg cap',caps.length?(caps.reduce((a,b)=>a+b,0)/caps.length).toFixed(2)+'% ('+caps.length+')':null)
    +pill('Avg area rent',rents.length?'$'+Math.round(rents.reduce((a,b)=>a+b,0)/rents.length).toLocaleString()+'/mo':null)
    +pill('Median area GRY',ylds.length?median(ylds).toFixed(2)+'%':null)
    +pill('Median year',yrs.length?Math.round(median(yrs)):null);
}
// Listing lifecycle: status 'off_market' means it left Redfin's active feed (pulled). disposition
// 'sold' (cross-referenced vs closed_sale) vs 'withdrawn'. Surfaced as Status = Active/Sold/Withdrawn.
const isEscrow=v=>/pending|contingent/i.test(v||'');
// Status column shows lifecycle first, then In Escrow for active-but-under-contract listings.
function lifeStatus(x){ if(x.status==='off_market') return x.disposition==='sold'?'Sold':'Withdrawn'; if(isEscrow(x.market_status)) return 'In Escrow'; return 'Active'; }
function mapCondo(c){ return { id:c.id, address:c.address, city:c.city, zip:c.zip, type:'Condo', price:+c.price||0, units:1, cap_rate:null, year_built:c.year_built||null, status:lifeStatus(c), first_seen:c.created_at||null, prev_price:c.prev_price?+c.prev_price:null, price_cut_at:c.price_changed_at?String(c.price_changed_at).slice(0,10):null, dom:c.days_on_market!=null?+c.days_on_market:null, listed_date:c.listed_date?String(c.listed_date).slice(0,10):null, mkt:c.market_status||null, prev:c.prev_market_status||null, off_date:c.off_market_at?String(c.off_market_at).slice(0,10):null, disposition:c.disposition||null, sold_price:c.sold_price?+c.sold_price:null, sold_date:c.sold_date?String(c.sold_date).slice(0,10):null, source:c.source, firm:c.firm_name||null, beds:c.beds, baths:c.baths, sqft:c.sqft, hoa:c.hoa, warrantable_status:c.warrantable_status, broker_name:c.broker_name, project_name:c.project_name }; }
function mapSfr(s){ return { id:s.id, address:s.address, city:s.city, zip:s.zip, type:'Single-Family', price:+s.price||0, units:1, cap_rate:null, year_built:s.year_built||null, status:lifeStatus(s), first_seen:s.created_at||null, prev_price:s.prev_price?+s.prev_price:null, price_cut_at:s.price_changed_at?String(s.price_changed_at).slice(0,10):null, dom:s.days_on_market!=null?+s.days_on_market:null, listed_date:s.listed_date?String(s.listed_date).slice(0,10):null, mkt:s.market_status||null, prev:s.prev_market_status||null, off_date:s.off_market_at?String(s.off_market_at).slice(0,10):null, disposition:s.disposition||null, sold_price:s.sold_price?+s.sold_price:null, sold_date:s.sold_date?String(s.sold_date).slice(0,10):null, source:s.source, firm:s.firm_name||null, beds:s.beds, baths:s.baths, sqft:s.sqft, broker_name:s.broker_name||null }; }
// START TIGHT (Steve rule 2026-07-31): only the essentials show on first load; the user
// toggles the rest on from the rail. A saved pref in VISCOL always wins.
const DEFAULT_ON=new Set(['address','city','type','price','cap_rate','status','sold_date']);
let VISCOL=(function(){let s={};try{s=JSON.parse(localStorage.getItem('mlsCols')||'{}');}catch(e){}return s;})();
function colVis(k){ if(k in VISCOL) return !!VISCOL[k]; return DEFAULT_ON.has(k); }
// Draggable column ORDER (Steve rule: move columns around). Keys in display order;
// auto-discovered/new keys are appended, stale keys pruned. Persisted per browser.
let COLORDER=(function(){let a=[];try{a=JSON.parse(localStorage.getItem('mlsColOrder')||'[]');}catch(e){}return Array.isArray(a)?a:[];})();
function syncColOrder(){
  const keys=COLS.map(c=>c.k);
  COLORDER=COLORDER.filter(k=>keys.includes(k));                    // prune removed
  keys.forEach(k=>{ if(!COLORDER.includes(k)) COLORDER.push(k); }); // append new
}
function orderedCols(){ syncColOrder(); return COLORDER.map(k=>COLS.find(c=>c.k===k)).filter(Boolean); }
function visCols(){ return orderedCols().filter(c=>colVis(c.k)); }
function saveCols(){ try{localStorage.setItem('mlsCols',JSON.stringify(VISCOL));}catch(e){} }
function saveColOrder(){ try{localStorage.setItem('mlsColOrder',JSON.stringify(COLORDER));}catch(e){} }
// When the visible column SET or ORDER changes, hand the table back to col-resize.js
// so it rebuilds its <colgroup> + drag handles for the current columns.
function reinitResize(){
  const tbl=document.querySelector('#tableView table.mls'); if(!tbl) return;
  const cg=tbl.querySelector('colgroup[data-cr]'); if(cg) cg.remove();
  delete tbl.dataset.crDone; tbl.style.tableLayout=''; tbl.style.width='';
  if(window.ColResize && window.ColResize.refresh) window.ColResize.refresh();
}
function tog(set,v){ set.has(v)?set.delete(v):set.add(v); }
// Discover EVERY scalar data point present in the feed and expose each as a toggleable column (default hidden).
function autoCols(){
  const known=new Set(COLS.map(c=>c.k)); const skip=new Set(['id','finance','qwen','deal','warrant_signals','listings_json','tx_history','risks']);
  const seen=new Set();
  // sample from BOTH ends so appended datasets (e.g. condos) contribute their unique fields
  DATA.slice(0,50).concat(DATA.slice(-50)).forEach(r=>Object.keys(r||{}).forEach(k=>{
    if(known.has(k)||skip.has(k)||seen.has(k)) return; const v=r[k]; if(v==null) return;
    if(typeof v==='object') return; seen.add(k);
    const isNum = typeof v==='number' || (v!==''&&!isNaN(+v));
    COLS.push({k, l:k.replace(/_/g,' ').replace(/\b\w/g,ch=>ch.toUpperCase()).slice(0,20), t:isNum?'n':'s', def:0});
  }));
}
function buildRail(){
  const tc={},cc={},sc={},oc={},fc={};
  DATA.forEach(r=>{ if(r.type)tc[r.type]=(tc[r.type]||0)+1; if(r.city)cc[r.city]=(cc[r.city]||0)+1; const s=String(r.status||'').startsWith('Active')?'Active':'Off-market'; sc[s]=(sc[s]||0)+1; if(r.source_of_truth)oc[r.source_of_truth]=(oc[r.source_of_truth]||0)+1; if(r.firm&&!/^crexi$/i.test(r.firm))fc[r.firm]=(fc[r.firm]||0)+1; });
  const chip=(obj,attr,limit)=>Object.entries(obj).sort((a,b)=>b[1]-a[1]).slice(0,limit||99).map(([k,c])=>`<span class="chip" data-${attr}="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
  $('#fType').innerHTML=chip(tc,'ty'); $('#fStatus').innerHTML=chip(sc,'st'); $('#fCity').innerHTML=chip(cc,'cy',16); $('#fSource').innerHTML=chip(oc,'so'); $('#fFirm').innerHTML=chip(fc,'fm',24);
  buildFieldToggles();
}
function buildFieldToggles(){ $('#fFields').innerHTML=COLS.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join(''); }
function isNonQM(r){ const t=(r.type||'').toLowerCase();
  if(r.type==='Condo' && r.warrantable_status && r.warrantable_status!=='fha_approved') return true;
  if(/multi/.test(t) && r.units>=1 && r.units<=9) return true;
  if(/mixed/.test(t)) return true; return false; }
function anySelection(){ return F.types.size||F.cities.size||F.status.size||F.source.size||F.firms.size||F.pmin!=null||F.pmax!=null||F.unwarrantable||F.nonqm||F.shortlist||F.justsold||F.justlisted||F.inescrow||F.fellout||F.pricecut||(q&&q.length); }
function passFacets(r){
  if(F.types.size && !F.types.has(r.type)) return false;
  if(F.cities.size && !F.cities.has(r.city)) return false;
  if(F.source.size && !F.source.has(r.source_of_truth)) return false;
  if(F.firms.size && !F.firms.has(r.firm)) return false;
  if(F.status.size){ const s=String(r.status||'').startsWith('Active')?'Active':'Off-market'; if(!F.status.has(s)) return false; }
  if(F.pmin!=null && !(+r.price>=F.pmin)) return false;
  if(F.pmax!=null && !(+r.price<=F.pmax)) return false;
  if(F.unwarrantable && !(r.type==='Condo' && r.warrantable_status && r.warrantable_status!=='fha_approved')) return false;
  if(F.nonqm && !isNonQM(r)) return false;
  // Just Listed = still on the market (not sold/withdrawn) & listed within N days (real DAYS ON MARKET).
  if(F.justlisted && !(r.status!=='Sold' && r.status!=='Withdrawn' && listAge(r)<=F.recentDays)) return false;
  // Just Sold / Closed escrow = recently LEFT the market (off_market_at) with a matched sale.
  if(F.justsold && !(r.disposition==='sold' && daysSince(r.off_date)<=F.recentDays)) return false;
  // In Escrow = currently pending/contingent. Fell Out = was pending/contingent, now back on market.
  if(F.inescrow && !isEscrow(r.mkt)) return false;
  if(F.fellout && !(isEscrow(r.prev) && !isEscrow(r.mkt) && r.status!=='Sold' && r.status!=='Withdrawn')) return false;
  // Price Cut = current price dropped below its previous price, changed within N days.
  if(F.pricecut && !(r.prev_price>0 && +r.price<+r.prev_price && daysSince(r.price_cut_at)<=F.recentDays)) return false;
  if(F.shortlist && !SHORT.has(r.id)) return false;
  return true;
}
const cval=(r,c)=>c.calc?c.calc(r):r[c.k];
function cellHTML(r,c){
  const v=cval(r,c);
  if(c.t==='money') return money(v);
  if(c.t==='pct') return v==null||v===''?'—':v+'%';
  if(c.t==='n') return num(v);
  if(c.t==='status'){ const a=String(v||'').startsWith('Active'); return `<span class="pill ${a?'active':'other'}">${esc(v||'—')}</span>`; }
  if(c.t==='link') return v?`<a href="${esc(v)}" target="_blank" rel="noopener noreferrer">link↗</a>`:'—';
  // Doctrine deep-link: the brokerage name clicks through to the broker's OWN site (canonical source).
  if(c.k==='firm' && r.broker_url) return `<a href="${esc(r.broker_url)}" target="_blank" rel="noopener noreferrer" title="the listing broker's own site — source of truth">${esc(v==null||v===''?'—':v)} ↗</a>`;
  return esc(v==null||v===''?'—':v);
}
function filtered(){
  let rows=DATA.filter(passFacets);
  if(q){
    // Search ALL data (Steve rule 2026-07-31): every field the API returned on a row —
    // not just the defined/visible columns — incl. nested objects. Space = AND.
    const terms=q.toLowerCase().split(/\s+/).filter(Boolean);
    rows=rows.filter(r=>{
      const hay=Object.keys(r).map(k=>{const v=r[k];return v==null?'':(typeof v==='object'?JSON.stringify(v):String(v));}).join('  ').toLowerCase();
      return terms.every(t=>hay.includes(t));
    });
  }
  const c=COLS.find(x=>x.k===sortKey)||COLS[0];
  rows=rows.slice().sort((a,b)=>{
    let x=cval(a,c), y=cval(b,c);
    // missing values always sink to the bottom, regardless of sort direction
    const xm=(x==null||x===''), ym=(y==null||y==='');
    if(xm&&ym) return 0; if(xm) return 1; if(ym) return -1;
    const xn=(!isNaN(+x)), yn=(!isNaN(+y));
    if(xn&&yn){ x=+x; y=+y; } else { x=String(x).toLowerCase(); y=String(y).toLowerCase(); }
    if(x<y) return -1*sortDir; if(x>y) return 1*sortDir; return 0;
  });
  return rows;
}
function render(){
  updateShortBtn();
  // Amazon-style: land POPULATED. Show the whole book on open (paginated to 200 rows in the DOM),
  // and let the left rail NARROW it. The only empty case is the split-second before data arrives.
  if(!DATA.length){
    $('#count').textContent='loading listings…';
    $('#statsbar').innerHTML=''; const _mw=$('#moreWrap'); if(_mw) _mw.innerHTML='';
    return;
  }
  const rows=filtered(); const cols=visCols();
  renderStats(rows);
  $('#count').textContent = anySelection()
    ? `${rows.length.toLocaleString()} of ${DATA.length.toLocaleString()} properties`
    : `${rows.length.toLocaleString()} properties · filter on the left to narrow ↤`;
  // table head (full column set; independent of pagination)
  $('#thead').innerHTML='<tr><th class="starth">⭐</th>'+cols.map(c=>`<th data-k="${c.k}" draggable="true" title="Drag to reorder · click to sort" class="dragcol${sortKey===c.k?(sortDir>0?' asc':' desc'):''}">${esc(c.l)}</th>`).join('')+'</tr>';
  _rows=rows; _cols=cols; _shown=Math.min(PAGE(), rows.length);   // paginate the DOM; count+stats use the full set
  paintBody();
}
$('#thead').addEventListener('click',e=>{ const th=e.target.closest('th'); if(!th) return; const k=th.dataset.k; if(!k) return;
  if(sortKey===k) sortDir*=-1; else { sortKey=k; sortDir=(k==='address'||k==='city'||k==='firm'||k==='type')?1:-1; }
  localStorage.setItem('mlsSortKey',sortKey); localStorage.setItem('mlsSortDir',sortDir); if(window.__syncSortSel) window.__syncSortSel(); render(); });
// ── drag-to-reorder columns (Steve rule 2026-07-31: move columns around) ──
let dragKey=null;
$('#thead').addEventListener('dragstart',e=>{ const th=e.target.closest('th'); if(!th||!th.dataset.k) return;
  if(e.target.closest('.cr-handle')){ e.preventDefault(); return; }        // that grab is a resize, not a move
  dragKey=th.dataset.k; e.dataTransfer.effectAllowed='move'; try{e.dataTransfer.setData('text/plain',dragKey);}catch(_){}
  th.classList.add('dragging'); });
$('#thead').addEventListener('dragover',e=>{ if(!dragKey) return; const th=e.target.closest('th'); if(!th||!th.dataset.k||th.dataset.k===dragKey) return;
  e.preventDefault(); e.dataTransfer.dropEffect='move';
  const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
  th.classList.toggle('dropR',after); th.classList.toggle('dropL',!after); });
$('#thead').addEventListener('dragleave',e=>{ const th=e.target.closest('th'); if(th) th.classList.remove('dropL','dropR'); });
$('#thead').addEventListener('drop',e=>{ if(!dragKey) return; const th=e.target.closest('th'); if(!th||!th.dataset.k) return; e.preventDefault();
  const tgt=th.dataset.k; if(tgt!==dragKey){
    const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
    syncColOrder(); const from=COLORDER.indexOf(dragKey); if(from>=0){
      COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,dragKey);
      saveColOrder(); lastColSig=''; render();
    }
  }
  dragKey=null; });
$('#thead').addEventListener('dragend',()=>{ document.querySelectorAll('#thead th.dragging,#thead th.dropL,#thead th.dropR').forEach(x=>x.classList.remove('dragging','dropL','dropR')); dragKey=null; });
// click a property → 100-yr history popup (address-only, no names)
async function propHistory(address,city){
  const ov=$('#ov'), mb=$('#mbody'); ov.classList.add('on'); mb.innerHTML='<div class="ph-mut">loading history for '+esc(address)+'…</div>';
  let d; try{ d=await (await fetch('/api/property-history?address='+encodeURIComponent(address)+'&city='+encodeURIComponent(city||''))).json(); }catch(e){ mb.innerHTML='<div class="ph-mut">error</div>'; return; }
  const a=d.assessor||d.assessorArchive;
  let h='<h3>'+esc(address)+'</h3><div class="ph-sub">'+esc(city||'')+' · property history (address only — no names)</div>';
  if(a){ h+='<div class="ph-sec"><h4>Assessor parcel'+(a.roll_year?' ('+esc(a.roll_year)+' roll)':'')+'</h4><div class="ph-ci">'
    +(a.year_built?'<span>🏗 Built '+a.year_built+'</span>':'')
    +(a.bedrooms!=null?'<span>🛏 '+a.bedrooms+' bd</span>':'')+(a.bathrooms!=null?'<span>🛁 '+a.bathrooms+' ba</span>':'')
    +(a.sqft_main?'<span>📐 '+Number(a.sqft_main).toLocaleString()+' sqft</span>':'')+(a.units?'<span>🏠 '+a.units+' units</span>':'')
    +((a.use_desc2||a.use_desc1||a.use_description)?'<span>🏷 '+esc(a.use_desc2||a.use_desc1||a.use_description)+'</span>':'')
    +((a.roll_total_value||a.total_value)?'<span>💰 assessed '+money(a.roll_total_value||a.total_value)+'</span>':'')+'</div></div>'; }
  h+='<div class="ph-sec"><h4>Sold history ('+d.sales.length+')</h4>'+(d.sales.length?d.sales.map(x=>'<div class="ph-item"><span>'+esc(x.address)+(x.city?', '+esc(x.city):'')+'</span><span class="ph-mut">'+money(x.sold_price)+' · '+String(x.sold_date||'').slice(0,10)+(x.mls?' · MLS '+esc(x.mls):'')+'</span></div>').join(''):'<div class="ph-mut">no sold records on file for this address</div>')+'</div>';
  if(d.permits&&d.permits.length) h+='<div class="ph-sec"><h4>Permits ('+d.permits.length+')</h4>'+d.permits.map(p=>'<div class="ph-item"><span>'+esc(p.permit_type||p.use_description||'permit')+'</span><span class="ph-mut">'+esc(String(p.issue_date||'').slice(0,10))+(p.valuation?' · '+money(p.valuation):'')+'</span></div>').join('')+'</div>';
  if(!a && !d.sales.length && !(d.permits||[]).length) h+='<div class="ph-mut">No assessor record matched this address in the LA County parcel roll (2.4M parcels), and no sold or permit history is on file. Try the street number plus street name exactly as it appears on the deed.</div>';
  mb.innerHTML=h;
}
document.addEventListener('click',e=>{ const cp=e.target.closest('[data-copy]'); if(cp){ copySummary(cp.dataset.copy,cp); return; } const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); updateShortBtn(); if(F.shortlist) render(); return; }
  const row=e.target.closest('.prow'); if(row && e.target.tagName!=='A'){ propHistory(row.dataset.addr,row.dataset.city); }
  if(e.target.id==='ov'||e.target.classList.contains('ph-x')) $('#ov').classList.remove('on'); });
$('#q').addEventListener('input',()=>{ q=$('#q').value.trim(); render(); });
function exportCSV(){ const rows=filtered(); const cols=visCols(); const cv=(r,c)=>{let v=cval(r,c); v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"';};
  const csv=cols.map(c=>'"'+c.l+'"').join(',')+'\n'+rows.map(r=>cols.map(c=>cv(r,c)).join(',')).join('\n');
  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'})); a.download='cre-mls-'+rows.length+'.csv'; a.click(); URL.revokeObjectURL(a.href); }
$('#csv').addEventListener('click',exportCSV);
$('#shortbtn').addEventListener('click',()=>{ F.shortlist=!F.shortlist; render(); });
$('#moreWrap').addEventListener('click',e=>{ if(e.target.closest('#showmore')){ _shown=Math.min(_shown+PAGE(), _rows.length); paintBody(); } });
$('#heat').value=heat; $('#heat').addEventListener('change',()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat_mls',heat); render(); });
$('#view').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b) return;
  document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x===b)); view=b.dataset.v; localStorage.setItem('mlsView',view); $('#denswrap').style.display=(view==='grid')?'inline-flex':'none'; render(); });
// ── sort <select> + density slider (Steve rule: every grid gets sort + density) ──
(function(){
  const sel=$('#sortsel'); const dz=$('#density');
  // build sort options from COLS (skip the source link column)
  sel.innerHTML=COLS.filter(c=>c.t!=='link').map(c=>`<option value="${c.k}">Sort: ${esc(c.l==='#'?'Default (rank)':c.l)}</option>`).join('');
  // restore persisted sort
  const sk=localStorage.getItem('mlsSortKey'); const sd=localStorage.getItem('mlsSortDir');
  if(sk && COLS.some(c=>c.k===sk)) sortKey=sk;
  if(sd==='1'||sd==='-1') sortDir=+sd;
  sel.value=sortKey;
  sel.addEventListener('change',()=>{ sortKey=sel.value; const c=COLS.find(x=>x.k===sortKey);
    sortDir=(c&&(c.t==='s'||c.t==='status'))?1:-1;   // text asc, numbers/money desc
    localStorage.setItem('mlsSortKey',sortKey); localStorage.setItem('mlsSortDir',sortDir); render(); });
  // density 1..10  →  card-min width (1 = big/sparse, 10 = small/dense)
  const widthFor=v=>Math.round(380-(Math.max(1,Math.min(10,v))-1)*(380-140)/9);
  function applyDensity(v){ document.documentElement.style.setProperty('--card-min', widthFor(v)+'px'); }
  const dv=parseInt(localStorage.getItem('mlsDensity'),10); const dval=(dv>=1&&dv<=10)?dv:5;
  dz.value=dval; applyDensity(dval);
  dz.addEventListener('input',()=>{ const v=+dz.value; applyDensity(v); localStorage.setItem('mlsDensity',v); });
  // restore persisted view (and show density only in grid view)
  const pv=localStorage.getItem('mlsView'); if(pv==='grid'||pv==='table'){ view=pv; document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x.dataset.v===view)); }
  $('#denswrap').style.display=(view==='grid')?'inline-flex':'none';
  // keep the select in sync when the user sorts via a table-column header
  window.__syncSortSel=()=>{ if(sel.value!==sortKey) sel.value=sortKey; };
})();
// left-rail facets + column toggles
$('#fType').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.types,c.dataset.ty);c.classList.toggle('active');render();});
$('#fStatus').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.status,c.dataset.st);c.classList.toggle('active');render();});
$('#fCity').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.cities,c.dataset.cy);c.classList.toggle('active');render();});
$('#fSource').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.source,c.dataset.so);c.classList.toggle('active');render();});
$('#fFirm').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.firms,c.dataset.fm);c.classList.toggle('active');render();});
$('#pMin').addEventListener('input',()=>{F.pmin=$('#pMin').value?+$('#pMin').value:null;render();});
$('#pMax').addEventListener('input',()=>{F.pmax=$('#pMax').value?+$('#pMax').value:null;render();});
$('#fFields').addEventListener('change',e=>{const cb=e.target.closest('input[data-ck]');if(!cb)return;VISCOL[cb.dataset.ck]=cb.checked;saveCols();render();});
$('#fAll').addEventListener('click',()=>{COLS.forEach(c=>VISCOL[c.k]=true);saveCols();buildFieldToggles();render();});
$('#fNone').addEventListener('click',()=>{COLS.forEach(c=>VISCOL[c.k]=false);saveCols();buildFieldToggles();render();});
// Reset the whole table layout back to the tight default: visibility, order, widths.
$('#fReset').addEventListener('click',()=>{ VISCOL={}; saveCols(); COLORDER=[]; saveColOrder();
  try{Object.keys(localStorage).forEach(k=>{ if(k.indexOf('cr:'+location.pathname+':')===0) localStorage.removeItem(k); });}catch(e){}
  lastColSig=''; buildFieldToggles(); render(); });
const unwEl=document.getElementById('unwChip');
if(unwEl) unwEl.addEventListener('click',()=>{ F.unwarrantable=!F.unwarrantable; unwEl.classList.toggle('active',F.unwarrantable); if(F.unwarrantable){ F.types=new Set(['Condo']); document.querySelectorAll('#fType .chip').forEach(x=>x.classList.toggle('active',x.dataset.ty==='Condo')); } render(); });
const nqEl=document.getElementById('nqChip');
if(nqEl) nqEl.addEventListener('click',()=>{ F.nonqm=!F.nonqm; nqEl.classList.toggle('active',F.nonqm); render(); });
// Just Listed / Just Sold quick-filters + adjustable day window (persisted)
const listedEl=document.getElementById('listedChip'), soldEl=document.getElementById('soldChip'), rdEl=document.getElementById('recentDays');
if(rdEl){ rdEl.value=F.recentDays; rdEl.addEventListener('change',()=>{ const v=parseInt(rdEl.value,10); if(v>0&&v<=365){ F.recentDays=v; localStorage.setItem('mlsRecentDays',v); if(F.justsold||F.justlisted) render(); } }); }
const escrowEl=document.getElementById('escrowChip'), fellEl=document.getElementById('fellChip'), cutEl=document.getElementById('cutChip');
const quickChips=[['justlisted',listedEl],['justsold',soldEl],['inescrow',escrowEl],['fellout',fellEl],['pricecut',cutEl]];
function setQuick(key,el){ const on=!F[key]; quickChips.forEach(([k,e])=>{ F[k]=false; e&&e.classList.remove('active'); }); F[key]=on; if(el) el.classList.toggle('active',on); render(); }
if(listedEl) listedEl.addEventListener('click',()=>setQuick('justlisted',listedEl));
if(soldEl) soldEl.addEventListener('click',()=>setQuick('justsold',soldEl));
if(escrowEl) escrowEl.addEventListener('click',()=>setQuick('inescrow',escrowEl));
if(fellEl) fellEl.addEventListener('click',()=>setQuick('fellout',fellEl));
if(cutEl) cutEl.addEventListener('click',()=>setQuick('pricecut',cutEl));
// collapsible rail tabs — all collapsed on load
const railEl=document.querySelector('.rail');
if(railEl){ railEl.addEventListener('click',e=>{ const h=e.target.closest('h4'); if(h && h.parentElement.classList.contains('rsec')) h.parentElement.classList.toggle('collapsed'); });
  document.querySelectorAll('.rail .rsec').forEach(s=>s.classList.add('collapsed')); }
fetch('/data/ranked.json').then(r=>r.json()).then(async d=>{ DATA=(d.ranked||d)||[];
  // Residential inventory bolted onto the CRE table (asset-class chips auto-build from r.type):
  try{ const cd=await (await fetch('/api/condos')).json(); DATA=DATA.concat((cd.condos||[]).map(mapCondo)); }catch(e){}
  try{ const sr=await (await fetch('/api/residential')).json(); DATA=DATA.concat((sr.sfr||[]).map(mapSfr)); }catch(e){}
  // SOURCING DOCTRINE (docs/SOURCING.md): the listing broker-of-record is the truth; the aggregator
  // is a breadcrumb. Display the REAL broker firm (not "Crexi"), and classify residential rows the
  // same way CRE deals are classified in enrich-provenance.js.
  (function applyDoctrine(){
    const AGG=['crexi','redfin','zillow','costar','loopnet','realtor','myelisting'];
    const hostAgg=u=>{ try{ const h=new URL(u).hostname.replace(/^www\./,''); return AGG.find(a=>h.includes(a))||null; }catch(e){ return null; } };
    DATA.forEach(r=>{
      if(r.broker_firm && !/^crexi$/i.test(r.broker_firm)) r.firm=r.broker_firm;   // show the real brokerage
      if(!r.broker_agent && r.broker_name) r.broker_agent=r.broker_name;            // residential agent
      if(!r.source_of_truth){                                                       // classify residential
        const agg=hostAgg(r.source);
        r.source_of_truth = !agg && r.source ? 'firm-direct' : (r.firm ? 'broker-of-record' : 'aggregator');
      }
    });
  })();
  // Rent averages: join Census ACS median gross rent by city (area proxy — NOT the specific home's
  // achievable rent). Powers Area Rent/mo, Area GRY % (gross rental yield), and area rent stat.
  try{ const dm=await (await fetch('/data/demographics.json')).json(); const bc=dm.byCity||{};
    const RENT={}; Object.keys(bc).forEach(k=>{ RENT[k.toLowerCase().trim()]=bc[k]; });
    DATA.forEach(r=>{ const e=RENT[String(r.city||'').toLowerCase().trim()]; if(!e) return;
      r.area_rent=e.median_gross_rent||null;
      r.renter_pct=e.renter_pct||null; r.rent_burden_pct=e.rent_burden_pct||null;
      if(r.area_rent&&+r.price>0) r.area_yield=+((r.area_rent*12/+r.price)*100).toFixed(2);
    });
  }catch(e){}
  autoCols(); buildRail(); render(); })
  .catch(()=>{ $('#count').textContent='failed to load /data/ranked.json'; });
</script>
  <script src="/col-resize.js" defer></script>
</body>
</html>