[object Object]

← back to Commercialrealestate

CRCP list sweep: apply 7-control list-UX standard to sibling pages

439898ba7de3be40959287c1127e31c0b12349bc · 2026-07-31 08:55:23 -0700 · Steve Abrams

Mirrors broker-grid.html across every browsable CRCP list:
- mls, gov-agents, condos, rent-rolls, fha-loans, sales (tables): tight-default
  columns, drag-to-reorder headers, show/hide toggles, expandable-row modal,
  click-sort (data-no-sort so col-resize.js doesn't double-fire), search-ALL-fields
  (space=AND), Reset button; page-unique localStorage keys per page
- licensed-agents (cards): cards subset — sort, field-toggle, start-tight,
  card-expand, search-all (column resize/reorder N/A to cards)
- rent-rolls/fha-loans/sales: added a real client table view where only a card/grid
  engine existed, preserving each page's data fetch, charts, and calculators
Verified: all 7 parse clean + HTTP 200 + markers on a clean serve.js boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit 439898ba7de3be40959287c1127e31c0b12349bc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 31 08:55:23 2026 -0700

    CRCP list sweep: apply 7-control list-UX standard to sibling pages
    
    Mirrors broker-grid.html across every browsable CRCP list:
    - mls, gov-agents, condos, rent-rolls, fha-loans, sales (tables): tight-default
      columns, drag-to-reorder headers, show/hide toggles, expandable-row modal,
      click-sort (data-no-sort so col-resize.js doesn't double-fire), search-ALL-fields
      (space=AND), Reset button; page-unique localStorage keys per page
    - licensed-agents (cards): cards subset — sort, field-toggle, start-tight,
      card-expand, search-all (column resize/reorder N/A to cards)
    - rent-rolls/fha-loans/sales: added a real client table view where only a card/grid
      engine existed, preserving each page's data fetch, charts, and calculators
    Verified: all 7 parse clean + HTTP 200 + markers on a clean serve.js boot.
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 public/condos.html          | 210 +++++++++++++++++++----
 public/fha-loans.html       | 301 ++++++++++++++++++++++++++++++---
 public/gov-agents.html      | 205 ++++++++++++++++++++--
 public/licensed-agents.html | 102 +++++++++--
 public/mls.html             |  78 ++++++++-
 public/rent-rolls.html      | 300 +++++++++++++++++++++++++++++++--
 public/sales.html           | 403 +++++++++++++++++++++++++++++++++++++-------
 7 files changed, 1422 insertions(+), 177 deletions(-)

diff --git a/public/condos.html b/public/condos.html
index 2b67ba9..d43c018 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -57,10 +57,18 @@
   .listwrap{overflow-x:auto;border:2px solid #fff;border-radius:12px;}
   .listtbl{width:100%;border-collapse:collapse;font-size:12.5px;white-space:nowrap;}
   .listtbl th,.listtbl td{text-align:right;padding:9px 13px;border-top:1px solid var(--line);}
-  .listtbl th{position:sticky;top:0;background:#0b0e13;color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;border-top:0;}
+  .listtbl th{position:sticky;top:0;background:#0b0e13;color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;border-top:0;cursor:pointer;user-select:none;}
+  .listtbl th:hover{color:var(--ink);}
+  .listtbl th.asc::after{content:' ▲';color:var(--gold);} .listtbl th.desc::after{content:' ▼';color:var(--gold);}
+  /* draggable column reorder (Steve rule 2026-07-31: move columns around) */
+  .listtbl th.dragcol{cursor:grab;}
+  .listtbl th.dragging{opacity:.45;}
+  .listtbl th.dropL{box-shadow:inset 3px 0 0 var(--gold);}
+  .listtbl th.dropR{box-shadow:inset -3px 0 0 var(--gold);}
   .listtbl td.stick,.listtbl th.stick{text-align:left;position:sticky;left:0;background:var(--card);}
   .listtbl th.stick{background:#0b0e13;}
   .listtbl tr:hover td{background:rgba(88,166,255,.06);}
+  .listtbl tbody tr{cursor:pointer;}
   .listtbl .la{font-weight:600;color:var(--ink);} .listtbl .lc{color:var(--mut);font-size:11px;}
   @media(max-width:820px){ .rail{display:none;} }
   :focus-visible { outline:2px solid var(--blue); outline-offset:2px; }
@@ -78,6 +86,16 @@
   .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); }
+  /* row/card expand — full detail modal */
+  #ov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.62);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:20px;}
+  #modal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
+  #modal .maddr{color:var(--mut);font-size:13px;margin-bottom:10px;}
+  #modal .litem{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-bottom:1px solid #20262f;font-size:13px;}
+  #modal .litem .m{color:var(--mut);}
+  #modal .src{font-size:12px;color:var(--mut);margin-top:12px;border-top:1px solid var(--line);padding-top:10px;}
 </style>
 </head>
 <body>
@@ -99,14 +117,15 @@
   <div class="rsec"><h4>Sq Ft</h4><div class="row2"><div class="fld"><label>Min</label><input type="number" id="sqMin" step="100" placeholder="any"></div><div class="fld"><label>Max</label><input type="number" id="sqMax" step="100" placeholder="any"></div></div></div>
   <div class="rsec"><h4>$ / Sq Ft</h4><div class="row2"><div class="fld"><label>Min</label><input type="number" id="psfMin" step="25" placeholder="any"></div><div class="fld"><label>Max</label><input type="number" id="psfMax" step="25" placeholder="any"></div></div></div>
   <div class="rsec"><h4>Year built</h4><div class="row2"><div class="fld"><label>After</label><input type="number" id="yrMin" step="1" placeholder="any"></div><div class="fld"><label>Before</label><input type="number" id="yrMax" step="1" placeholder="any"></div></div></div>
-  <div class="rsec"><h4>Fields — show / hide</h4>
+  <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="fDef">Default</button></div>
+    <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fDef">Default</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">In List view — drag a column header to reorder · drag its right edge to resize · click to sort.</div>
   </div>
 </aside>
 <main>
   <div class="topbar">
-    <input type="text" id="q" placeholder="search project / city / zip / address / broker / firm…">
+    <input type="text" id="q" placeholder="search all fields — project, city, zip, address, broker, firm, status… (space = AND)">
     <span class="viewseg" id="viewseg"><button data-view="grid" class="active">▦ Grid</button><button data-view="list">☰ List</button></span>
     <button id="shortbtn" title="Show only your shortlisted (starred) listings — shared with the Explorer & MLS">⭐ Shortlist (0)</button>
     <select id="csort" title="Sort condos" style="background:var(--card,#141925);color:var(--ink,#E6E8EC);border:1px solid var(--line,#232a38);border-radius:7px;padding:5px 8px;font-size:12px">
@@ -126,6 +145,7 @@
   <div class="gridwrap"><div class="grid" id="grid"><div class="empty">loading…</div></div></div>
 </main>
 </div>
+<div id="ov"><div id="modal"><button class="x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
 
 <script>
 const $=s=>document.querySelector(s);
@@ -142,28 +162,55 @@ function statusBadge(s){
 }
 
 // ---- every data point (incl. computed) — each toggleable, drives cards + list columns ----
+// Order below is the DEFAULT column order (draggable-reorderable, persisted).
 const FIELDS=[
-  {k:'warrantable_status', l:'Warrantability', badge:1, col:1, def:1},
-  {k:'price',       l:'Price',        money:1, col:1, def:1},
-  {k:'hoa',         l:'HOA/mo',       money:1, col:1, def:1, suf:'/mo'},
-  {k:'beds',        l:'Beds',                  col:1, def:1},
-  {k:'baths',       l:'Baths',                 col:1, def:1},
-  {k:'sqft',        l:'Sq Ft',        num:1,   col:1, def:1},
-  {k:'ppsf',        l:'$/Sq Ft',      money:1, col:1, def:1, calc:c=>c.sqft?Math.round(c.price/c.sqft):null},
-  {k:'ppbed',       l:'$/Bed',        money:1, col:1, def:0, calc:c=>c.beds?Math.round(c.price/c.beds):null},
-  {k:'year_built',  l:'Year built',            col:1, def:1},
-  {k:'age',         l:'Age (yrs)',             col:1, def:0, calc:c=>c.year_built?(NOW_YEAR-c.year_built):null},
-  {k:'city',        l:'City',                  col:1, def:1},
-  {k:'zip',         l:'ZIP',                   col:1, def:0},
-  {k:'project_name',l:'Project',               col:1, def:0},
-  {k:'broker_name', l:'Broker',                col:1, def:1},
-  {k:'firm_name',   l:'Firm',                  col:1, def:0},
-  {k:'warrant_source',l:'Warrant note',        col:0, def:0},
-  {k:'created_at',  l:'Added',        date:1,  col:1, def:0},
+  {k:'project_name',l:'Project',               col:1},
+  {k:'warrantable_status', l:'Warrantability', badge:1, col:1},
+  {k:'city',        l:'City',                  col:1},
+  {k:'hoa',         l:'HOA/mo',       money:1, col:1, suf:'/mo'},
+  {k:'beds',        l:'Beds',                  col:1},
+  {k:'price',       l:'Price',        money:1, col:1},
+  {k:'baths',       l:'Baths',                 col:1},
+  {k:'sqft',        l:'Sq Ft',        num:1,   col:1},
+  {k:'ppsf',        l:'$/Sq Ft',      money:1, col:1, calc:c=>c.sqft?Math.round(c.price/c.sqft):null},
+  {k:'ppbed',       l:'$/Bed',        money:1, col:1, calc:c=>c.beds?Math.round(c.price/c.beds):null},
+  {k:'year_built',  l:'Year built',            col:1},
+  {k:'age',         l:'Age (yrs)',             col:1, calc:c=>c.year_built?(NOW_YEAR-c.year_built):null},
+  {k:'zip',         l:'ZIP',                   col:1},
+  {k:'broker_name', l:'Broker',                col:1},
+  {k:'firm_name',   l:'Firm',                  col:1},
+  {k:'warrant_source',l:'Warrant note',        col:0},
+  {k:'created_at',  l:'Added',        date:1,  col:1},
 ];
+// 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 VIS always wins.
+const DEFAULT_ON=new Set(['project_name','warrantable_status','city','hoa','beds']);
+FIELDS.forEach(f=>{ if(!('def' in f)) f.def=DEFAULT_ON.has(f.k)?1:0; });
 const VIS=(function(){ let s={}; try{s=JSON.parse(localStorage.getItem('condoFields')||'{}');}catch(e){} const o={}; FIELDS.forEach(f=>o[f.k]=(f.k in s)?!!s[f.k]:!!f.def); return o; })();
 function fvis(k){ return !!VIS[k]; }
 function saveVis(){ try{localStorage.setItem('condoFields',JSON.stringify(VIS));}catch(e){} }
+// ---- Draggable column ORDER (Steve rule: move columns around). Keys in display order;
+// auto-discovered/new keys appended, stale keys pruned. Persisted per browser. ----
+let COLORDER=(function(){let a=[];try{a=JSON.parse(localStorage.getItem('condoColOrder')||'[]');}catch(e){}return Array.isArray(a)?a:[];})();
+function syncColOrder(){
+  const keys=FIELDS.map(f=>f.k);
+  COLORDER=COLORDER.filter(k=>keys.includes(k));                    // prune removed
+  keys.forEach(k=>{ if(!COLORDER.includes(k)) COLORDER.push(k); }); // append new
+}
+function orderedFields(){ syncColOrder(); return COLORDER.map(k=>FIELDS.find(f=>f.k===k)).filter(Boolean); }
+function saveColOrder(){ try{localStorage.setItem('condoColOrder',JSON.stringify(COLORDER));}catch(e){} }
+// page-owned list-view sort — click a header; survives re-render (col-resize sort would be wiped)
+let LSORTKEY=localStorage.getItem('condoListSortKey')||'';
+let LSORTDIR=(localStorage.getItem('condoListSortDir')==='1')?1:(localStorage.getItem('condoListSortDir')==='-1'?-1:1);
+let lastColSig='';
+// When the visible column SET or ORDER changes, hand the list table back to col-resize.js
+// so it rebuilds its <colgroup> + drag handles for the current columns.
+function reinitResize(){
+  const tbl=document.querySelector('#grid table.listtbl'); 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 fval(c,f){ return f.calc?f.calc(c):c[f.k]; }
 function fcell(c,f){ const v=fval(c,f);
   if(f.badge) return statusBadge(c.warrantable_status||c.warrant_signal||'—');
@@ -227,12 +274,12 @@ function buildRail(){
   $('#fBaths').innerHTML=[0,1,2,3].map(n=>`<span class="chip ${F.bathsMin===n?'active':''}" data-baths="${n}">${n===0?'Any':n+'+'}</span>`).join('');
   buildFieldToggles();
 }
-function buildFieldToggles(){ $('#fFields').innerHTML=FIELDS.map(f=>`<label class="ftog"><input type="checkbox" data-fk="${f.k}" ${fvis(f.k)?'checked':''}><span>${esc(f.l)}</span>${f.col?'<span class="ct" title="Also a List column">▤</span>':''}</label>`).join(''); }
+function buildFieldToggles(){ $('#fFields').innerHTML=orderedFields().map(f=>`<label class="ftog"><input type="checkbox" data-fk="${f.k}" ${fvis(f.k)?'checked':''}><span>${esc(f.l)}</span>${f.col?'<span class="ct" title="Also a List column">▤</span>':''}</label>`).join(''); }
 
 // ---- renderers ----
 const CARD_SKIP=new Set(['warrantable_status','city','zip','project_name','warrant_source']);
 function dbCards(rows){
-  const vis=FIELDS.filter(f=>fvis(f.k)&&!CARD_SKIP.has(f.k));
+  const vis=orderedFields().filter(f=>fvis(f.k)&&!CARD_SKIP.has(f.k));
   return rows.map(c=>`<div class="condo">
     <div class="chead"><div class="name">${esc(c.project_name||c.address||'Condo')}</div><button class="starbtn ${SHORT.has(c.id)?'on':''}" data-star="${c.id}" aria-label="${SHORT.has(c.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(c.id)}" title="Shortlist">★</button></div>
     <div class="addr">${esc(c.address||'')}${c.city?', '+esc(c.city):''} ${esc(c.zip||'')}</div>
@@ -243,24 +290,54 @@ function dbCards(rows){
   </div>`).join('');
 }
 function dbList(rows){
-  const cols=FIELDS.filter(f=>f.col&&fvis(f.k));
-  const head='<th class="stick">Project</th>'+cols.map(f=>`<th>${esc(f.l)}</th>`).join('');
-  const body=rows.map(c=>`<tr><td class="stick"><button class="starbtn ${SHORT.has(c.id)?'on':''}" data-star="${c.id}" aria-label="${SHORT.has(c.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(c.id)}" title="Shortlist">★</button> <span class="la">${esc(c.project_name||c.address||'Condo')}</span><div class="lc">${esc(c.address||'')}${c.city?', '+esc(c.city):''}</div></td>`
+  const cols=orderedFields().filter(f=>f.col&&fvis(f.k));
+  const head='<th class="stick">Project</th>'+cols.map(f=>{
+    const sc=(LSORTKEY===f.k)?(LSORTDIR>0?' asc':' desc'):'';
+    return `<th data-k="${f.k}" draggable="true" title="Drag to reorder · click to sort" class="dragcol${sc}">${esc(f.l)}</th>`;
+  }).join('');
+  const body=rows.map(c=>`<tr data-id="${c.id}"><td class="stick"><button class="starbtn ${SHORT.has(c.id)?'on':''}" data-star="${c.id}" aria-label="${SHORT.has(c.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(c.id)}" title="Shortlist">★</button> <span class="la">${esc(c.project_name||c.address||'Condo')}</span><div class="lc">${esc(c.address||'')}${c.city?', '+esc(c.city):''}</div></td>`
     +cols.map(f=>`<td>${fcell(c,f)}</td>`).join('')+'</tr>').join('');
-  return `<div class="listwrap"><table class="listtbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
+  return `<div class="listwrap"><table class="listtbl" data-no-sort><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
 }
 function paint(rows){ const g=$('#grid');
-  if(!rows.length){ g.className='grid'; g.innerHTML='<div class="empty">no condos match these filters.</div>'; return; }
-  if(VIEW==='list'){ g.className='grid listhost'; g.innerHTML=dbList(rows); }
-  else { g.className='grid'; g.innerHTML=dbCards(rows); }
+  if(!rows.length){ g.className='grid'; g.innerHTML='<div class="empty">no condos match these filters.</div>'; lastColSig=''; return; }
+  if(VIEW==='list'){
+    g.className='grid listhost'; g.innerHTML=dbList(rows);
+    // hand the freshly-rendered table to col-resize.js when the visible columns/order change
+    const csig=orderedFields().filter(f=>f.col&&fvis(f.k)).map(f=>f.k).join(',');
+    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
+  }
+  else { g.className='grid'; g.innerHTML=dbCards(rows); lastColSig=''; }
+}
+// page-owned list-view sort (Steve rule: all sortable; survives search re-render) —
+// applies a click-header sort on the raw field value, any type. Runs on top of the
+// top-bar sort select so a header click always wins in List view.
+function listSort(rows){
+  if(VIEW!=='list' || !LSORTKEY) return rows;
+  const f=FIELDS.find(x=>x.k===LSORTKEY); if(!f) return rows;
+  return rows.slice().sort((a,b)=>{
+    let x=fval(a,f), y=fval(b,f);
+    const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
+    if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
+    if(x<y) return -LSORTDIR; if(x>y) return LSORTDIR; return 0;
+  });
 }
 
 // ---- apply filters + render ----
 function apply(){
   let rows=DATA.filter(passFacets);
+  // Search ALL data (Steve rule 2026-07-31): every field the API returned on a row —
+  // not just a hand-picked few — incl. nested objects. Space = AND.
   const ql=($('#q').value||'').trim().toLowerCase();
-  if(ql) rows=rows.filter(c=>((c.project_name||'')+' '+(c.city||'')+' '+(c.zip||'')+' '+(c.address||'')+' '+(c.broker_name||'')+' '+(c.firm_name||'')).toLowerCase().includes(ql));
+  if(ql){
+    const terms=ql.split(/\s+/).filter(Boolean);
+    rows=rows.filter(c=>{
+      const hay=Object.keys(c).map(k=>{const v=c[k];return v==null?'':(typeof v==='object'?JSON.stringify(v):String(v));}).join('  ').toLowerCase();
+      return terms.every(t=>hay.includes(t));
+    });
+  }
   rows=sortRows(rows);
+  rows=listSort(rows);
   renderStats(rows); updateShortBtn();
   const unw=F.warr==='unwarrantable';
   $('#count').innerHTML=`<b>${rows.length}</b> of ${DATA.length} ${unw?'unwarrantable (non-FHA) condos':(MODE==='fha'?'FHA-list projects':'condos')}`;
@@ -322,6 +399,77 @@ $('#viewseg').onclick=e=>{ const b=e.target.closest('button[data-view]'); if(!b)
 // shortlist stars (unified) + toggle
 $('#grid').addEventListener('click',e=>{ 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) apply(); } });
 $('#shortbtn').addEventListener('click',()=>{ F.shortlist=!F.shortlist; apply(); });
+// ── expandable rows/cards → full detail modal ──
+const money=v=>(v==null||v==='')?'—':'$'+(+v).toLocaleString();
+function openDetail(id){
+  const c=DATA.find(x=>String(x.id)===String(id)); if(!c) return;
+  const ov=$('#ov'), mb=$('#mbody'); ov.classList.add('on');
+  const line=(l,v)=>v==null||v===''?'':`<div class="litem"><span class="m">${esc(l)}</span><span>${v}</span></div>`;
+  const ppsf=c.sqft?Math.round(c.price/c.sqft):null;
+  mb.innerHTML='<h3>'+esc(c.project_name||c.address||'Condo')+'</h3>'
+    +'<div class="maddr">'+esc(c.address||'')+(c.city?', '+esc(c.city):'')+' '+esc(c.zip||'')+'</div>'
+    +statusBadge(c.warrantable_status||c.warrant_signal||'—')
+    +'<div style="margin-top:12px">'
+    +line('Price',money(c.price))
+    +line('HOA / mo',c.hoa!=null?money(c.hoa)+'/mo':'')
+    +line('Beds',c.beds)
+    +line('Baths',c.baths)
+    +line('Sq Ft',c.sqft!=null?(+c.sqft).toLocaleString():'')
+    +line('$ / Sq Ft',ppsf!=null?money(ppsf):'')
+    +line('Year built',c.year_built)
+    +line('Age (yrs)',c.year_built?(NOW_YEAR-c.year_built):'')
+    +line('Broker',c.broker_name)
+    +line('Firm',c.firm_name)
+    +line('Added',c.created_at?(function(){try{return new Date(c.created_at).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'});}catch(e){return '';}})():'')
+    +'</div>'
+    +(c.warrant_source?'<div class="src">'+esc(c.warrant_source)+'</div>':'');
+}
+$('#ov').addEventListener('click',e=>{ if(e.target.id==='ov') $('#ov').classList.remove('on'); });
+document.addEventListener('keydown',e=>{ if(e.key==='Escape') $('#ov').classList.remove('on'); });
+// row/card click opens detail (but not when the click is a star, header, or link)
+$('#grid').addEventListener('click',e=>{
+  if(e.target.closest('[data-star]')||e.target.closest('th')||e.target.tagName==='A'||e.target.tagName==='BUTTON') return;
+  const row=e.target.closest('tr[data-id]')||e.target.closest('.condo');
+  if(row){ let id=row.dataset.id; if(id==null){ const st=row.querySelector('[data-star]'); id=st&&st.dataset.star; } if(id!=null) openDetail(id); }
+});
+
+// ── list-view header: click-to-sort (page-owned; survives search re-render) ──
+$('#grid').addEventListener('click',e=>{ const th=e.target.closest('th.dragcol'); if(!th) return;
+  const k=th.dataset.k; if(!k) return;
+  if(LSORTKEY===k) LSORTDIR*=-1; else { LSORTKEY=k; LSORTDIR=1; }
+  localStorage.setItem('condoListSortKey',LSORTKEY); localStorage.setItem('condoListSortDir',LSORTDIR);
+  apply();
+});
+// ── list-view header: drag-to-reorder columns (Steve rule: move columns around) ──
+let dragKey=null;
+$('#grid').addEventListener('dragstart',e=>{ const th=e.target.closest('th.dragcol'); if(!th) 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'); });
+$('#grid').addEventListener('dragover',e=>{ if(!dragKey) return; const th=e.target.closest('th.dragcol'); if(!th||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); });
+$('#grid').addEventListener('dragleave',e=>{ const th=e.target.closest('th.dragcol'); if(th) th.classList.remove('dropL','dropR'); });
+$('#grid').addEventListener('drop',e=>{ if(!dragKey) return; const th=e.target.closest('th.dragcol'); if(!th) 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=''; buildFieldToggles(); apply();
+    }
+  }
+  dragKey=null; });
+$('#grid').addEventListener('dragend',()=>{ document.querySelectorAll('#grid th.dragging,#grid th.dropL,#grid th.dropR').forEach(x=>x.classList.remove('dragging','dropL','dropR')); dragKey=null; });
+// ── Reset: back to the tight default — visibility, order & widths ──
+$('#fReset').addEventListener('click',()=>{
+  try{localStorage.removeItem('condoFields');}catch(e){}
+  FIELDS.forEach(f=>VIS[f.k]=!!f.def);
+  COLORDER=[]; saveColOrder();
+  LSORTKEY=''; try{localStorage.removeItem('condoListSortKey');localStorage.removeItem('condoListSortDir');}catch(e){}
+  try{Object.keys(localStorage).forEach(k=>{ if(k.indexOf('cr:'+location.pathname+':')===0) localStorage.removeItem(k); });}catch(e){}
+  lastColSig=''; buildFieldToggles(); apply();
+});
 // 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'); });
diff --git a/public/fha-loans.html b/public/fha-loans.html
index de6e40f..2d5f3e2 100644
--- a/public/fha-loans.html
+++ b/public/fha-loans.html
@@ -94,6 +94,43 @@
   .whybadge.below{background:rgba(96,165,250,.18);color:#9cc4ff;border:1px solid rgba(96,165,250,.35);}
   .whybadge.era{background:rgba(248,81,73,.18);color:#ff9b94;border:1px solid rgba(248,81,73,.40);}
   .empty{padding:50px;text-align:center;color:var(--muted);}
+  /* sortable + draggable + resizable headers (Steve list-build rule 2026-07-31) */
+  thead th.dragcol{cursor:grab;}
+  thead th.dragging{opacity:.45;}
+  thead th.dropL{box-shadow:inset 3px 0 0 var(--gold);}
+  thead th.dropR{box-shadow:inset -3px 0 0 var(--gold);}
+  thead th.asc .ar::after{content:'▲';} thead th.desc .ar::after{content:'▼';}
+  tbody tr.frow{cursor:pointer;}
+  /* left rail — facets + column toggles */
+  .fha-shell{display:flex;align-items:flex-start;gap:0;}
+  .fha-rail{flex:0 0 250px;align-self:stretch;border-right:1px solid var(--glassb);background:rgba(255,255,255,.035);
+    position:sticky;top:0;max-height:100vh;overflow-y:auto;padding-bottom:30px;border-radius:0;}
+  .fha-main{flex:1 1 auto;min-width:0;}
+  .fha-sec{padding:12px 15px;border-bottom:1px solid var(--glassb);}
+  .fha-sec h4{margin:0 0 9px;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:space-between;user-select:none;}
+  .fha-sec h4::after{content:'▾';font-size:11px;margin-left:8px;transition:transform .15s;}
+  .fha-sec.collapsed h4::after{transform:rotate(-90deg);}
+  .fha-sec.collapsed > *:not(h4){display:none !important;}
+  .fchips{display:flex;flex-wrap:wrap;gap:6px;}
+  .fchip{background:var(--glass);color:var(--muted);border:1px solid var(--glassb);border-radius:16px;padding:4px 10px;font-size:12px;cursor:pointer;user-select:none;}
+  .fchip:hover{color:#fff;} .fchip.active{border-color:var(--gold);color:var(--gold);background:rgba(233,196,106,.12);}
+  .fct{color:var(--muted);font-size:10px;margin-left:4px;}
+  .frow2{display:flex;gap:8px;} .ffld{flex:1;} .ffld label{display:block;font-size:10px;color:var(--muted);margin-bottom:3px;}
+  .ffld input{width:100%;background:var(--glass);color:var(--ink);border:1px solid var(--glassb);border-radius:8px;padding:7px 9px;font-size:13px;}
+  .ftogs{display:flex;flex-direction:column;gap:1px;max-height:300px;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(233,196,106,.08);} .ftog input{accent-color:var(--gold);}
+  .fgrp{font-size:9.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);margin:9px 0 3px;font-weight:700;opacity:.85;} .fgrp:first-child{margin-top:0;}
+  .fmini{margin-top:8px;display:flex;gap:5px;flex-wrap:wrap;}
+  .fmini button{background:var(--glass);color:var(--muted);border:1px solid var(--glassb);border-radius:7px;padding:4px 9px;font-size:11px;cursor:pointer;} .fmini button:hover{border-color:var(--gold);color:var(--ink);}
+  /* expandable-row detail modal */
+  #fov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.62);z-index:80;align-items:flex-start;justify-content:center;padding:40px 14px;overflow-y:auto;}
+  #fov.on{display:flex;}
+  #fmodal{width:min(640px,96vw);background:rgba(16,20,28,.98);border:1px solid var(--glassb);border-radius:16px;padding:20px 22px;position:relative;backdrop-filter:blur(22px);}
+  #fmodal h3{margin:0 0 3px;font-size:19px;color:#fff;} #fmodal .msub{color:var(--gold);font-size:12.5px;margin-bottom:6px;}
+  #fmodal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--muted);font-size:22px;cursor:pointer;}
+  #fmodal .litem{display:flex;justify-content:space-between;gap:12px;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.06);font-size:13px;}
+  #fmodal .litem .m{color:var(--muted);}
+  @media(max-width:820px){.fha-rail{display:none;}}
   .foot{margin-top:20px;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;}
   @media(prefers-reduced-motion:reduce){body::before{animation:none;}.glass{animation:none;opacity:1;transform:none;}}
@@ -144,13 +181,26 @@
 
   <div class="tiles" id="tiles"></div>
 
+  <div class="glass controls" style="animation-delay:.08s">
+    <input type="text" id="q" placeholder="search all fields — property, city, lender, rate, ZIP… (space = AND)">
+    <label>density <input type="range" id="density" min="4" max="24" step="1"></label>
+    <span class="count" id="count">…</span>
+  </div>
+
   <div class="glass" style="animation-delay:.1s;padding:0;overflow:hidden">
-    <div class="cg-shell">
-      <aside class="cg-rail" id="cgRail"></aside>
-      <div class="cg-main" id="cgMain"><div class="empty" style="padding:50px">loading real HUD data…</div></div>
+    <div class="fha-shell">
+      <aside class="fha-rail" id="fRail"></aside>
+      <div class="fha-main">
+        <div class="tablewrap">
+          <table id="ftable" data-no-sort><thead id="fthead"></thead><tbody id="ftbody"></tbody></table>
+        </div>
+        <div class="empty" id="femptymsg" style="display:none">loading real HUD data…</div>
+      </div>
     </div>
   </div>
 
+  <div id="fov"><div id="fmodal"><button class="x" aria-label="Close" onclick="document.getElementById('fov').classList.remove('on')">✕</button><div id="fmbody"></div></div></div>
+
   <div class="foot">
     <span class="pill">Liquid-glass UI · 2026</span><span class="pill">HUD Open Data</span>
     <span class="pill">Freddie Mac PMMS</span><span class="pill">real data · $0</span><br>
@@ -165,20 +215,25 @@ const F={
   num:n=>{n=Number(n);return isFinite(n)?n.toLocaleString():'—';},
   pct:n=>(n==null||!isFinite(n))?'—':(+n).toFixed(2)+'%',
 };
-let DATA=null, ROWS=[], GRID=null;
-
-// Every data point — each toggleable (drives cards + list columns) for the crcp-grid engine.
-const FIELDS=[
-  {k:'rate',           l:'Orig. Rate',  pct:1,   col:1, def:1},
-  {k:'pmmsBenchmark',  l:'Market avg',  pct:1,   col:1, def:1},
-  {k:'originalAmount', l:'Loan amount', money:1, col:1, def:1},
-  {k:'originationDate',l:'Originated',           col:1, def:1},
-  {k:'units',          l:'Units',       num:1,   col:1, def:1},
-  {k:'flag',           l:'Flag',                 col:1, def:1, calc:r=> r.red ? (r.belowMarket&&r.historicLowEra?'Below + Era':r.belowMarket?'Below market':'Historic-low era') : '—'},
-  {k:'city',           l:'City',                 col:1, def:0},
-  {k:'state',          l:'State',                col:1, def:0},
-  {k:'zip',            l:'ZIP',                  col:1, def:0},
-  {k:'holder',         l:'Lender',               col:1, def:0},
+let DATA=null, ROWS=[];
+
+// ── esc + formatters ──────────────────────────────────────────────────────────
+const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+const flagOf=r=> r.red ? (r.belowMarket&&r.historicLowEra?'Below + Era':r.belowMarket?'Below market':'Historic-low era') : '—';
+// Every data point carries a group `g` for organized toggles + sort menu.
+// COLS order below IS the tight default column order; def:1 shows on first load.
+const COLS=[
+  {k:'property',       l:'Property',    g:'Identity', t:'s',     def:1},
+  {k:'rate',           l:'Orig. Rate',  g:'Loan',     t:'pct',   def:1},
+  {k:'pmmsBenchmark',  l:'Market avg',  g:'Loan',     t:'pct',   def:1},
+  {k:'originalAmount', l:'Loan amount', g:'Loan',     t:'money', def:1},
+  {k:'originationDate',l:'Originated',  g:'Loan',     t:'s',     def:1},
+  {k:'units',          l:'Units',       g:'Loan',     t:'num',   def:1},
+  {k:'flag',           l:'Flag',        g:'Flag',     t:'flag',  def:0, calc:flagOf},
+  {k:'city',           l:'City',        g:'Location', t:'s',     def:0},
+  {k:'state',          l:'State',       g:'Location', t:'s',     def:0},
+  {k:'zip',            l:'ZIP',         g:'Location', t:'s',     def:0},
+  {k:'holder',         l:'Lender',      g:'Details',  t:'s',     def:0},
 ];
 const FACETS=[
   {k:'flag',  l:'Flag',          type:'chip', calc:r=> r.red ? (r.belowMarket&&r.historicLowEra?'Below + Era':r.belowMarket?'Below market':'Historic-low era') : 'Not flagged'},
@@ -205,16 +260,208 @@ function badge(r){
   return html;
 }
 
-function mountGrid(rows){
-  GRID=CrcpGrid.mount({
-    data:rows, fields:FIELDS, facets:FACETS,
-    mount:'#cgMain', rail:'#cgRail', view:'list', cols:2,
-    storageKey:'fha', titleCol:'Property',
-    title:r=>({ name:r.property||'(name n/a)', sub:[r.city,r.state].filter(Boolean).join(', ')+' '+(r.zip||'')+(r.holder?' · '+r.holder:'') }),
-    search:r=>((r.property||'')+' '+(r.city||'')+' '+(r.state||'')+' '+(r.zip||'')+' '+(r.holder||'')),
-    badgeHtml:r=> r.red ? `<div style="margin:2px 0">${badge(r)}</div>` : '',
-    onRow:r=>({cls:r.red?'red':''}),
+// ── cell formatting ───────────────────────────────────────────────────────────
+const fmtDate=v=>{ if(!v) return '—'; try{ return new Date(v).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }catch(e){ return '—'; } };
+function fval(r,c){ return c.calc?c.calc(r):r[c.k]; }
+function cell(r,c){
+  const v=fval(r,c);
+  if(c.k==='flag') return r.red?badge(r):'<span style="color:var(--muted)">—</span>';
+  if(v==null||v==='') return '<span style="color:var(--muted)">—</span>';
+  if(c.t==='pct')   return '<span class="rate">'+(+v).toFixed(2)+'%</span>';
+  if(c.t==='money') return F.money(+v);
+  if(c.t==='num')   return (+v).toLocaleString();
+  if(c.k==='originationDate') return fmtDate(v);
+  return esc(v);
+}
+
+// ── column visibility (START TIGHT — only def:1 on first load; saved pref wins) ─
+let VISCOL=(function(){let s={};try{s=JSON.parse(localStorage.getItem('fhaCols')||'{}');}catch(e){}return s;})();
+function colVis(k){ const c=COLS.find(x=>x.k===k); if(k in VISCOL) return !!VISCOL[k]; return c?!!c.def:false; }
+function saveCols(){ try{localStorage.setItem('fhaCols',JSON.stringify(VISCOL));}catch(e){} }
+// ── draggable column ORDER (persisted per page) ───────────────────────────────
+let COLORDER=(function(){let a=[];try{a=JSON.parse(localStorage.getItem('fhaColOrder')||'[]');}catch(e){}return Array.isArray(a)?a:[];})();
+function syncColOrder(){
+  const keys=COLS.map(c=>c.k);
+  COLORDER=COLORDER.filter(k=>keys.includes(k));
+  keys.forEach(k=>{ if(!COLORDER.includes(k)) COLORDER.push(k); });
+}
+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 saveColOrder(){ try{localStorage.setItem('fhaColOrder',JSON.stringify(COLORDER));}catch(e){} }
+// Hand the table back to col-resize.js when the visible column set/order changes.
+let lastColSig='';
+function reinitResize(){
+  const tbl=document.querySelector('#ftable'); 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();
+}
+
+// ── search + sort state ───────────────────────────────────────────────────────
+let q='', sortKey='rate', sortDir=1;
+const FF={ chips:{}, ranges:{} };
+const gv=(r,fc)=>fc.calc?fc.calc(r):r[fc.k];
+FACETS.forEach(fc=>{ if(fc.type==='chip') FF.chips[fc.k]=new Set(); else FF.ranges[fc.k]={min:null,max:null}; });
+function passFacets(r){
+  for(const fc of FACETS){
+    if(fc.type==='chip'){ const set=FF.chips[fc.k]; if(set.size && !set.has(String(gv(r,fc)))) return false; }
+    else { const rg=FF.ranges[fc.k], v=gv(r,fc);
+      if(rg.min!=null && !(+v>=rg.min)) return false;
+      if(rg.max!=null && !(+v<=rg.max)) return false; }
+  }
+  return true;
+}
+function filtered(){
+  let rows=ROWS.filter(passFacets);
+  if(q){
+    // Search ALL data — every field on the row, incl. nested (JSON.stringify). 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=fval(a,c), y=fval(b,c);
+    const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
+    if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
+    if(x<y) return -sortDir; if(x>y) return sortDir; return 0;
+  });
+  return rows;
+}
+
+// ── render table ──────────────────────────────────────────────────────────────
+function render(){
+  const rows=filtered(), cols=visCols();
+  $('#count').innerHTML=`<b>${rows.length.toLocaleString()}</b> of ${ROWS.length.toLocaleString()} loans`;
+  $('#fthead').innerHTML='<tr>'+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 sorted':' desc sorted'):''}">${esc(c.l)}<span class="ar"></span></th>`).join('')+'</tr>';
+  $('#ftbody').innerHTML=rows.map(r=>`<tr class="frow${r.red?' red':''}" data-id="${esc(r.__i)}">`
+    +cols.map(c=>{const numT=(c.t==='pct'||c.t==='money'||c.t==='num');return `<td class="${c.k==='property'?'prop':''}${numT?' num':''}">`
+      +(c.k==='property'?(esc(r.property||'(name n/a)')+(r.city||r.state?`<span class="loc">${esc([r.city,r.state].filter(Boolean).join(', ')+' '+(r.zip||''))}${r.holder?' · '+esc(r.holder):''}</span>`:'')):cell(r,c))
+      +'</td>';}).join('')+'</tr>').join('');
+  if(!rows.length){ $('#femptymsg').style.display='block'; $('#femptymsg').textContent='no matches for these filters.'; }
+  else $('#femptymsg').style.display='none';
+  const csig=cols.map(c=>c.k).join(',');
+  if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
+}
+
+// ── expandable row → detail modal (all fields) ───────────────────────────────
+function openDetail(i){
+  const r=ROWS[+i]; if(!r) return;
+  const ov=$('#fov'), mb=$('#fmbody'); ov.classList.add('on');
+  const rowsHtml=COLS.map(c=>`<div class="litem"><span class="m">${esc(c.l)}</span><span>${cell(r,c)}</span></div>`).join('');
+  // Any extra fields the API returned that aren't defined columns:
+  const known=new Set(COLS.map(c=>c.k).concat(['red','belowMarket','historicLowEra','redReasons','__i']));
+  const extra=Object.keys(r).filter(k=>!known.has(k)&&r[k]!=null&&typeof r[k]!=='object')
+    .map(k=>`<div class="litem"><span class="m">${esc(k)}</span><span>${esc(r[k])}</span></div>`).join('');
+  mb.innerHTML=`<h3>${esc(r.property||'(name n/a)')}</h3>`
+    +`<div class="msub">${esc([r.city,r.state].filter(Boolean).join(', ')+' '+(r.zip||''))}${r.holder?' · '+esc(r.holder):''}</div>`
+    +(r.red?`<div style="margin:6px 0 10px">${badge(r)}</div>`:'')
+    +rowsHtml+extra
+    +((r.redReasons&&r.redReasons.length)?`<div style="margin-top:12px;font-size:12px;color:var(--muted);line-height:1.6">${r.redReasons.map(esc).join('<br>')}</div>`:'');
+}
+
+// ── left rail: facets + column toggles ────────────────────────────────────────
+function buildRail(){
+  let html='';
+  FACETS.forEach(fc=>{
+    if(fc.type==='chip'){
+      const counts={};
+      ROWS.forEach(r=>{ const v=gv(r,fc); if(v!=null&&v!=='') counts[v]=(counts[v]||0)+1; });
+      const ent=Object.entries(counts).sort((a,b)=>b[1]-a[1]).slice(0,fc.max||20);
+      if(!ent.length) return;
+      html+=`<div class="fha-sec collapsed"><h4>${esc(fc.l)}</h4><div class="fchips" data-facet="${esc(fc.k)}">`
+        +ent.map(([k,c])=>`<span class="fchip" data-v="${esc(k)}">${esc(k)}<span class="fct">${c}</span></span>`).join('')+`</div></div>`;
+    } else {
+      html+=`<div class="fha-sec collapsed"><h4>${esc(fc.l)}</h4><div class="frow2" data-facet="${esc(fc.k)}">`
+        +`<div class="ffld"><label>Min</label><input type="number" class="fmin" placeholder="any"></div>`
+        +`<div class="ffld"><label>Max</label><input type="number" class="fmax" placeholder="any"></div></div></div>`;
+    }
   });
+  // column toggles — every column toggleable, grouped
+  const groups={}; COLS.forEach(c=>{ (groups[c.g||'Other']=groups[c.g||'Other']||[]).push(c); });
+  html+=`<div class="fha-sec"><h4>Fields / columns — show / hide</h4><div class="ftogs">`
+    +Object.entries(groups).map(([g,cs])=>`<div class="fgrp">${esc(g)}</div>`
+      +cs.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join('')).join('')
+    +`</div><div class="fmini"><button data-all="1">All</button><button data-none="1">None</button>`
+    +`<button id="fReset" title="Reset columns to the tight default — order, widths &amp; visibility">↺ Reset</button></div>`
+    +`<div style="font-size:10.5px;color:var(--muted);margin-top:7px;line-height:1.35">Drag a header to reorder · drag its right edge to resize · click to sort.</div></div>`;
+  $('#fRail').innerHTML=html;
+
+  // wire facets
+  $('#fRail').querySelectorAll('.fchips').forEach(box=>box.addEventListener('click',e=>{
+    const c=e.target.closest('.fchip'); if(!c) return;
+    const set=FF.chips[box.dataset.facet], v=c.dataset.v;
+    set.has(v)?set.delete(v):set.add(v); c.classList.toggle('active'); render();
+  }));
+  $('#fRail').querySelectorAll('.frow2').forEach(box=>{
+    const k=box.dataset.facet;
+    box.querySelector('.fmin').addEventListener('input',e=>{FF.ranges[k].min=e.target.value?+e.target.value:null;render();});
+    box.querySelector('.fmax').addEventListener('input',e=>{FF.ranges[k].max=e.target.value?+e.target.value:null;render();});
+  });
+  // column toggles
+  $('#fRail').querySelector('.ftogs').addEventListener('change',e=>{const cb=e.target.closest('input[data-ck]');if(!cb)return;VISCOL[cb.dataset.ck]=cb.checked;saveCols();render();});
+  const mini=$('#fRail').querySelector('.fmini');
+  mini.addEventListener('click',e=>{
+    const b=e.target.closest('button'); if(!b) return;
+    if(b.id==='fReset'){ VISCOL={}; saveCols(); COLORDER=[]; saveColOrder();
+      try{Object.keys(localStorage).forEach(k=>{ if(k.indexOf('cr:'+location.pathname+':')===0) localStorage.removeItem(k); });}catch(e){}
+      lastColSig=''; buildRail(); render(); return; }
+    if(b.dataset.all) COLS.forEach(c=>VISCOL[c.k]=true);
+    else if(b.dataset.none) COLS.forEach(c=>VISCOL[c.k]=false);
+    saveCols(); buildRail(); render();
+  });
+  // collapsible headers
+  $('#fRail').querySelectorAll('.fha-sec > h4').forEach(h=>h.addEventListener('click',()=>h.parentElement.classList.toggle('collapsed')));
+}
+
+// ── wire header sort + drag-reorder + row-click + search + density ─────────────
+function wireGrid(){
+  const thead=$('#fthead');
+  thead.addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;
+    if(sortKey===k)sortDir*=-1;else{const c=COLS.find(x=>x.k===k);sortKey=k;sortDir=(c&&(c.t==='money'||c.t==='num'||c.t==='pct'||c.k==='originationDate'))?-1:1;}
+    localStorage.setItem('fhaSortKey',sortKey);localStorage.setItem('fhaSortDir',sortDir);render();});
+  // drag-to-reorder columns
+  let dragKey=null;
+  thead.addEventListener('dragstart',e=>{ const th=e.target.closest('th'); if(!th) return;
+    if(e.target.closest('.cr-handle')){ e.preventDefault(); return; }
+    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===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) 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('#fthead th.dragging,#fthead th.dropL,#fthead th.dropR').forEach(x=>x.classList.remove('dragging','dropL','dropR')); dragKey=null; });
+  // row click → detail modal
+  $('#ftbody').addEventListener('click',e=>{ const tr=e.target.closest('tr.frow'); if(tr) openDetail(tr.dataset.id); });
+  $('#fov').addEventListener('click',e=>{ if(e.target.id==='fov') $('#fov').classList.remove('on'); });
+  // search
+  const qi=$('#q'); qi.addEventListener('input',()=>{ clearTimeout(qi._t); qi._t=setTimeout(()=>{q=qi.value.trim();render();},140); });
+  // density → column count / card min (drives table font scale here via --tsz stays; use --cg? keep simple: font size + col width n/a for table → adjust --tsz)
+  const dz=$('#density');
+  const applyDensity=v=>document.documentElement.style.setProperty('--tsz',(10+(+v-4)/20*6).toFixed(1)+'px');
+  const dv=parseInt(localStorage.getItem('fhaDensity'),10), dval=(dv>=4&&dv<=24)?dv:13;
+  dz.value=dval; applyDensity(dval);
+  dz.addEventListener('input',()=>{const v=+dz.value;applyDensity(v);localStorage.setItem('fhaDensity',v);});
+  // restore saved sort
+  const sk=localStorage.getItem('fhaSortKey'), sd=localStorage.getItem('fhaSortDir');
+  if(sk&&COLS.some(c=>c.k===sk))sortKey=sk; if(sd==='1'||sd==='-1')sortDir=+sd;
+}
+
+function mountGrid(rows){
+  ROWS=rows.map((r,i)=>{ r.__i=i; return r; });
+  buildRail(); wireGrid(); render();
 }
 
 function renderTiles(d){
@@ -232,7 +479,7 @@ function renderTiles(d){
 async function load(){
   let d;
   try{ d=await (await fetch('/data/fha-loans.json')).json(); }
-  catch(e){ $('#asof').textContent='data load error — run scripts/fetch-fha-loans.js to populate data/fha-loans.json'; $('#cgMain').innerHTML='<div class="empty" style="padding:50px">No data file. Run <code>node scripts/fetch-fha-loans.js</code> to fetch real HUD + PMMS data.</div>'; return; }
+  catch(e){ $('#asof').textContent='data load error — run scripts/fetch-fha-loans.js to populate data/fha-loans.json'; const em=$('#femptymsg'); if(em){em.style.display='block';em.innerHTML='No data file. Run <code>node scripts/fetch-fha-loans.js</code> to fetch real HUD + PMMS data.';} return; }
   DATA=d; ROWS=d.rows||[];
   renderTiles(d);
   // source links
diff --git a/public/gov-agents.html b/public/gov-agents.html
index a76ee0c..da87163 100644
--- a/public/gov-agents.html
+++ b/public/gov-agents.html
@@ -26,9 +26,37 @@
   table.g thead th{position:sticky;top:0;background:#1b2230;color:var(--mut);text-transform:uppercase;letter-spacing:.3px;font-size:10.5px;padding:8px 12px;border-bottom:1px solid var(--line);cursor:pointer;user-select:none;z-index:2;}
   table.g thead th:hover{color:var(--ink);} table.g thead th.asc::after{content:' ▲';color:var(--gold);} table.g thead th.desc::after{content:' ▼';color:var(--gold);}
   table.g tbody td{padding:7px 12px;border-bottom:1px solid #20262f;} table.g tbody tr:hover td{background:rgba(200,162,75,.07);}
+  table.g tbody tr{cursor:pointer;}
   .lic{display:inline-block;font-size:10px;font-weight:700;padding:1px 7px;border-radius:8px;}
   .lic.b{background:rgba(200,162,75,.16);color:var(--gold);} .lic.s{background:rgba(88,166,255,.16);color:var(--blue);} .lic.o{background:rgba(139,148,158,.14);color:var(--mut);}
   .miss{color:#5A6472;} .cap{color:var(--warn);}
+  /* draggable column reorder (Steve rule: move columns around) */
+  table.g thead th.dragcol{cursor:grab;}
+  table.g thead th.dragging{opacity:.45;}
+  table.g thead th.dropL{box-shadow:inset 3px 0 0 var(--gold);}
+  table.g thead th.dropR{box-shadow:inset -3px 0 0 var(--gold);}
+  /* ---- left rail (columns show/hide) ---- */
+  .shell{display:flex;align-items:flex-start;}
+  .rail{flex:0 0 250px;background:#0a0d13;border-right:1px solid var(--line);align-self:stretch;position:sticky;top:53px;height:calc(100vh - 53px);overflow-y:auto;padding:0 0 40px;z-index:5;}
+  .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);}
+  .fieldtoggles{display:flex;flex-direction:column;gap:1px;max-height:340px;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(200,162,75,.08);} .ftog input{accent-color:var(--gold);}
+  .fgrp{font-size:9.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);margin:9px 0 3px;font-weight:700;opacity:.85;} .fgrp:first-child{margin-top:0;}
+  .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(--gold);color:var(--ink);}
+  .rwrap{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; }
+  /* ---- row-detail modal ---- */
+  #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(620px,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:20px;} #modal .mfirm{color:var(--gold);font-size:13px;}
+  #modal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
+  #modal .litem{display:flex;justify-content:space-between;gap:12px;padding:5px 0;border-bottom:1px solid #20262f;font-size:13px;} #modal .litem .m{color:var(--mut);}
 </style>
 </head>
 <body>
@@ -36,44 +64,144 @@
   <h1>🏛️ <b>Government-Licensed</b> Agents</h1>
   <div class="nav"><a href="/broker-grid.html">← Brokers</a><a href="/mls.html">📋 Listings</a><a href="/crcp.html">🛰️ CRCP</a></div>
   <span class="spacer"></span>
-  <input class="search" id="q" type="text" placeholder="search name, firm, license #, city…">
+  <input class="search" id="q" type="text" placeholder="search all fields — name, firm, license #, city, county, zip, expiry… (space = AND)">
   <select id="sortsel" title="Sort"></select>
   <label class="densl" title="Rows per fetch">⤢<input type="range" id="density" min="200" max="2000" step="100" value="500"></label>
   <span class="count" id="count">…</span>
 </header>
 <div class="metros" id="metros"></div>
 <div class="note" id="note"></div>
-<div class="tblwrap"><table class="g"><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
+<div class="shell">
+<aside class="rail">
+  <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;color:var(--mut)">Drag a column header to reorder · drag its right edge to resize · click to sort · click a row for full detail.</div>
+  </div>
+</aside>
+<div class="rwrap">
+  <div class="tblwrap"><table class="g" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
+</div>
+</div>
+<div id="ov"><div id="modal"><button class="x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
 <script>
 const $=s=>document.querySelector(s);
 const esc=s=>(s==null?'':String(s)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
-const COLS=[['name','Name'],['firm','Firm / Company'],['license_type','License Type'],['license_number','License #'],
-  ['city','City'],['county','County'],['zip','Zip'],['expiration','Expires'],['state','State']];
-let METRO='', AGENTS=[], META={}, q='', sortKey='name', sortDir=1, limit=500, timer=null;
-
-$('#sortsel').innerHTML=COLS.map(([k,l])=>`<option value="${k}">Sort: ${l}</option>`).join('');
+// Every field carries a group `g` for the organized rail toggles. Order below IS the
+// column order (before any saved reorder). Every field is sortable (raw-value sort).
+const COLS=[
+  {k:'name',l:'Name',g:'Identity'},
+  {k:'firm',l:'Firm / Company',g:'Identity'},
+  {k:'license_type',l:'License Type',g:'License'},
+  {k:'license_number',l:'License #',g:'License'},
+  {k:'expiration',l:'Expires',g:'License'},
+  {k:'status',l:'Status',g:'License',def:0},
+  {k:'city',l:'City',g:'Location'},
+  {k:'county',l:'County',g:'Location'},
+  {k:'zip',l:'Zip',g:'Location'},
+  {k:'state',l:'State',g:'Location'},
+];
+let METRO='', AGENTS=[], META={}, q='', sortKey='name', sortDir=1, limit=500, timer=null, lastColSig='';
+// START TIGHT (Steve rule): 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(['name','firm','license_type','license_number','expiration','status']);
+let VISCOL=(function(){let s={};try{s=JSON.parse(localStorage.getItem('govCols')||'{}');}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('govColOrder')||'[]');}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('govCols',JSON.stringify(VISCOL));}catch(e){} }
+function saveColOrder(){ try{localStorage.setItem('govColOrder',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('.tblwrap table.g'); 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();
+}
+// Auto-discover any extra field the API returns beyond the known COLS, so every data
+// point is toggleable + searchable.
+function autoCols(){
+  const known=new Set(COLS.map(c=>c.k)); const skip=new Set(['id','metro']); const seen=new Set();
+  AGENTS.slice(0,60).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);
+    COLS.push({k, l:k.replace(/_/g,' ').replace(/\b\w/g,ch=>ch.toUpperCase()).slice(0,22), g:'Other', def:0});
+  }));
+}
 function licCls(t){ t=(t||'').toLowerCase(); if(t.includes('broker'))return 'b'; if(t.includes('sales'))return 's'; return 'o'; }
-function cell(a,k){
+function cell(a,c){
+  const k=c.k;
   if(k==='license_type') return `<span class="lic ${licCls(a[k])}">${esc(a[k]||'—')}</span>`;
   if(k==='name') return '<b>'+esc(a[k])+'</b>';
   const v=a[k]; return v==null||v===''?'<span class="miss">—</span>':esc(v);
 }
-function render(){
-  let rows=AGENTS.slice().sort((x,y)=>{
-    let a=(x[sortKey]||'').toString().toLowerCase(), b=(y[sortKey]||'').toString().toLowerCase();
+function buildFieldToggles(){
+  const groups={};
+  COLS.forEach(c=>{ (groups[c.g||'Other']=groups[c.g||'Other']||[]).push(c); });
+  $('#fFields').innerHTML=Object.entries(groups).map(([g,cs])=>
+    `<div class="fgrp">${esc(g)}</div>`+cs.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join('')
+  ).join('');
+}
+function filtered(){
+  let rows=AGENTS.slice();
+  if(q){
+    // Search ALL data (Steve rule): 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));
+    });
+  }
+  rows.sort((x,y)=>{
+    let a=x[sortKey], b=y[sortKey];
+    const an=(a!=null&&a!==''&&!isNaN(+a)), bn=(b!=null&&b!==''&&!isNaN(+b));
+    if(an&&bn){ a=+a; b=+b; } else { a=String(a==null?'':a).toLowerCase(); b=String(b==null?'':b).toLowerCase(); }
     return a<b?-sortDir:a>b?sortDir:0;
   });
-  $('#thead').innerHTML='<tr>'+COLS.map(([k,l])=>`<th data-k="${k}" class="${sortKey===k?(sortDir>0?'asc':'desc'):''}">${esc(l)}</th>`).join('')+'</tr>';
-  $('#tbody').innerHTML=rows.map(a=>'<tr>'+COLS.map(([k])=>`<td>${cell(a,k)}</td>`).join('')+'</tr>').join('')||'<tr><td colspan="9" class="miss" style="padding:20px">no matches</td></tr>';
-  const capped=META.total>rows.length;
-  $('#count').textContent=`${rows.length}${capped?' of '+META.total.toLocaleString():''} agents`;
-  $('#note').innerHTML=`Source: <b>${esc(META.source||'?')}</b> · ${esc(META.label||'')}`+(capped?` <span class="cap">— showing first ${rows.length} of ${META.total.toLocaleString()} matches; refine your search to narrow.</span>`:'');
+  return rows;
 }
+function render(){
+  const cols=visCols();
+  const rows=filtered();
+  $('#thead').innerHTML='<tr>'+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>';
+  $('#tbody').innerHTML=rows.map((a,i)=>`<tr data-i="${i}">`+cols.map(c=>`<td>${cell(a,c)}</td>`).join('')+'</tr>').join('')||`<tr><td colspan="${cols.length||1}" class="miss" style="padding:20px">no matches</td></tr>`;
+  ROWS=rows;
+  const capped=META.total>AGENTS.length;
+  $('#count').textContent=`${rows.length}${(q||capped)?' of '+(capped?META.total.toLocaleString():AGENTS.length):''} agents`;
+  $('#note').innerHTML=`Source: <b>${esc(META.source||'?')}</b> · ${esc(META.label||'')}`+(capped?` <span class="cap">— server returned ${AGENTS.length.toLocaleString()} of ${META.total.toLocaleString()} matches; refine your search to narrow.</span>`:'');
+  const csig=cols.map(c=>c.k).join(',');
+  if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
+}
+let ROWS=[];
+// ── expandable row: click a row for full detail (all fields) ──
+function openDetail(a){
+  const ov=$('#ov'), mb=$('#mbody'); ov.classList.add('on');
+  const rows=COLS.filter(c=>{const v=a[c.k];return v!=null&&v!=='';})
+    .map(c=>`<div class="litem"><span class="m">${esc(c.l)}</span><span>${c.k==='license_type'?`<span class="lic ${licCls(a[c.k])}">${esc(a[c.k])}</span>`:esc(a[c.k])}</span></div>`).join('');
+  // include any extra keys not in COLS too
+  const known=new Set(COLS.map(c=>c.k));
+  const extra=Object.keys(a).filter(k=>!known.has(k)&&k!=='id'&&a[k]!=null&&a[k]!==''&&typeof a[k]!=='object')
+    .map(k=>`<div class="litem"><span class="m">${esc(k)}</span><span>${esc(a[k])}</span></div>`).join('');
+  mb.innerHTML=`<h3>${esc(a.name||'—')}</h3><div class="mfirm">${esc(a.firm||'—')}${a.license_type?' · '+esc(a.license_type):''}${a.license_number?' · '+esc(a.license_number):''}</div><div style="margin-top:12px">${rows}${extra}</div>`;
+}
+document.addEventListener('click',e=>{ if(e.target.id==='ov'){ $('#ov').classList.remove('on'); return; }
+  const tr=e.target.closest('#tbody tr'); if(tr && tr.dataset.i!=null && e.target.tagName!=='A'){ const a=ROWS[+tr.dataset.i]; if(a) openDetail(a); } });
+document.addEventListener('keydown',e=>{ if(e.key==='Escape') $('#ov').classList.remove('on'); });
 async function loadMetro(){
   $('#count').textContent='loading…';
   const u=`/api/gov-agents?metro=${encodeURIComponent(METRO)}&limit=${limit}`+(q?`&q=${encodeURIComponent(q)}`:'');
   let d; try{ d=await (await fetch(u)).json(); }catch(e){ $('#note').textContent='load failed'; return; }
-  AGENTS=d.agents||[]; META=d; render();
+  AGENTS=d.agents||[]; META=d; autoCols(); buildSortSel(); buildFieldToggles(); render();
 }
 async function loadIndex(){
   let d; try{ d=await (await fetch('/api/gov-agents')).json(); }catch(e){ $('#metros').textContent='load failed'; return; }
@@ -82,11 +210,52 @@ async function loadIndex(){
   if(ms.length){ METRO=ms[0].metro; setActive(); loadMetro(); }
 }
 function setActive(){ document.querySelectorAll('.mchip').forEach(c=>c.classList.toggle('on',c.dataset.m===METRO)); }
+function buildSortSel(){
+  const groups={};
+  COLS.forEach(c=>{ (groups[c.g||'Other']=groups[c.g||'Other']||[]).push(c); });
+  $('#sortsel').innerHTML=Object.entries(groups).map(([g,cs])=>
+    `<optgroup label="${esc(g)}">`+cs.map(c=>`<option value="${c.k}">${esc(c.l)}</option>`).join('')+`</optgroup>`).join('');
+  if(COLS.some(c=>c.k===sortKey)) $('#sortsel').value=sortKey;
+}
 $('#metros').addEventListener('click',e=>{const c=e.target.closest('.mchip');if(!c)return;METRO=c.dataset.m;setActive();loadMetro();});
-$('#q').addEventListener('input',()=>{q=$('#q').value.trim();clearTimeout(timer);timer=setTimeout(loadMetro,280);});
+// Search: keep the server-side query (narrows the fetch) AND filter client-side across ALL fields.
+$('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();clearTimeout(timer);timer=setTimeout(loadMetro,280);});
 $('#sortsel').addEventListener('change',()=>{sortKey=$('#sortsel').value;render();});
 $('#thead').addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=1;}$('#sortsel').value=sortKey;render();});
 $('#density').addEventListener('change',()=>{limit=+$('#density').value;loadMetro();});
+// ── drag-to-reorder columns (Steve rule: move columns around) ──
+let dragKey=null;
+$('#thead').addEventListener('dragstart',e=>{ const th=e.target.closest('th'); if(!th) 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===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) 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; });
+// ── left-rail column show/hide toggles ──
+$('#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(); });
+// collapsible rail tabs
+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'); }); }
+buildSortSel(); buildFieldToggles();
 loadIndex();
 </script>
   <script src="/col-resize.js" defer></script>
diff --git a/public/licensed-agents.html b/public/licensed-agents.html
index 1490cf3..5b3885f 100644
--- a/public/licensed-agents.html
+++ b/public/licensed-agents.html
@@ -43,6 +43,22 @@
   .bstat .sv{color:var(--acc);} .bstat .sp{color:var(--warn);} .bstat .su{color:var(--mut);}
   .agnum{font-size:26px;font-weight:800;line-height:1;font-variant-numeric:tabular-nums;}
   .agnum small{font-size:11px;font-weight:500;color:var(--mut);}
+  .card{cursor:pointer;} .card:hover{border-color:var(--blue);}
+  /* show / hide data points (Steve rule 2026-07-31: START TIGHT, every field toggleable) */
+  .fieldbar{display:flex;flex-wrap:wrap;gap:6px 8px;align-items:center;padding:9px 18px;border-bottom:1px solid var(--line);}
+  .fieldbar .fbl{font-size:10.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);margin-right:2px;}
+  .ftog{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--ink);cursor:pointer;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:3px 9px;user-select:none;}
+  .ftog:hover{border-color:var(--blue);} .ftog input{accent-color:var(--blue);margin:0;}
+  .ftog.off{color:var(--mut);opacity:.7;}
+  .minibtn{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:12px;padding:3px 9px;font-size:11px;cursor:pointer;} .minibtn:hover{border-color:var(--warn);color:var(--ink);}
+  /* card-expand detail modal — all fields */
+  #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(560px,96vw);background:var(--card);border:1px solid var(--line);border-radius:14px;padding:20px 22px;position:relative;}
+  #modal h3{margin:0 0 2px;font-size:19px;} #modal .msub{color:var(--mut);font-size:13px;margin-bottom:10px;}
+  #modal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
+  #modal .kv{margin-top:8px;display:grid;grid-template-columns:auto 1fr;gap:5px 12px;font-size:13px;}
+  #modal .kv .k{color:var(--mut);} #modal .kv .v a{color:var(--blue);text-decoration:none;}
 </style>
 </head>
 <body>
@@ -50,7 +66,7 @@
   <h1>🎫 Licensed <b>Agents &amp; Brokers</b></h1>
   <div class="nav"><a href="/mls.html">← Listings</a><a href="/">Explorer</a><a href="/broker-grid.html">Broker Grid</a><a href="/crcp.html">🛰️ CRCP</a></div>
   <div class="modes" id="modes"><button class="mode on" data-m="agents">👤 Agents</button><button class="mode" data-m="brokers">🏢 Brokers</button></div>
-  <input class="search" id="q" type="text" placeholder="search name, brokerage, license #, responsible broker…">
+  <input class="search" id="q" type="text" placeholder="search ALL fields — name, brokerage, license #, type, city, status, broker… (space = AND)">
   <select id="sortsel" title="Sort">
     <option value="name">Sort: Name A→Z</option>
     <option value="status">Sort: Status</option>
@@ -63,13 +79,54 @@
   <span class="count" id="count">…</span>
 </header>
 <div class="bar" id="pills"></div>
+<div class="fieldbar" id="fieldbar" style="display:none"></div>
 <div class="note" id="note"></div>
 <main><div class="grid" id="grid"></div></main>
+<div id="ov"><div id="modal"><button class="x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
 <script>
 const $=s=>document.querySelector(s);
 let DATA=[], META={}, filt='all', q='', sortKey='name';
 const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
 
+// ── Card data-point fields (Steve rule 2026-07-31: show/hide toggles, START TIGHT, expand for detail) ──
+// Every value the /api/agents/licensed feed carries per agent. `on` = the tight default set.
+const AGENT_FIELDS=[
+  {k:'license',       l:'License'},
+  {k:'license_type',  l:'Type'},
+  {k:'expiration',    l:'Expires'},
+  {k:'responsible_broker', l:'Resp. broker'},
+  {k:'discipline',    l:'Discipline'},
+  {k:'license_city',  l:'City'},
+  {k:'issued',        l:'Issued'},
+  {k:'status',        l:'Status (raw)'},
+  {k:'match_confidence', l:'Match confidence'},
+];
+// START TIGHT — only these show by default; the user toggles the rest on. A saved pref always wins.
+const FIELD_DEFAULT_ON=new Set(['license','license_type','expiration','responsible_broker','discipline']);
+let FIELDVIS=(function(){let s={};try{s=JSON.parse(localStorage.getItem('laFields')||'{}');}catch(e){}return s;})();
+function fieldVis(k){ if(k in FIELDVIS) return !!FIELDVIS[k]; return FIELD_DEFAULT_ON.has(k); }
+function saveFields(){ try{localStorage.setItem('laFields',JSON.stringify(FIELDVIS));}catch(e){} }
+function visFields(){ return AGENT_FIELDS.filter(f=>fieldVis(f.k)); }
+// Render one field value for a card / modal row (blue link for the license, plain otherwise).
+function fieldVal(a,k){
+  if(k==='license') return a.dre_url?`<a href="${esc(a.dre_url)}" target="_blank" rel="noopener noreferrer">${esc(a.license||'—')} ↗</a>`:esc(a.license||'—');
+  if(k==='responsible_broker') return esc((a.responsible_broker||'—').split(/\s\d/)[0]).slice(0,60);
+  return esc(a[k]||'—');
+}
+function buildFieldBar(){
+  const fb=$('#fieldbar');
+  fb.innerHTML='<span class="fbl">Data points:</span>'+
+    AGENT_FIELDS.map(f=>`<label class="ftog${fieldVis(f.k)?'':' off'}"><input type="checkbox" data-fk="${f.k}" ${fieldVis(f.k)?'checked':''}>${esc(f.l)}</label>`).join('')+
+    '<button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fReset" title="Reset data points to the tight default">↺ Reset</button>';
+}
+// Card-expand: full detail of one agent — ALL fields (visible or not).
+function openAgent(a){
+  const ov=$('#ov'), mb=$('#mbody'); ov.classList.add('on');
+  mb.innerHTML='<h3>'+esc(a.agent)+(a.dre_verified?' <span class="verified" title="Verified against CA DRE public license record">✔</span>':'')+'</h3>'
+    +'<div class="msub">'+esc(a.brokerage||'—')+' · <span class="badge '+statusClass(a)+'">'+esc(statusText(a))+'</span></div>'
+    +'<div class="kv">'+AGENT_FIELDS.map(f=>'<span class="k">'+esc(f.l)+'</span><span class="v">'+fieldVal(a,f.k)+'</span>').join('')+'</div>';
+}
+
 function statusClass(a){
   if(a.dre_match==='probable') return 'b-warn';
   if(!a.dre_verified) return a.status_label==='error'?'b-bad':'b-un';
@@ -93,7 +150,12 @@ function render(){
     if(filt==='broker'&&!/broker/i.test(a.license_type||'')) return false;
     if(filt==='salesperson'&&!/salesperson/i.test(a.license_type||'')) return false;
     if(filt==='unmatched'&&a.license) return false;
-    if(q){ const hay=(a.agent+' '+(a.brokerage||'')+' '+(a.license||'')+' '+(a.responsible_broker||'')+' '+(a.license_type||'')).toLowerCase(); if(!hay.includes(q)) return false; }
+    if(q){
+      // Search ALL data (Steve rule 2026-07-31): every field the API returned on the row —
+      // not just a hand-picked few, incl. nested objects. Space = AND.
+      const hay=Object.keys(a).map(k=>{const v=a[k];return v==null?'':(typeof v==='object'?JSON.stringify(v):String(v));}).join('  ').toLowerCase();
+      if(!q.split(/\s+/).filter(Boolean).every(t=>hay.includes(t))) return false;
+    }
     return true;
   });
   const cmp={
@@ -105,26 +167,18 @@ function render(){
     conf:(a,b)=>(a.match_confidence||'zz').localeCompare(b.match_confidence||'zz'),
   }[sortKey];
   rows.sort(cmp);
-  $('#grid').innerHTML=rows.map(cardHtml).join('')||'<div class="note">No agents match.</div>';
+  $('#grid').innerHTML=rows.map((a,i)=>cardHtml(a,DATA.indexOf(a))).join('')||'<div class="note">No agents match.</div>';
   $('#count').textContent=rows.length+' / '+DATA.length+' agents';
 }
-function cardHtml(a){
-  const dre=a.dre_url?`<a href="${esc(a.dre_url)}" target="_blank" rel="noopener noreferrer">${esc(a.license)} ↗</a>`:'—';
-  const rb=a.responsible_broker?esc(a.responsible_broker.split(/\s\d/)[0]).slice(0,60):'—';
-  return `<div class="card">
+function cardHtml(a,idx){
+  const fields=visFields();
+  return `<div class="card" data-idx="${idx}">
     <div class="top">
       <div><div class="nm">${esc(a.agent)}${a.dre_verified?' <span class="verified" title="Verified against CA DRE public license record">✔</span>':''}</div>
         <div class="bk">${esc(a.brokerage||'—')}</div></div>
       <span class="badge ${statusClass(a)}">${esc(statusText(a))}</span>
     </div>
-    <div class="kv">
-      <span class="k">License</span><span class="v">${dre}</span>
-      <span class="k">Type</span><span class="v">${esc(a.license_type||'—')}</span>
-      <span class="k">Expires</span><span class="v">${esc(a.expiration||'—')}</span>
-      <span class="k">Resp. broker</span><span class="v">${rb}</span>
-      <span class="k">Discipline</span><span class="v">${esc(a.discipline||'—')}</span>
-    </div>
-    ${a.match_confidence?`<div class="conf">match: ${esc(a.match_confidence)}</div>`:''}
+    ${fields.length?`<div class="kv">`+fields.map(f=>`<span class="k">${esc(f.l)}</span><span class="v">${fieldVal(a,f.k)}</span>`).join('')+`</div>`:''}
   </div>`;
 }
 function pills(){
@@ -196,12 +250,27 @@ async function setMode(m){
   mode=m; localStorage.setItem('laMode',m);
   document.querySelectorAll('.mode').forEach(x=>x.classList.toggle('on',x.dataset.m===m));
   $('#pills').style.display=m==='brokers'?'none':'';
+  $('#fieldbar').style.display=m==='brokers'?'none':'flex';   // data-point toggles are agents-only
   if(m==='brokers'){ await loadBrokers(); fillSort(BROKER_SORTS,bSort); }
   else { fillSort(AGENT_SORTS,sortKey); load(); }
   draw();
 }
 document.querySelectorAll('.mode').forEach(x=>x.onclick=()=>setMode(x.dataset.m));
 
+// ── card-expand: click an agent card → detail modal (all fields) ──
+$('#grid').addEventListener('click',e=>{ if(mode==='brokers') return; const card=e.target.closest('.card'); if(!card) return;
+  if(e.target.tagName==='A') return;                       // let license links through
+  const a=DATA[+card.dataset.idx]; if(a) openAgent(a); });
+$('#ov').addEventListener('click',e=>{ if(e.target.id==='ov') $('#ov').classList.remove('on'); });
+document.addEventListener('keydown',e=>{ if(e.key==='Escape') $('#ov').classList.remove('on'); });
+// ── data-point show/hide toggles (agents view only) ──
+$('#fieldbar').addEventListener('change',e=>{ const cb=e.target.closest('input[data-fk]'); if(!cb) return;
+  FIELDVIS[cb.dataset.fk]=cb.checked; saveFields(); buildFieldBar(); render(); });
+$('#fieldbar').addEventListener('click',e=>{
+  if(e.target.id==='fAll'){ AGENT_FIELDS.forEach(f=>FIELDVIS[f.k]=true); saveFields(); buildFieldBar(); render(); }
+  if(e.target.id==='fNone'){ AGENT_FIELDS.forEach(f=>FIELDVIS[f.k]=false); saveFields(); buildFieldBar(); render(); }
+  if(e.target.id==='fReset'){ FIELDVIS={}; saveFields(); buildFieldBar(); render(); }
+});
 // controls
 $('#q').oninput=e=>{q=e.target.value.toLowerCase().trim();draw();};
 $('#sortsel').onchange=e=>{ if(mode==='brokers'){bSort=e.target.value;localStorage.setItem('laBSort',bSort);} else {sortKey=e.target.value;localStorage.setItem('laSort',sortKey);} draw();};
@@ -209,7 +278,8 @@ $('#density').oninput=e=>{document.documentElement.style.setProperty('--cw',e.ta
 // restore persisted
 (()=>{const s=localStorage.getItem('laSort'); if(s){sortKey=s;$('#sortsel').value=s;}
   const dv=localStorage.getItem('laDens')||'320'; $('#density').value=dv; document.documentElement.style.setProperty('--cw',dv+'px');})();
-if(localStorage.getItem('laMode')==='brokers') setMode('brokers'); else load();
+buildFieldBar();
+if(localStorage.getItem('laMode')==='brokers') setMode('brokers'); else { $('#fieldbar').style.display='flex'; load(); }
 </script>
   <script src="/col-resize.js" defer></script>
 </body>
diff --git a/public/mls.html b/public/mls.html
index e392146..c701260 100644
--- a/public/mls.html
+++ b/public/mls.html
@@ -25,6 +25,11 @@
   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;}
@@ -93,7 +98,7 @@
   <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="/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 anything — address, city, firm, type, year…">
+  <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>
@@ -115,12 +120,13 @@
   <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></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"><thead id="thead"></thead><tbody id="tbody"></tbody></table></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>
@@ -162,7 +168,7 @@ const COLS=[
   {k:'composite',l:'Score',t:'n'},
   {k:'source',l:'Source',t:'link'},
 ];
-let DATA=[], view='table', sortKey='dom', sortDir=1, q='';  // default landing: freshest listings first (days-on-market asc); localStorage overrides for returning users
+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; };
@@ -184,6 +190,8 @@ function paintBody(){
     $('#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 {
     $('#tableView').classList.add('hide'); $('#gridView').classList.remove('hide');
     const bodyCols=cols.filter(c=>!['address','city','zip','type'].includes(c.k));
@@ -240,10 +248,31 @@ const isEscrow=v=>/pending|contingent/i.test(v||'');
 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]; const c=COLS.find(x=>x.k===k); return c?c.def!==0:true; }
-function visCols(){ return COLS.filter(c=>colVis(c.k)); }
+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(){
@@ -306,7 +335,15 @@ function cellHTML(r,c){
 }
 function filtered(){
   let rows=DATA.filter(passFacets);
-  if(q){ const t=q.toLowerCase(); rows=rows.filter(r=>COLS.some(c=>{const v=cval(r,c);return v!=null&&String(v).toLowerCase().includes(t);})); }
+  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);
@@ -334,13 +371,34 @@ function render(){
     ? `${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}" class="${sortKey===c.k?(sortDir>0?'asc':'desc'):''}">${esc(c.l)}</th>`).join('')+'</tr>';
+  $('#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>';
@@ -407,6 +465,10 @@ $('#pMax').addEventListener('input',()=>{F.pmax=$('#pMax').value?+$('#pMax').val
 $('#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');
diff --git a/public/rent-rolls.html b/public/rent-rolls.html
index 41dad95..ba5136c 100644
--- a/public/rent-rolls.html
+++ b/public/rent-rolls.html
@@ -52,6 +52,54 @@
   .dline{display:flex;justify-content:space-between;font-size:13px;margin-top:8px;color:var(--muted);} .dline b{color:var(--ink);font-variant-numeric:tabular-nums;}
   .verdict{font-size:12px;margin-top:8px;} .note{font-size:11px;color:var(--muted);margin-top:12px;line-height:1.6;}
   .empty{padding:50px 24px;text-align:center;color:var(--muted);} .empty code{background:#0a0d13;padding:2px 7px;border-radius:6px;color:var(--gold);}
+
+  /* ── table-view toolbar + controls (Steve list/table rule 2026-07-31) ── */
+  .toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:16px;}
+  .search{background:var(--glass);border:1px solid var(--glassb);color:var(--ink);border-radius:11px;padding:8px 13px;font-size:13.5px;min-width:260px;flex:1 1 260px;backdrop-filter:blur(14px);}
+  .search::placeholder{color:var(--muted);}
+  .toggle{display:flex;border:1px solid var(--glassb);border-radius:11px;overflow:hidden;background:var(--glass);}
+  .toggle button{background:transparent;border:0;color:var(--muted);padding:8px 13px;font-size:13px;cursor:pointer;}
+  .toggle button.on{background:var(--gold);color:#1a1205;font-weight:700;}
+  #sortsel{background:var(--glass);color:var(--ink);border:1px solid var(--glassb);border-radius:11px;padding:8px 10px;font-size:13px;backdrop-filter:blur(14px);}
+  #sortsel optgroup{color:var(--muted);} #sortsel option{color:var(--ink);background:#0a0d13;}
+  .count{color:var(--muted);font-size:12px;margin-left:auto;}
+  /* Columns ▾ dropdown */
+  .coldrop{position:relative;}
+  .coldrop>button{background:var(--glass);border:1px solid var(--glassb);color:var(--ink);border-radius:11px;padding:8px 12px;font-size:13px;cursor:pointer;backdrop-filter:blur(14px);}
+  .coldrop>button:hover{border-color:rgba(233,196,106,.5);}
+  .colmenu{display:none;position:absolute;right:0;top:calc(100% + 6px);z-index:50;min-width:240px;max-height:60vh;overflow:auto;
+    background:#0c1018;border:1px solid var(--glassb);border-radius:13px;padding:10px;box-shadow:0 16px 44px rgba(0,0,0,.55);}
+  .coldrop.open .colmenu{display:block;}
+  .colmenu .fgrp{font-size:9.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);margin:9px 0 3px;font-weight:700;opacity:.85;}
+  .colmenu .fgrp:first-child{margin-top:0;}
+  .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(233,196,106,.08);} .ftog input{accent-color:var(--gold);}
+  .minibtn{background:var(--glass);color:var(--muted);border:1px solid var(--glassb);border-radius:8px;padding:4px 9px;font-size:11px;cursor:pointer;margin-right:5px;} .minibtn:hover{border-color:var(--gold);color:var(--ink);}
+  .colmenu .mbrow{display:flex;gap:5px;margin-top:9px;border-top:1px solid var(--glassb);padding-top:9px;}
+
+  /* data table */
+  .tblwrap{overflow:auto;border:1px solid var(--glassb);border-radius:16px;background:var(--glass);backdrop-filter:blur(22px) saturate(1.5);}
+  table.g{border-collapse:separate;border-spacing:0;width:100%;font-size:12.5px;white-space:nowrap;}
+  table.g thead th{position:sticky;top:0;background:#12161f;color:var(--muted);text-transform:uppercase;letter-spacing:.3px;font-size:10.5px;padding:9px 11px;border-bottom:1px solid var(--glassb);cursor:pointer;user-select:none;z-index:2;text-align:left;}
+  table.g thead th:hover{color:var(--ink);}
+  table.g thead th.asc::after{content:' ▲';color:var(--gold);} table.g thead th.desc::after{content:' ▼';color:var(--gold);}
+  table.g thead th.dragcol{cursor:grab;}
+  table.g thead th.dragging{opacity:.45;}
+  table.g thead th.dropL{box-shadow:inset 3px 0 0 var(--gold);}
+  table.g thead th.dropR{box-shadow:inset -3px 0 0 var(--gold);}
+  table.g tbody td{padding:8px 11px;border-bottom:1px solid rgba(255,255,255,.06);}
+  table.g tbody tr:hover td{background:rgba(233,196,106,.07);}
+  table.g td.n{text-align:right;font-variant-numeric:tabular-nums;}
+  table.g td.miss{color:#5A6472;}
+  .dscrpill{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;border-radius:9px;font-variant-numeric:tabular-nums;}
+  .dscrpill.pass{background:rgba(52,211,153,.16);color:var(--ok);} .dscrpill.lean{background:rgba(251,146,60,.16);color:var(--warn);} .dscrpill.fail{background:rgba(248,113,113,.16);color:var(--bad);}
+
+  /* detail modal */
+  #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(760px,96vw);position:relative;padding:0;}
+  #modal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--muted);font-size:22px;cursor:pointer;z-index:2;}
+  .hide{display:none;}
 </style>
 </head>
 <body>
@@ -60,15 +108,36 @@
     <div><h1>📑 Rent Rolls → DSCR</h1><div class="sub" id="sub">document-grade underwriting from parsed rent rolls · local · $0</div></div>
     <nav><a href="/">Properties</a><a href="/deals.html">Deals</a><a href="/map.html">Map</a><a href="/crcp.html">CRCP</a></nav>
   </header>
+
+  <div class="toolbar" id="toolbar" style="display:none">
+    <input class="search" id="q" type="text" placeholder="search all fields — address, city, zip, source, units, NOI, rent… (space = AND)">
+    <div class="toggle" id="view"><button data-v="table" class="on">▦ Table</button><button data-v="cards">▤ Cards</button></div>
+    <select id="sortsel" title="Sort rent rolls"></select>
+    <div class="coldrop" id="coldrop">
+      <button id="colbtn" title="Show / hide columns">Columns ▾</button>
+      <div class="colmenu" id="colmenu"></div>
+    </div>
+    <span class="count" id="count">…</span>
+  </div>
+
+  <div id="tableView" class="tblwrap hide"><table class="g" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
   <div id="rolls"></div>
 </div>
+
+<div id="ov"><div id="modal" class="glass"><button class="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 M = n => (n==null||!isFinite(n)) ? '—' : '$'+Math.round(n).toLocaleString();
 const PCT = n => (n==null||!isFinite(n)) ? '—' : n.toFixed(1)+'%';
+const num = n => (n==null||n==='')?'—':Number(n).toLocaleString();
+const dt = s => { if(!s) return '—'; try{ return new Date(s).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }catch(e){ return '—'; } };
 // amortizing payment + annual debt service constant
 function pmt(L, ratePct, years){ const c=ratePct/100/12, n=years*12; return c===0 ? L/n : L*c/(1-Math.pow(1+c,-n)); }
 function annualConstant(ratePct, years){ return pmt(1, ratePct, years)*12; }
 
+/* ── existing per-rent-roll CARD render (faithfully preserved) ── */
 function rollHtml(r, i){
   const uw = r.underwriting||{};
   const noi = uw.noiProxyAnnual||0, inplace = uw.inPlaceRentAnnual||0;
@@ -76,8 +145,8 @@ function rollHtml(r, i){
   const unitRows = units.map(u=>`<tr><td>${u.unit||'—'}</td><td>${u.beds?u.beds+'bd':'—'}</td>
     <td class="${u.status==='vacant'?'vac':'occ'}">${u.status||'—'}</td><td class="n">${u.marketRent?M(u.marketRent):'—'}</td></tr>`).join('');
   return `<div class="glass roll" data-i="${i}">
-    <h2>${(r.property?.address)||r.source||'Rent roll'} ${r.isSample?'<span class="badge">demo</span>':''}</h2>
-    <div class="addr">${[r.property?.city,r.property?.zip].filter(Boolean).join(' ')} · ${r.unitCount||units.length} units · parsed from ${r.source||'PDF'}</div>
+    <h2>${esc((r.property?.address)||r.source||'Rent roll')} ${r.isSample?'<span class="badge">demo</span>':''}</h2>
+    <div class="addr">${esc([r.property?.city,r.property?.zip].filter(Boolean).join(' '))} · ${r.unitCount||units.length} units · parsed from ${esc(r.source||'PDF')}</div>
     <div class="uwgrid">
       <div class="uw"><div class="k">Units</div><div class="v">${uw.unitCount||units.length}</div></div>
       <div class="uw"><div class="k">Occupied</div><div class="v teal">${uw.occupied??'—'} <span style="font-size:12px;color:var(--muted)">/ ${uw.vacant??0} vac</span></div></div>
@@ -99,10 +168,9 @@ function rollHtml(r, i){
         <div class="dscrout" data-out>—</div>
       </div>
     </div>
-    <div class="note">${uw.noiBasis?('NOI basis: '+uw.noiBasis+'. '):''}${r.note||''} DSCR = NOI ÷ annual debt service. Not an appraisal or a lending commitment.</div>
+    <div class="note">${uw.noiBasis?('NOI basis: '+esc(uw.noiBasis)+'. '):''}${esc(r.note||'')} DSCR = NOI ÷ annual debt service. Not an appraisal or a lending commitment.</div>
   </div>`;
 }
-
 function recalc(roll){
   const g = f => parseFloat(roll.querySelector(`[data-f="${f}"]`).value)||0;
   const noi=g('noi'), loan=g('loan'), rate=g('rate'), amort=g('amort'), target=g('target')||1.25;
@@ -118,21 +186,229 @@ function recalc(roll){
      <div class="dline">Monthly payment <b>${M(ads/12)}</b></div>
      <div class="dline">Max loan @ ${target.toFixed(2)} DSCR <b>${M(maxLoan)}</b></div>`;
 }
+function wireCards(){
+  document.querySelectorAll('.roll').forEach(roll=>{
+    recalc(roll);
+    roll.querySelectorAll('input').forEach(inp=>inp.addEventListener('input',()=>recalc(roll)));
+  });
+}
+
+/* ══════════════ TABLE LAYER (7 controls) ══════════════ */
+// Flatten each rent roll into one searchable/sortable row. The original object is
+// kept on __raw so the detail modal can render the full card (units + DSCR calc).
+// A quick DSCR estimate at a benchmark rate powers the DSCR column + sort.
+const DSCR_RATE=6.75, DSCR_AMORT=30, DSCR_LTV_CONST=0.085;
+function estDscr(noi){ if(!noi) return null; const loan=noi/DSCR_LTV_CONST; const ads=pmt(loan,DSCR_RATE,DSCR_AMORT)*12; return ads>0?noi/ads:null; }
+function flatRow(r,i){
+  const uw=r.underwriting||{}, p=r.property||{};
+  const noi=uw.noiProxyAnnual||0;
+  return {
+    id:i, __raw:r,
+    address:p.address||r.source||'Rent roll',
+    city:p.city||'', zip:p.zip||'',
+    units:uw.unitCount||(r.units?r.units.length:0)||r.unitCount||0,
+    occupied:(uw.occupied==null?null:uw.occupied),
+    vacant:(uw.vacant==null?null:uw.vacant),
+    vacancyRate:(uw.vacancyRate==null?null:uw.vacancyRate),
+    grossRent:uw.grossScheduledRentAnnual||null,
+    inPlaceRent:uw.inPlaceRentAnnual||null,
+    noi:noi||null,
+    dscr:estDscr(noi),
+    source:r.source||'', isSample:!!r.isSample,
+    parsedAt:r.parsedAt||'', note:r.note||'', noiBasis:uw.noiBasis||''
+  };
+}
+// COLS: order below IS the column order. Every field is sortable + toggleable.
+const COLS=[
+  {k:'address',l:'Property / Address',t:'s',g:'Identity'},
+  {k:'city',l:'City',t:'s',g:'Identity'},
+  {k:'zip',l:'ZIP',t:'s',g:'Identity',def:0},
+  {k:'units',l:'Units',t:'n',g:'Occupancy'},
+  {k:'occupied',l:'Occupied',t:'n',g:'Occupancy',def:0},
+  {k:'vacant',l:'Vacant',t:'n',g:'Occupancy',def:0},
+  {k:'vacancyRate',l:'Vacancy %',t:'pct',g:'Occupancy',def:0},
+  {k:'grossRent',l:'Gross Rent /yr',t:'money',g:'Underwriting',def:0},
+  {k:'inPlaceRent',l:'In-Place Rent /yr',t:'money',g:'Underwriting',def:0},
+  {k:'noi',l:'NOI /yr',t:'money',g:'Underwriting'},
+  {k:'dscr',l:'DSCR',t:'dscr',g:'Underwriting'},
+  {k:'source',l:'Source',t:'s',g:'Provenance',def:0},
+  {k:'parsedAt',l:'Parsed',t:'date',g:'Provenance',def:0},
+  {k:'note',l:'Note',t:'s',g:'Provenance',def:0},
+  {k:'noiBasis',l:'NOI Basis',t:'s',g:'Provenance',def:0},
+];
+let DATA=[], view='table', sortKey='dscr', sortDir=-1, q='', lastColSig='';
+// START TIGHT: only ~6 essentials on first load; a saved pref (rentCols) always wins.
+const DEFAULT_ON=new Set(['address','city','units','grossRent','noi','dscr']);
+let VISCOL=(function(){let s={};try{s=JSON.parse(localStorage.getItem('rentCols')||'{}');}catch(e){}return s;})();
+function colVis(k){ if(k in VISCOL) return !!VISCOL[k]; return DEFAULT_ON.has(k); }
+// Draggable column ORDER — persisted per browser under rentColOrder.
+let COLORDER=(function(){let a=[];try{a=JSON.parse(localStorage.getItem('rentColOrder')||'[]');}catch(e){}return Array.isArray(a)?a:[];})();
+function syncColOrder(){
+  const keys=COLS.map(c=>c.k);
+  COLORDER=COLORDER.filter(k=>keys.includes(k));
+  keys.forEach(k=>{ if(!COLORDER.includes(k)) COLORDER.push(k); });
+}
+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('rentCols',JSON.stringify(VISCOL));}catch(e){} }
+function saveColOrder(){ try{localStorage.setItem('rentColOrder',JSON.stringify(COLORDER));}catch(e){} }
+// Hand the table back to col-resize.js so it rebuilds <colgroup> + handles.
+function reinitResize(){
+  const tbl=document.querySelector('#tableView table.g'); 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 dscrCls(v){ return v==null?'':(v>=1.25?'pass':v>=1.0?'lean':'fail'); }
+function cell(r,c){
+  const v=r[c.k];
+  if(c.t==='n') return num(v);
+  if(c.t==='money') return M(v);
+  if(c.t==='pct') return PCT(v);
+  if(c.t==='date') return dt(v);
+  if(c.t==='dscr'){ if(v==null) return '<span class="miss">—</span>'; return `<span class="dscrpill ${dscrCls(v)}">${v.toFixed(2)}</span>`; }
+  if(c.k==='address'){ return esc(v==null?'':v)+(r.isSample?' <span class="badge">demo</span>':''); }
+  return esc(v==null||v===''?'—':v);
+}
+function filtered(){
+  let rows=DATA.slice();
+  if(q){
+    // Search ALL data — every field the API returned per row (nested via JSON.stringify),
+    // excluding the __raw back-reference (its contents ARE the row). Space = AND.
+    const terms=q.toLowerCase().split(/\s+/).filter(Boolean);
+    rows=rows.filter(r=>{
+      const src=r.__raw||r;
+      const hay=Object.keys(src).map(k=>{const v=src[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=a[c.k], y=b[c.k];
+    const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
+    if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
+    if(x<y) return -sortDir; if(x>y) return sortDir; return 0;
+  });
+  return rows;
+}
+function render(){
+  if(view==='table'){
+    $('#tableView').classList.remove('hide'); $('#rolls').classList.add('hide');
+    const rows=filtered(); const cols=visCols();
+    $('#count').textContent=`${rows.length} of ${DATA.length} rent rolls`;
+    $('#thead').innerHTML='<tr>'+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>';
+    $('#tbody').innerHTML=rows.map(r=>`<tr class="rrow" data-id="${r.id}" style="cursor:pointer">`+cols.map(c=>{const cls=(c.t==='n'||c.t==='money'||c.t==='pct')?' class="n"':'';return `<td${cls}>${cell(r,c)}</td>`;}).join('')+'</tr>').join('');
+    const csig=cols.map(c=>c.k).join(',');
+    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
+  } else {
+    $('#tableView').classList.add('hide'); $('#rolls').classList.remove('hide');
+    const rows=filtered();
+    $('#count').textContent=`${rows.length} of ${DATA.length} rent rolls`;
+    $('#rolls').innerHTML=rows.map((r,idx)=>rollHtml(r.__raw,idx)).join('');
+    wireCards();
+  }
+}
+// Row click → detail modal with ALL fields (renders the full original card inside).
+function openDetail(id){
+  const r=DATA.find(x=>+x.id===+id); if(!r) return;
+  const ov=$('#ov'), mb=$('#mbody');
+  mb.innerHTML=rollHtml(r.__raw,0).replace('glass roll','roll').replace(/margin-bottom:18px/,'');
+  ov.classList.add('on');
+  const roll=mb.querySelector('.roll');
+  if(roll){ recalc(roll); roll.querySelectorAll('input').forEach(inp=>inp.addEventListener('input',()=>recalc(roll))); }
+}
+
+/* ── column show/hide dropdown ── */
+function buildColMenu(){
+  const groups={};
+  COLS.forEach(c=>{ (groups[c.g||'Other']=groups[c.g||'Other']||[]).push(c); });
+  $('#colmenu').innerHTML=Object.entries(groups).map(([g,cs])=>
+    `<div class="fgrp">${esc(g)}</div>`+cs.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join('')
+  ).join('')+
+  `<div class="mbrow"><button class="minibtn" id="cAll">All</button><button class="minibtn" id="cNone">None</button><button class="minibtn" id="cReset" title="Reset columns to the tight default — order, widths &amp; visibility">↺ Reset</button></div>`;
+}
+
+/* ── sort <select> ── */
+function buildSortSel(){
+  const sel=$('#sortsel'); const groups={};
+  COLS.forEach(c=>{ (groups[c.g||'Other']=groups[c.g||'Other']||[]).push(c); });
+  sel.innerHTML=Object.entries(groups).map(([g,cs])=>
+    `<optgroup label="${esc(g)}">`+cs.map(c=>`<option value="${c.k}">${esc(c.l)}</option>`).join('')+`</optgroup>`).join('');
+  if(Array.from(sel.options).some(o=>o.value===sortKey)) sel.value=sortKey;
+}
+
+/* ══════════════ event wiring ══════════════ */
+$('#q').addEventListener('input',()=>{q=$('#q').value.trim();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('rentView',view);render();});
+// header sort (delegated) — verified to work after a search since filtered() re-applies q
+$('#thead').addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;
+  if(sortKey===k)sortDir*=-1;else{sortKey=k;const c=COLS.find(x=>x.k===k);sortDir=(c&&(c.t==='n'||c.t==='money'||c.t==='pct'||c.t==='dscr'||c.t==='date'))?-1:1;}
+  localStorage.setItem('rentSortKey',sortKey);localStorage.setItem('rentSortDir',sortDir);
+  const sel=$('#sortsel'); if(Array.from(sel.options).some(o=>o.value===sortKey))sel.value=sortKey;
+  render();});
+$('#sortsel').addEventListener('change',()=>{sortKey=$('#sortsel').value;const c=COLS.find(x=>x.k===sortKey);
+  sortDir=(c&&(c.t==='n'||c.t==='money'||c.t==='pct'||c.t==='dscr'||c.t==='date'))?-1:1;
+  localStorage.setItem('rentSortKey',sortKey);localStorage.setItem('rentSortDir',sortDir);render();});
+// drag-to-reorder columns
+let dragKey=null;
+$('#thead').addEventListener('dragstart',e=>{ const th=e.target.closest('th'); if(!th) return;
+  if(e.target.closest('.cr-handle')){ e.preventDefault(); return; }
+  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===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) 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; });
+// row click → detail (ignore clicks on links/buttons/inputs so nested controls still work)
+document.addEventListener('click',e=>{
+  const row=e.target.closest('.rrow');
+  if(row && !e.target.closest('a,button,input')){ openDetail(row.dataset.id); }
+  if(e.target.id==='ov') $('#ov').classList.remove('on');
+});
+// Columns ▾ dropdown open/close + toggles
+$('#colbtn').addEventListener('click',e=>{ e.stopPropagation(); $('#coldrop').classList.toggle('open'); });
+document.addEventListener('click',e=>{ if(!e.target.closest('#coldrop')) $('#coldrop').classList.remove('open'); });
+$('#colmenu').addEventListener('change',e=>{const cb=e.target.closest('input[data-ck]');if(!cb)return;VISCOL[cb.dataset.ck]=cb.checked;saveCols();render();});
+$('#colmenu').addEventListener('click',e=>{
+  if(e.target.id==='cAll'){ COLS.forEach(c=>VISCOL[c.k]=true);saveCols();buildColMenu();render(); }
+  else if(e.target.id==='cNone'){ COLS.forEach(c=>VISCOL[c.k]=false);saveCols();buildColMenu();render(); }
+  else if(e.target.id==='cReset'){ VISCOL={}; saveCols(); COLORDER=[]; saveColOrder();
+    try{Object.keys(localStorage).forEach(k=>{ if(k.indexOf('cr:'+location.pathname+':')===0) localStorage.removeItem(k); });}catch(e2){}
+    lastColSig=''; buildColMenu(); render(); }
+});
 
+/* ══════════════ boot ══════════════ */
 fetch('/api/rent-rolls').then(r=>r.json()).then(d=>{
   const rolls=d.rentRolls||[];
-  const host=document.getElementById('rolls');
+  const host=$('#rolls');
   if(!rolls.length){ host.innerHTML=`<div class="glass empty">No rent rolls parsed yet.<br><br>
     Drop a rent-roll PDF into <code>data/rent-rolls/</code> and run <code>node scripts/rent-roll-ingest.js</code>.<br>
     It parses the unit/rent table locally ($0) and it shows up here with a live DSCR calculator.<br><br>
     <span style="font-size:11px">Rent rolls are confidential — they're read locally, never uploaded or served as PDFs.</span></div>`; return; }
-  document.getElementById('sub').textContent=`${rolls.length} rent roll${rolls.length>1?'s':''} · document-grade NOI → DSCR · local · $0`;
-  host.innerHTML=rolls.map(rollHtml).join('');
-  document.querySelectorAll('.roll').forEach(roll=>{
-    recalc(roll);
-    roll.querySelectorAll('input').forEach(inp=>inp.addEventListener('input',()=>recalc(roll)));
-  });
-}).catch(e=>{ document.getElementById('rolls').innerHTML='<div class="glass empty">Failed to load rent rolls: '+e.message+'</div>'; });
+  DATA=rolls.map(flatRow);
+  $('#sub').textContent=`${rolls.length} rent roll${rolls.length>1?'s':''} · document-grade NOI → DSCR · local · $0`;
+  // restore saved view + sort prefs
+  const pv=localStorage.getItem('rentView'); if(pv==='grid'||pv==='table'||pv==='cards'){ view=(pv==='grid'?'cards':pv); }
+  const sk=localStorage.getItem('rentSortKey'), sd=localStorage.getItem('rentSortDir');
+  if(sk&&COLS.some(c=>c.k===sk))sortKey=sk; if(sd==='1'||sd==='-1')sortDir=+sd;
+  buildSortSel(); buildColMenu();
+  document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x.dataset.v===view));
+  $('#toolbar').style.display='flex';
+  render();
+}).catch(e=>{ $('#rolls').innerHTML='<div class="glass empty">Failed to load rent rolls: '+esc(e.message)+'</div>'; });
 </script>
   <script src="/col-resize.js" defer></script>
 </body>
diff --git a/public/sales.html b/public/sales.html
index 97ca9fe..1ee19ba 100644
--- a/public/sales.html
+++ b/public/sales.html
@@ -5,87 +5,353 @@
 <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>Closed Sales — LA Condo Comps</title>
-<link rel="stylesheet" href="/crcp-grid.css">
 <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
-<script src="/crcp-grid.js"></script>
 <style>
-  :root{ --bg:#0c0f15; --panel:#141925; --fg:#E6E8EC; --muted:#8A93A2; --gold:#C8A24B; --active:#70AD47; --draft:#5B9BD5; --line:#232a38;
-    /* crcp-grid engine theme mapping (gold/panel) */
-    --cg-line:#232a38; --cg-mut:#8A93A2; --cg-ink:#E6E8EC; --cg-card:#141925;
-    --cg-acc:#C8A24B; --cg-accbg:rgba(200,162,75,.12); --cg-railbg:#0b0e13; --cg-topbg:rgba(12,15,21,.95); --cg-red:#f85149; }
-  *{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;}
-  header{display:flex;align-items:center;gap:14px;padding:14px 22px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:20;}
-  header h1{margin:0;font-size:17px;} header h1 b{color:var(--gold);} header a{color:var(--draft);text-decoration:none;font-size:13px;}
-  header .nav{margin-left:auto;display:flex;gap:14px;}
-  .stat{color:var(--muted);font-size:13px;padding:12px 22px 4px;} .stat b{color:var(--fg);}
-  #depthNote{font-size:12px;color:var(--muted);margin:2px 22px 8px;padding:6px 10px;border-left:2px solid var(--gold);background:rgba(200,162,75,.06);}
-  .charts{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:18px;} @media(max-width:880px){.charts{grid-template-columns:1fr;}}
-  .panel{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:16px;}
+  :root{ --bg:#0c0f15; --card:#141925; --line:#232a38; --ink:#E6E8EC; --mut:#8A93A2; --gold:#C8A24B; --acc:#70AD47; --blue:#58a6ff; --warn:#d29922; }
+  *{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",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(12,15,21,.98);backdrop-filter:blur(8px);flex-wrap:wrap;}
+  header h1{font-size:16px;margin:0;} header h1 b{color:var(--gold);}
+  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(--gold);}
+  .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(--gold);color:#1a1205;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;}
+  .stat{color:var(--mut);font-size:13px;padding:12px 22px 4px;} .stat b{color:var(--ink);}
+  #depthNote{font-size:12px;color:var(--mut);margin:2px 22px 8px;padding:6px 10px;border-left:2px solid var(--gold);background:rgba(200,162,75,.06);}
+  #depthNote:empty{display:none;}
+  .wrap{padding:24px 28px 60px;}
+  .tblwrap{overflow:auto;max-height:calc(100vh - 210px);border:1px solid var(--line);border-radius:10px;}
+  table.g{border-collapse:separate;border-spacing:0;width:100%;font-size:12.5px;white-space:nowrap;}
+  table.g 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.g thead th:hover{color:var(--ink);}
+  table.g thead th.asc::after{content:' ▲';color:var(--gold);} table.g thead th.desc::after{content:' ▼';color:var(--gold);}
+  /* draggable column reorder (Steve rule: move columns around) */
+  table.g thead th.dragcol{cursor:grab;}
+  table.g thead th.dragging{opacity:.45;}
+  table.g thead th.dropL{box-shadow:inset 3px 0 0 var(--gold);}
+  table.g thead th.dropR{box-shadow:inset -3px 0 0 var(--gold);}
+  table.g tbody td{padding:7px 10px;border-bottom:1px solid #20262f;}
+  table.g tbody tr:hover td{background:rgba(200,162,75,.07);}
+  table.g td.n{text-align:right;font-variant-numeric:tabular-nums;}
+  table.g a{color:var(--blue);text-decoration:none;} table.g a:hover{text-decoration:underline;}
+  .price{color:var(--acc);font-weight:600;}
+  .mut{color:var(--mut);} .miss{color:#5A6472;}
+  #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(640px,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:20px;} #modal .msub{color:var(--gold);font-size:13px;}
+  #modal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
+  #modal .sec{margin-top:14px;} #modal .sec h4{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);}
+  #modal .litem{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-bottom:1px solid #20262f;font-size:13px;} #modal .litem .m{color:var(--mut);}
+  #modal .ci{display:flex;gap:16px;flex-wrap:wrap;margin:10px 0;font-size:14px;} #modal .ci a{color:var(--blue);text-decoration:none;}
+  .charts{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:18px 28px;} @media(max-width:880px){.charts{grid-template-columns:1fr;}}
+  .panel{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:16px;}
   .panel h3{margin:0 0 12px;font-size:12px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);}
   canvas{max-height:240px;}
-  .price{color:var(--active);font-weight:600;}
+  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--card-min,280px),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;cursor:pointer;}
+  .gc .nm{font-weight:700;} .gc .fm{color:var(--mut);font-size:12px;margin-bottom:6px;}
+  .gc .row{display:flex;justify-content:space-between;gap:8px;font-size:12.5px;padding:2px 0;border-top:1px solid #20262f;}
+  .gc .row .k{color:var(--mut);}
+  .hide{display:none;}
+  /* ---- Amazon-style left rail ---- */
+  .shell{display:flex;align-items:flex-start;}
+  .rail{flex:0 0 260px;background:#0a0d13;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(--gold);color:var(--gold);background:rgba(200,162,75,.10);}
+  .chip .ct{color:var(--mut);font-size:10px;margin-left:4px;}
+  .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(200,162,75,.08);} .ftog input{accent-color:var(--gold);}
+  .fgrp{font-size:9.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);margin:9px 0 3px;font-weight:700;opacity:.85;} .fgrp:first-child{margin-top:0;}
+  .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(--gold);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; }
 </style>
 </head>
 <body>
 <header>
-  <h1><b>Closed Sales</b> · LA condo comps</h1>
-  <nav class="nav"><a href="/crcp.html">← CRCP</a><a href="/condos.html">Condos</a><a href="/graphics.html">Graphics</a><a href="/fha-loans.html">FHA Loans</a></nav>
+  <h1><b>💲 Closed Sales</b> · LA condo comps</h1>
+  <div class="nav"><a href="/crcp.html">← CRCP</a><a href="/condos.html">Condos</a><a href="/graphics.html">Graphics</a><a href="/fha-loans.html">FHA Loans</a></div>
+  <span class="spacer"></span>
+  <input class="search" id="q" type="text" placeholder="search all fields — address, city, zip, price, beds, sqft… (space = AND)">
+  <div class="toggle" id="view"><button data-v="table" class="on">▦ List</button><button data-v="grid">▤ Grid</button></div>
+  <select id="sortsel" title="Sort sold comps"></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="csv" title="Export current rows to CSV">⬇ CSV</button>
+  <span class="count" id="count">…</span>
 </header>
 <div class="stat" id="stat">loading…</div>
 <div id="depthNote"></div>
-
-<div class="cg-shell">
-  <aside class="cg-rail" id="cgRail"></aside>
-  <div class="cg-main" id="cgMain"><div class="cg-empty" style="padding:50px">loading sold comps…</div></div>
+<div class="shell">
+<aside class="rail">
+  <div class="rsec"><h4>City</h4><div class="chips" id="fCity"></div></div>
+  <div class="rsec"><h4>Sold year</h4><div class="chips" id="fYear"></div></div>
+  <div class="rsec"><h4>Beds</h4><div class="chips" id="fBeds"></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 id="tableView" class="tblwrap"><table class="g" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
+  <div id="gridView" class="grid hide"></div>
+  <div class="charts">
+    <div class="panel"><h3>By city (avg price)</h3><canvas id="cityChart"></canvas></div>
+    <div class="panel"><h3>Sales by year</h3><canvas id="yearChart"></canvas></div>
+  </div>
 </div>
-
-<div class="charts">
-  <div class="panel"><h3>By city (avg price)</h3><canvas id="cityChart"></canvas></div>
-  <div class="panel"><h3>Sales by year</h3><canvas id="yearChart"></canvas></div>
 </div>
-
+<div id="ov"><div id="modal"><button class="x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
 <script>
 const $=s=>document.querySelector(s);
-const money=n=>n?'$'+Number(n).toLocaleString():'—';
-let cityChart, yearChart;
-
-// Every data point — each toggleable (cards + list columns).
-const FIELDS=[
-  {k:'sold_price',l:'Sold',      money:1, col:1, def:1},
-  {k:'sold_date', l:'Sold date',          col:1, def:1, calc:r=>String(r.sold_date||'').slice(0,10)},
-  {k:'beds',      l:'Beds',      num:1,   col:1, def:1},
-  {k:'baths',     l:'Baths',     num:1,   col:1, def:1},
-  {k:'sqft',      l:'Sq Ft',     num:1,   col:1, def:1},
-  {k:'ppsf',      l:'$/Sq Ft',   money:1, col:1, def:1, calc:r=>r.sqft?Math.round(r.sold_price/r.sqft):null},
-  {k:'city',      l:'City',               col:1, def:1},
-  {k:'zip',       l:'ZIP',                col:1, def:0},
-];
-const FACETS=[
-  {k:'city',      l:'City',       type:'chip', max:20},
-  {k:'year',      l:'Sold year',  type:'chip', calc:r=>String(r.sold_date||'').slice(0,4), max:12},
-  {k:'beds',      l:'Beds',       type:'chip', calc:r=>String(r.beds||'—')},
-  {k:'sold_price',l:'Sold price $',type:'range', money:1},
-  {k:'sqft',      l:'Sq Ft',      type:'range'},
-  {k:'ppsf',      l:'$/Sq Ft',    type:'range', money:1, calc:r=>r.sqft?Math.round(r.sold_price/r.sqft):null},
+const esc=s=>(s==null?'':String(s)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+const num=n=>(n==null||n==='')?'—':Number(n).toLocaleString();
+const money=n=>(n==null||n==='')?'—':'$'+Number(n).toLocaleString();
+const url=(v,lbl)=>{ if(!v) return '<span class="miss">—</span>'; const h=/^https?:/.test(v)?v:'https://'+v; return `<a href="${esc(h)}" target="_blank" rel="noopener noreferrer">${lbl}</a>`; };
+const dt=s=>{ if(!s) return '—'; try{ return new Date(s).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }catch(e){ return '—'; } };
+const ppsf=r=>r.sqft?Math.round(r.sold_price/r.sqft):null;
+// Every field carries a group `g` (sort menu optgroups + organized rail toggles).
+// Order below IS the column order. Every field is sortable (raw-value sort, any type).
+const COLS=[
+  // ── Property ──
+  {k:'address',l:'Address',t:'s',g:'Property'},
+  {k:'city',l:'City',t:'s',g:'Property'},
+  {k:'zip',l:'ZIP',t:'s',g:'Property',def:0},
+  // ── Sale ──
+  {k:'sold_price',l:'Sold',t:'money',g:'Sale'},
+  {k:'sold_date',l:'Sold Date',t:'date',g:'Sale',get:r=>String(r.sold_date||'').slice(0,10)},
+  {k:'ppsf',l:'$/Sq Ft',t:'money',g:'Sale',get:ppsf},
+  // ── Unit ──
+  {k:'beds',l:'Beds',t:'n',g:'Unit'},
+  {k:'baths',l:'Baths',t:'n',g:'Unit'},
+  {k:'sqft',l:'Sq Ft',t:'n',g:'Unit'},
+  // ── Details ──
+  {k:'url',l:'Listing',t:'link',g:'Details',def:0},
 ];
-
-async function load(){
-  let d;
-  try{ d=await (await fetch('/api/closed-sales?limit=5000')).json(); }
-  catch(e){ $('#cgMain').innerHTML='<div class="cg-empty" style="padding:50px">sold-comps load error.</div>'; return; }
-  const t=d.total||{};
-  $('#stat').innerHTML=`<b>${(t.n||0).toLocaleString()}</b> sold condos · avg <b>${money(t.avg_price)}</b> · ${(t.oldest||'').toString().slice(0,10)} → ${(t.newest||'').toString().slice(0,10)} · <span style="color:var(--muted)">${d.source||''}</span>`;
-  if(d.depthNote) $('#depthNote').textContent='ℹ '+d.depthNote;
-
-  CrcpGrid.mount({
-    data:d.rows||[], fields:FIELDS, facets:FACETS,
-    mount:'#cgMain', rail:'#cgRail', view:'list', cols:3,
-    storageKey:'sales', titleCol:'Address',
-    title:r=>({ name:r.address||'—', sub:[r.city,r.zip].filter(Boolean).join(' '), href:r.url||'' }),
-    search:r=>((r.address||'')+' '+(r.city||'')+' '+(r.zip||'')),
+const val=(r,c)=>c.get?c.get(r):r[c.k];
+let DATA=[], view='table', sortKey='sold_date', sortDir=-1, q='', lastColSig='';
+const F={city:new Set(),year:new Set(),beds:new Set()};
+// START TIGHT (Steve rule): 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','sold_price','sold_date','beds','baths','sqft']);
+let VISCOL=(function(){let s={};try{s=JSON.parse(localStorage.getItem('salesCols')||'{}');}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('salesColOrder')||'[]');}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('salesCols',JSON.stringify(VISCOL));}catch(e){} }
+function saveColOrder(){ try{localStorage.setItem('salesColOrder',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.g'); 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); }
+function autoCols(){
+  const known=new Set(COLS.map(c=>c.k)); const skip=new Set(['id']);
+  const seen=new Set();
+  DATA.slice(0,60).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', g:'Other', def:0});
+  }));
+}
+function buildRail(){
+  const cc={},yc={},bc={};
+  DATA.forEach(r=>{
+    if(r.city)cc[r.city]=(cc[r.city]||0)+1;
+    const yr=String(r.sold_date||'').slice(0,4); if(yr)yc[yr]=(yc[yr]||0)+1;
+    const bd=(r.beds==null||r.beds==='')?'—':String(r.beds); bc[bd]=(bc[bd]||0)+1;
+  });
+  $('#fCity').innerHTML=Object.entries(cc).sort((a,b)=>b[1]-a[1]).slice(0,20).map(([k,c])=>`<span class="chip" data-ci="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
+  $('#fYear').innerHTML=Object.entries(yc).sort((a,b)=>b[0]<a[0]?-1:1).map(([k,c])=>`<span class="chip" data-yr="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
+  $('#fBeds').innerHTML=Object.entries(bc).sort((a,b)=>b[1]-a[1]).map(([k,c])=>`<span class="chip" data-bd="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
+  buildFieldToggles();
+}
+function buildFieldToggles(){
+  const groups={};
+  COLS.forEach(c=>{ (groups[c.g||'Other']=groups[c.g||'Other']||[]).push(c); });
+  $('#fFields').innerHTML=Object.entries(groups).map(([g,cs])=>
+    `<div class="fgrp">${esc(g)}</div>`+cs.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join('')
+  ).join('');
+}
+function passFacets(r){
+  if(F.city.size && !F.city.has(r.city)) return false;
+  if(F.year.size && !F.year.has(String(r.sold_date||'').slice(0,4))) return false;
+  if(F.beds.size){ const bd=(r.beds==null||r.beds==='')?'—':String(r.beds); if(!F.beds.has(bd)) return false; }
+  return true;
+}
+function cell(r,c){
+  const v=val(r,c);
+  if(c.k==='address') return esc(v==null?'—':v);
+  if(c.t==='money') return v==null||v===''?'<span class="miss">—</span>':`<span class="price">${money(v)}</span>`;
+  if(c.t==='n') return num(v);
+  if(c.t==='date') return dt(v);
+  if(c.t==='link') return url(v,'view↗');
+  return esc(v==null||v===''?'—':v);
+}
+function filtered(){
+  let rows=DATA.filter(passFacets);
+  if(q){
+    // Search ALL data (Steve rule): 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).filter(k=>k!=='__hay').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=val(a,c), y=val(b,c);
+    const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
+    if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
+    if(x<y) return -sortDir; if(x>y) return sortDir; return 0;
   });
-
-  // charts (over the loaded window)
+  return rows;
+}
+function render(){
+  const rows=filtered(); const cols=visCols();
+  $('#count').textContent=`${rows.length} of ${DATA.length} sold comps`;
+  $('#thead').innerHTML='<tr>'+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>';
+  if(view==='table'){
+    $('#tableView').classList.remove('hide'); $('#gridView').classList.add('hide');
+    $('#tbody').innerHTML=rows.map((r,i)=>`<tr class="srow" data-i="${i}" style="cursor:pointer">`+cols.map(c=>{const cls=(c.t==='n'||c.t==='money')?' class="n"':'';return `<td${cls}>${cell(r,c)}</td>`;}).join('')+'</tr>').join('');
+    const csig=cols.map(c=>c.k).join(',');
+    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
+  } else {
+    $('#tableView').classList.add('hide'); $('#gridView').classList.remove('hide');
+    const skipGrid=new Set(['address','city']);
+    const bodyCols=cols.filter(c=>!skipGrid.has(c.k));
+    $('#gridView').innerHTML=rows.map((r,i)=>`<div class="gc srow" data-i="${i}">
+      <div class="nm">${esc(r.address||'—')}</div>
+      <div class="fm">${esc([r.city,r.zip].filter(Boolean).join(' ')||'—')}</div>`
+      +bodyCols.map(c=>`<div class="row"><span class="k">${esc(c.l)}</span><span>${cell(r,c)}</span></div>`).join('')
+    +`</div>`).join('');
+  }
+  // keep the filtered set for the modal (expandable rows)
+  render._rows=rows;
+}
+function openDetail(i){
+  const rows=render._rows||filtered(); const r=rows[i]; if(!r) return;
+  const ov=$('#ov'), mb=$('#mbody'); ov.classList.add('on');
+  const w=r.url?(/^https?:/.test(r.url)?r.url:'https://'+r.url):null;
+  mb.innerHTML='<h3>'+esc(r.address||'—')+'</h3><div class="msub">'+esc([r.city,r.zip].filter(Boolean).join(' ')||'—')+'</div>'
+    +'<div class="ci">'
+    +'<span class="price" style="font-size:18px">'+money(r.sold_price)+'</span>'
+    +'<span class="mut">sold '+dt(r.sold_date)+'</span>'
+    +(w?'<a href="'+esc(w)+'" target="_blank" rel="noopener noreferrer">🌐 listing↗</a>':'')
+    +'</div>'
+    +'<div class="sec"><h4>All fields</h4>'
+    +COLS.map(c=>{ const raw=val(r,c); return '<div class="litem"><span class="m">'+esc(c.l)+'</span><span>'+(c.t==='link'?(w?'<a href="'+esc(w)+'" target="_blank" rel="noopener noreferrer">view↗</a>':'—'):cell(r,c))+'</span></div>'; }).join('')
+    +Object.keys(r).filter(k=>!COLS.some(c=>c.k===k)).map(k=>{ const v=r[k]; if(v==null||typeof v==='object') return ''; return '<div class="litem"><span class="m">'+esc(k)+'</span><span>'+esc(v)+'</span></div>'; }).join('')
+    +'</div>';
+}
+document.addEventListener('click',e=>{ const row=e.target.closest('.srow'); if(row && e.target.tagName!=='A'){ openDetail(+row.dataset.i); } if(e.target.id==='ov') $('#ov').classList.remove('on'); });
+document.addEventListener('keydown',e=>{ if(e.key==='Escape') $('#ov').classList.remove('on'); });
+$('#thead').addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;
+  if(sortKey===k)sortDir*=-1;else{sortKey=k;const c=COLS.find(x=>x.k===k);sortDir=(c&&(c.t==='n'||c.t==='money'||c.t==='date'))?-1:1;}
+  localStorage.setItem('salesSortKey',sortKey);localStorage.setItem('salesSortDir',sortDir);if(window.__syncSortSel)window.__syncSortSel();render();});
+// ── drag-to-reorder columns (Steve rule: move columns around) ──
+let dragKey=null;
+$('#thead').addEventListener('dragstart',e=>{ const th=e.target.closest('th'); if(!th) 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===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) 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; });
+$('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();});
+function exportCSV(){ const rows=filtered(); const cols=visCols(); const cv=(r,c)=>{let v=val(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-closed-sales-'+rows.length+'.csv'; a.click(); URL.revokeObjectURL(a.href); }
+$('#csv').addEventListener('click',exportCSV);
+$('#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('salesView',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'),dz=$('#density');
+  // Grouped sort menu — EVERY field is sortable, organized under its category optgroup.
+  // Rebuildable so auto-discovered columns join the menu once data loads.
+  window.__buildSortSel=()=>{
+    const groups={};
+    COLS.forEach(c=>{ (groups[c.g||'Other']=groups[c.g||'Other']||[]).push(c); });
+    sel.innerHTML=Object.entries(groups).map(([g,cs])=>
+      `<optgroup label="${esc(g)}">`+cs.map(c=>`<option value="${c.k}">${esc(c.l)}</option>`).join('')+`</optgroup>`).join('');
+    if(Array.from(sel.options).some(o=>o.value===sortKey)) sel.value=sortKey;
+  };
+  window.__buildSortSel();
+  const sk=localStorage.getItem('salesSortKey'),sd=localStorage.getItem('salesSortDir');
+  if(sk&&COLS.some(c=>c.k===sk))sortKey=sk; if(sd==='1'||sd==='-1')sortDir=+sd;
+  if(!Array.from(sel.options).some(o=>o.value===sortKey))sortKey='sold_date';
+  sel.value=sortKey;
+  sel.addEventListener('change',()=>{sortKey=sel.value;const c=COLS.find(x=>x.k===sortKey);
+    sortDir=(c&&(c.t==='n'||c.t==='money'||c.t==='date'))?-1:1;
+    localStorage.setItem('salesSortKey',sortKey);localStorage.setItem('salesSortDir',sortDir);render();});
+  const widthFor=v=>Math.round(400-(Math.max(1,Math.min(10,v))-1)*(400-150)/9);
+  const applyDensity=v=>document.documentElement.style.setProperty('--card-min',widthFor(v)+'px');
+  const dv=parseInt(localStorage.getItem('salesDensity'),10),dval=(dv>=1&&dv<=10)?dv:5;
+  dz.value=dval;applyDensity(dval);
+  dz.addEventListener('input',()=>{const v=+dz.value;applyDensity(v);localStorage.setItem('salesDensity',v);});
+  const pv=localStorage.getItem('salesView'); 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';
+  window.__syncSortSel=()=>{if(Array.from(sel.options).some(o=>o.value===sortKey)&&sel.value!==sortKey)sel.value=sortKey;};
+})();
+// left-rail facets + column toggles
+$('#fCity').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.city,c.dataset.ci);c.classList.toggle('active');render();});
+$('#fYear').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.year,c.dataset.yr);c.classList.toggle('active');render();});
+$('#fBeds').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.beds,c.dataset.bd);c.classList.toggle('active');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(); });
+// 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')); }
+// ── charts ──
+let cityChart, yearChart;
+function drawCharts(d){
   const bc=d.byCity||[];
   if(cityChart) cityChart.destroy();
   cityChart=new Chart($('#cityChart'),{type:'bar',data:{labels:bc.map(c=>c.city),datasets:[{label:'avg $',data:bc.map(c=>c.avg_price),backgroundColor:'#C8A24B'}]},options:{plugins:{legend:{display:false}},scales:{x:{ticks:{color:'#8A93A2',font:{size:9}}},y:{ticks:{color:'#8A93A2'}}}}});
@@ -93,7 +359,14 @@ async function load(){
   if(yearChart) yearChart.destroy();
   yearChart=new Chart($('#yearChart'),{type:'line',data:{labels:by.map(y=>y.yr),datasets:[{label:'sales',data:by.map(y=>y.n),borderColor:'#70AD47',backgroundColor:'rgba(112,173,71,.15)',fill:true,tension:.3}]},options:{plugins:{legend:{display:false}},scales:{x:{ticks:{color:'#8A93A2'}},y:{ticks:{color:'#8A93A2'}}}}});
 }
-load();
+// ── load (preserves the /api/closed-sales fetch) ──
+fetch('/api/closed-sales?limit=5000').then(r=>r.json()).then(d=>{
+  DATA=d.rows||[];
+  const t=d.total||{};
+  $('#stat').innerHTML=`<b>${(t.n||0).toLocaleString()}</b> sold condos · avg <b>${money(t.avg_price)}</b> · ${(t.oldest||'').toString().slice(0,10)} → ${(t.newest||'').toString().slice(0,10)} · <span style="color:var(--mut)">${esc(d.source||'')}</span>`;
+  if(d.depthNote) $('#depthNote').textContent='ℹ '+d.depthNote;
+  autoCols(); if(window.__buildSortSel)window.__buildSortSel(); buildRail(); render(); drawCharts(d);
+}).catch(()=>{ $('#count').textContent='failed to load /api/closed-sales'; $('#stat').textContent='sold-comps load error.'; });
 </script>
   <script src="/col-resize.js" defer></script>
 </body>

← 5c7d62c RESELLABLE sale prices from public records: derive-sale-pric  ·  back to Commercialrealestate  ·  auto-save: 2026-07-31T08:55:43 (5 files) — public/brokers.ht 4efa70a →