← back to Commercialrealestate

public/gov-agents.html

380 lines

<!doctype html>
<html lang="en">
<head>
<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='light'?'light':'dark');}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();</script>
<script>/* credential-safe fetch (2026-08-18): resolve EVERY relative fetch (with or without a leading slash) against a credential-free base so creds embedded in the page URL (http://user:pass@host/...) don't poison document.baseURI and throw "Request cannot be constructed from a URL that includes credentials". */(function(){var of=window.fetch,O=location.origin;window.fetch=function(u,o){try{if(typeof u==='string'){if(/^[a-z][a-z0-9+.-]*:/i.test(u)){var x=new URL(u);if(x.username||x.password){x.username='';x.password='';u=x.href;}}else{u=new URL(u,O+location.pathname+location.search).href;}}else if(u&&typeof u.url==='string'){var y=new URL(u.url,O+location.pathname+location.search);if(y.username||y.password){y.username='';y.password='';u=new Request(y.href,u);}}}catch(e){}return of.call(this,u,o);};try{if(location.href.indexOf('@')>=0){history.replaceState(null,'',location.pathname+location.search+location.hash);}}catch(e){}})();</script>
<!-- Self-heal: nav-agent grid controls can persist a "removed"/"collapsed" state that hides the
     whole grid with no obvious way back. Clear it before nav-agent runs so a grid can never stay hidden. -->
<script>(function(){try{for(var i=localStorage.length-1;i>=0;i--){var k=localStorage.key(i);if(k&&k.indexOf('navagent:')===0){var v=localStorage.getItem(k);if(v&&v.indexOf('"removed":true')>=0){localStorage.removeItem(k);}}}}catch(e){}})();</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Government-Licensed Agents — CRCP</title>
<style>
  :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;}
  select,.search{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:8px 12px;font-size:13px;}
  .search{min-width:230px;}
  #density{accent-color:var(--acc);width:100px;}
  label.densl{display:inline-flex;align-items:center;gap:6px;color:var(--mut);font-size:12px;}
  .count{color:var(--mut);font-size:12px;}
  .metros{display:flex;gap:6px;flex-wrap:wrap;padding:10px 18px;border-bottom:1px solid var(--line);}
  .mchip{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:5px 12px;font-size:12.5px;color:var(--mut);cursor:pointer;}
  .mchip:hover{color:var(--ink);} .mchip.on{border-color:var(--gold);color:var(--gold);background:rgba(200,162,75,.1);}
  .mchip .st{color:var(--blue);font-weight:700;} .mchip .ct{color:var(--mut);font-size:10.5px;margin-left:5px;}
  .note{color:var(--mut);font-size:11.5px;padding:8px 18px;border-bottom:1px solid var(--line);}
  .tblwrap{overflow:auto;max-height:calc(100vh - 190px);border-top:1px solid var(--line);}
  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 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);}
  .ftog .flbl{flex:1;}
  .ftog .fdrag{cursor:grab;color:var(--mut);font-size:12px;opacity:.5;user-select:none;line-height:1;} .ftog:hover .fdrag{opacity:1;}
  .ftog.fdragging{opacity:.45;}
  .ftog.fdropT{box-shadow:inset 0 3px 0 var(--gold);} .ftog.fdropB{box-shadow:inset 0 -3px 0 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>
  <style>
  /* grid container + compact List view (added grid/list/table) */
  .hide{display:none!important}
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--card-min,280px),1fr));gap:16px;padding:16px 20px 0;}
  .lst{padding:14px 20px 0;display:flex;flex-direction:column;}
  .lrow{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;padding:10px 4px;border-bottom:1px solid var(--line2,#20262f);cursor:pointer;}
  .lrow:hover{background:rgba(200,162,75,.06);}
  .lnm{font-weight:700;min-width:190px;flex:0 0 auto;color:var(--ink);}
  .lmeta{display:flex;flex-wrap:wrap;gap:5px 16px;flex:1 1 320px;min-width:0;}
  .lm{font-size:12.5px;white-space:nowrap;}
  .lk{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;margin-right:5px;}
  /* mobile: ⚙ Filters button + rail modal (rail is display:none <820px) */
  .railbtn{display:none;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:8px 12px;font-size:13px;cursor:pointer;}
  .railbtn:hover{border-color:var(--gold);}
  .railov{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:70;display:none;align-items:flex-start;justify-content:center;padding:14px;}
  .railov.on{display:flex;}
  .railmd{width:100%;max-width:520px;max-height:90vh;display:flex;flex-direction:column;background:var(--card);border:1px solid var(--line);border-radius:15px;overflow:hidden;}
  .railmd-h{display:flex;align-items:center;justify-content:space-between;padding:13px 16px;border-bottom:1px solid var(--line);font-size:15px;color:var(--ink);}
  .railmd-x{background:none;border:0;color:var(--mut);font-size:20px;cursor:pointer;line-height:1;}
  .railmd-b{overflow:auto;padding:0;}
  .rail.in-modal{display:block!important;position:static!important;height:auto!important;width:auto!important;flex:none!important;top:auto!important;border:0!important;}
  @media(max-width:820px){ .railbtn{display:inline-block;} }
  </style>
  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
  <link rel="stylesheet" href="/crcp-theme.css"><!-- CRCP day/night tokens: load LAST so html[data-theme] wins -->
</head>
<body>
<header>
  <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 all fields — name, firm, license #, city, county, zip, expiry… (space = AND)">
  <button class="railbtn" id="railBtn" type="button">⚙ Filters</button>
  <div class="toggle" id="view"><button data-v="grid">▦ Grid</button><button data-v="list">☰ List</button><button data-v="table" class="on">▤ Table</button></div>
  <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="shell">
<aside class="rail">
  <div class="rsec" id="fieldsSec"><h4>Fields / columns — show / hide · reorder</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 row by its ⠿ handle to reorder columns. Drag a column header to reorder · its right edge to resize · click to sort.</div>
  </div>
</aside>
<div class="rwrap">
  <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 id="listView" class="lst hide"></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;');
// 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='', view=(localStorage.getItem('govView')||'table');
// 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,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);
}
// Column show/hide + in-panel search & drag-reorder via the shared column-manager.js
// drop-in (TK-10120), as a VIEW over this page's own VISCOL/COLORDER state.
let __cm=null;
function buildFieldToggles(){
  // Always use the native panel (draggable rows in COLORDER order) — ColumnManager is
  // used only for its onChange hook when present; the panel DOM is always ours.
  syncColOrder();
  const ordered=COLORDER.map(k=>COLS.find(c=>c.k===k)).filter(Boolean);
  $('#fFields').innerHTML=ordered.map(c=>`<label class="ftog" draggable="true" data-fk="${c.k}"><span class="fdrag" title="Drag to reorder column">⠿</span><input type="checkbox" data-fk="${c.k}" ${colVis(c.k)?'checked':''}><span class="flbl">${esc(c.l)}</span></label>`).join('');
  if(window.ColumnManager && __cm){ __cm.setCols(COLS.map(c=>({k:c.k,l:c.l,g:c.g||'Other'}))); }
  else if(window.ColumnManager && !__cm){
    __cm=ColumnManager.init({ mount:'#fFields', footer:false, hint:false, cols:COLS.map(c=>({k:c.k,l:c.l,g:c.g||'Other'})),
      state:{ visible:k=>colVis(k), setVisible:(k,v)=>{VISCOL[k]=v;saveCols();},
              order:()=>{syncColOrder();return COLORDER.slice();}, setOrder:a=>{COLORDER=a;saveColOrder();},
              reset:()=>{VISCOL={};saveCols();COLORDER=[];saveColOrder();} },
      onChange:()=>{ buildFieldToggles(); render(); } });
  }
}
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;
  });
  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>';
  const tv=$('#tableView'),gv=$('#gridView'),lv=$('#listView');
  if(tv) tv.classList.toggle('hide',view!=='table');
  if(gv) gv.classList.toggle('hide',view!=='grid');
  if(lv) lv.classList.toggle('hide',view!=='list');
  if(view==='table'){
    $('#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>`;
    const csig=cols.map(c=>c.k).join(',');
    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
    else if(window.ColResize && window.ColResize.reattach){ window.ColResize.reattach(document.querySelector('.tblwrap table.g')); } // resize handles survive sort/search re-render (TK-10089)
  } else if(view==='list' && lv){
    lv.innerHTML=rows.map((a,i)=>`<div class="lrow" data-i="${i}" style="cursor:pointer"><div class="lnm">${esc(a.name||'—')}</div><div class="lmeta">`+cols.filter(c=>c.k!=='name').map(c=>`<span class="lm"><span class="lk">${esc(c.l)}</span>${cell(a,c)}</span>`).join('')+`</div></div>`).join('')||'<div class="miss" style="padding:20px">no matches</div>';
  } else if(gv){
    gv.innerHTML=rows.map((a,i)=>`<div class="gc" data-i="${i}" style="cursor:pointer"><div class="nm">${esc(a.name||'—')}</div>${a.firm?`<div class="fm">${esc(a.firm)}</div>`:''}`+cols.filter(c=>c.k!=='name').map(c=>`<div class="row"><span class="k">${esc(c.l)}</span><span>${cell(a,c)}</span></div>`).join('')+`</div>`).join('')||'<div class="miss" style="padding:20px">no matches</div>';
  }
  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>`:'');
}
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 el=e.target.closest('#tbody tr,#gridView .gc,#listView .lrow'); if(el && el.dataset.i!=null && e.target.tagName!=='A'){ const a=ROWS[+el.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; 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; }
  const ms=d.metros||[];
  $('#metros').innerHTML=ms.map(m=>`<span class="mchip" data-m="${esc(m.metro)}"><span class="st">${esc(m.state)}</span> ${esc(m.metro)}<span class="ct">${m.n.toLocaleString()} · ${m.brokers.toLocaleString()} br</span></span>`).join('');
  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();});
// 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();});
// grid/list/table view toggle
document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x.dataset.v===view));
$('#view')&&$('#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('govView',view);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(); if(__cm)__cm.refresh();
    }
  }
  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 ──
$('#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(); });
// ── panel checkbox toggle (data-fk emitted by our buildFieldToggles) ──
$('#fFields').addEventListener('change',e=>{ const cb=e.target.closest('input[data-fk]'); if(!cb) return; VISCOL[cb.dataset.fk]=cb.checked; saveCols(); render(); });
// ── panel drag-to-reorder columns (Steve rule: resort order here in the panel too) ──
let pDragKey=null;
$('#fFields').addEventListener('dragstart',e=>{ const row=e.target.closest('.ftog'); if(!row) return;
  if(!e.target.closest('.fdrag')){ e.preventDefault(); return; }   // only the ⠿ handle starts a move (so checkbox clicks still work)
  pDragKey=row.dataset.fk; e.dataTransfer.effectAllowed='move'; try{e.dataTransfer.setData('text/plain',pDragKey);}catch(_){}
  row.classList.add('fdragging'); });
$('#fFields').addEventListener('dragover',e=>{ if(!pDragKey) return; const row=e.target.closest('.ftog'); if(!row||row.dataset.fk===pDragKey) return;
  e.preventDefault(); e.dataTransfer.dropEffect='move';
  const r=row.getBoundingClientRect(), after=(e.clientY-r.top)>r.height/2;
  $('#fFields').querySelectorAll('.ftog').forEach(x=>x.classList.remove('fdropT','fdropB'));
  row.classList.toggle('fdropB',after); row.classList.toggle('fdropT',!after); });
$('#fFields').addEventListener('dragleave',e=>{ const row=e.target.closest('.ftog'); if(row) row.classList.remove('fdropT','fdropB'); });
$('#fFields').addEventListener('drop',e=>{ if(!pDragKey) return; const row=e.target.closest('.ftog'); if(!row){ pDragKey=null; return; } e.preventDefault();
  const tgt=row.dataset.fk;
  if(tgt!==pDragKey){
    const r=row.getBoundingClientRect(), after=(e.clientY-r.top)>r.height/2;
    syncColOrder(); const from=COLORDER.indexOf(pDragKey);
    if(from>=0){ COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,pDragKey);
      saveColOrder(); lastColSig=''; buildFieldToggles(); render(); }
  }
  pDragKey=null; });
$('#fFields').addEventListener('dragend',()=>{ $('#fFields').querySelectorAll('.fdragging,.fdropT,.fdropB').forEach(x=>x.classList.remove('fdragging','fdropT','fdropB')); pDragKey=null; });
// collapsible rail tabs — all collapsed on load except fieldsSec (Steve rule: fields section stays open)
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=>{ if(s.id!=='fieldsSec') s.classList.add('collapsed'); }); }
buildSortSel(); buildFieldToggles();
loadIndex();
</script>
  <script src="/column-manager.js" defer></script>
  <script src="/col-resize.js" defer></script>
<script src="/corner-nav.js" defer></script>
  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
  <script src="/crcp-theme.js" defer></script><!-- CRCP day/night toggle (auto-injects into header) -->
  <script>
  /* mobile: relocate the filter rail into a ⚙ Filters modal (<820px), back to the sidebar on desktop */
  (function(){
    function boot(){
      var rail=document.querySelector('.rail'), hdr=document.querySelector('header'), btn=document.getElementById('railBtn');
      if(!rail||!hdr||!btn||document.getElementById('railOv')) return;
      var home=document.createComment('gov-rail-home'); rail.parentNode.insertBefore(home,rail);
      var ov=document.createElement('div'); ov.id='railOv'; ov.className='railov';
      var md=document.createElement('div'); md.className='railmd';
      var hd=document.createElement('div'); hd.className='railmd-h'; hd.innerHTML='<span>Filters</span><button type="button" class="railmd-x" aria-label="Close">✕</button>';
      var bd=document.createElement('div'); bd.className='railmd-b';
      md.appendChild(hd); md.appendChild(bd); ov.appendChild(md); document.body.appendChild(ov);
      var open=function(){ov.classList.add('on');}, close=function(){ov.classList.remove('on');};
      btn.addEventListener('click',open);
      hd.querySelector('.railmd-x').addEventListener('click',close);
      ov.addEventListener('click',function(e){ if(e.target===ov) close(); });
      document.addEventListener('keydown',function(e){ if(e.key==='Escape') close(); });
      var mq=window.matchMedia('(max-width: 820px)');
      function place(){ if(mq.matches){ if(rail.parentNode!==bd) bd.appendChild(rail); rail.classList.add('in-modal'); } else { if(home.parentNode&&rail.parentNode!==home.parentNode) home.parentNode.insertBefore(rail,home); rail.classList.remove('in-modal'); close(); } }
      if(mq.addEventListener) mq.addEventListener('change',place); else if(mq.addListener) mq.addListener(place);
      place();
    }
    if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',boot); else boot();
  })();
  </script>
</body>
</html>