← back to Commercialrealestate

public/condos.html

965 lines

<!doctype html>
<html lang="en">
<head>
<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>
<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='dark'?'dark':'light');}catch(e){document.documentElement.setAttribute('data-theme','light');}})();</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">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230e1116'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%233fb950'%3E%F0%9F%8F%A2%3C/text%3E%3C/svg%3E">
<title>LA Condos — Warrantability (FHA-approval proxy)</title>
<style>
  /* ── Theme tokens (dark = default, higher contrast than the old palette;
        light = a real day mode). Toggle sets html[data-theme]. ── */
  :root, :root[data-theme="dark"]{
    --bg:#0d1117; --bg2:#171d26; --card:#1c232e; --line:#33404f; --line2:#3f4c5c;
    --ink:#f2f6fb; --mut:#b3c0cf; --headbg:rgba(13,17,23,.98); --stripe:rgba(255,255,255,.025);
    --hover:rgba(88,166,255,.10);
    --acc:#4bd268; --blue:#79b8ff; --gold:#e3b341; --red:#ff7b72; --purp:#c8a2ff;
  }
  :root[data-theme="light"]{
    --bg:#f4f6f9; --bg2:#ffffff; --card:#ffffff; --line:#d5dbe2; --line2:#c2cad4;
    --ink:#12171e; --mut:#525f6e; --headbg:rgba(255,255,255,.98); --stripe:rgba(20,30,45,.028);
    --hover:rgba(9,105,218,.08);
    --acc:#1a7f37; --blue:#0969da; --gold:#9a6700; --red:#cf222e; --purp:#8250df;
  }
  *{box-sizing:border-box;} html,body{margin:0;}
  body{background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}
  header{display:flex;align-items:center;gap:16px;padding:14px 22px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--headbg);backdrop-filter:blur(8px);z-index:30;}
  header h1{font-size:18px;margin:0;} header a{color:var(--blue);text-decoration:none;font-size:13px;}
  header .nav{margin-left:auto;display:flex;gap:18px;}
  .banner{font-size:12px;color:var(--gold);padding:9px 22px;background:rgba(210,153,34,.08);border-bottom:1px solid rgba(210,153,34,.25);}
  input[type=text],input[type=number]{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 11px;font-size:13px;}
  input[type=range]{accent-color:var(--blue);}
  .count{font-size:12px;color:var(--mut);margin-left:auto;} .count b{color:var(--ink);}
  /* ---- Amazon-style left rail ---- */
  .shell{display:flex;align-items:flex-start;}
  .rail{flex:0 0 262px;background:var(--bg2);border-right:1px solid var(--line);align-self:stretch;position:sticky;top:53px;height:calc(100vh - 53px);overflow-y:auto;padding:0 0 40px;}
  .rail .rsec{padding:13px 16px;border-bottom:1px solid var(--line);}
  .rail h4{margin:0 0 9px;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--mut);}
  .chips{display:flex;flex-wrap:wrap;gap:6px;}
  .chip{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:16px;padding:4px 10px;font-size:12px;cursor:pointer;user-select:none;}
  .chip:hover{color:var(--ink);} .chip.active{border-color:var(--blue);color:var(--blue);background:rgba(88,166,255,.08);}
  .chip.warr-unwarrantable.active{border-color:var(--red);color:var(--red);background:rgba(248,81,73,.10);}
  .chip.warr-expiring90.active,.chip.warr-expiring180.active,.chip.warr-expiring365.active{border-color:var(--gold);color:var(--gold);background:rgba(210,153,34,.10);}
  .chip.warr-lapsed180.active{border-color:var(--red);color:var(--red);background:rgba(248,81,73,.10);}
  .chip .ct{color:var(--mut);font-size:10px;margin-left:4px;}
  .row2{display:flex;gap:8px;} .fld{flex:1;} .fld label{display:block;font-size:10px;color:var(--mut);margin-bottom:3px;}
  .rail input[type=number]{width:100%;}
  .fieldtoggles{display:flex;flex-direction:column;gap:1px;max-height:320px;overflow-y:auto;}
  .ftog{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink);cursor:pointer;padding:3px 4px;border-radius:6px;} .ftog:hover{background:rgba(88,166,255,.06);} .ftog input{accent-color:var(--blue);}
  .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);}
  .minibtn{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:7px;padding:4px 9px;font-size:11px;cursor:pointer;margin-right:5px;} .minibtn:hover{border-color:var(--blue);color:var(--ink);}
  main{flex:1 1 auto;min-width:0;padding:0 0 40px;}
  .topbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:14px 24px;border-bottom:1px solid var(--line);position:sticky;top:53px;background:var(--headbg);backdrop-filter:blur(6px);z-index:20;}
  .topbar input[type=text]{min-width:260px;}
  .topbar .dens{font-size:12px;color:var(--mut);display:flex;align-items:center;gap:7px;}
  .viewseg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;}
  .viewseg button{background:var(--card);color:var(--mut);border:0;border-right:1px solid var(--line);padding:7px 12px;font-size:12px;cursor:pointer;}
  .viewseg button:last-child{border-right:0;} .viewseg button.active{background:#1f6feb;color:#fff;}
  .gridwrap{padding:24px;}
  .grid{display:grid;gap:22px;grid-template-columns:repeat(var(--cols,3),minmax(0,1fr));}
  .drill{color:var(--blue,#4aa3ff);cursor:pointer;text-decoration:underline dotted;text-underline-offset:2px;}
  .drill:hover{text-decoration:underline;}
  .grid.listhost{display:block;}
  .condo{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:6px;}
  .condo .name{font-weight:600;font-size:14px;}
  .condo .addr{font-size:12px;color:var(--mut);}
  .row{display:flex;justify-content:space-between;font-size:12px;gap:10px;}
  .row .k{color:var(--mut);}
  .badge{display:inline-block;padding:2px 9px;border-radius:11px;font-size:11px;font-weight:700;width:fit-content;}
  .badge.fha_approved{background:rgba(63,185,80,.16);color:var(--acc);}
  .badge.fha_expired{background:rgba(210,153,34,.16);color:var(--gold);}
  .badge.heuristic_flag{background:rgba(248,81,73,.16);color:var(--red);}
  .badge.not_listed{background:rgba(139,148,158,.16);color:var(--mut);}
  .src{font-size:11px;color:var(--mut);border-top:1px solid var(--line);padding-top:7px;margin-top:2px;}
  .broker{font-size:11px;color:var(--purp);} .broker a{color:var(--blue);text-decoration:none;} .broker a:hover{text-decoration:underline;}
  .notewrap{margin-top:8px;position:relative;}
  .notebox{width:100%;box-sizing:border-box;background:var(--bg2);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 9px;font-size:12px;font-family:inherit;resize:vertical;min-height:34px;}
  .notebox:focus{outline:none;border-color:var(--gold);}
  .notestatus{position:absolute;right:8px;bottom:6px;font-size:10px;color:var(--acc);pointer-events:none;}
  .empty{padding:50px;text-align:center;color:var(--mut);}
  .listwrap{overflow-x:auto;border:1px solid var(--line);border-radius:12px;}
  /* readability: zebra striping + clearer separators + row hover (both themes) */
  .listtbl tbody tr:nth-child(even) td{background:var(--stripe);}
  .listtbl tbody tr:hover td{background:var(--hover);}
  .listtbl td.stick,.listtbl tbody tr:nth-child(even) td.stick,.listtbl tbody tr:hover td.stick{background:var(--card);}
  .listtbl .lc{color:var(--mut);}
  /* day/night toggle button */
  #themeToggle{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:6px 11px;font-size:13px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;}
  #themeToggle:hover{border-color:var(--blue);}
  .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:var(--bg2);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:var(--bg2);}
  .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; }
  @media (prefers-reduced-motion: reduce){ *,*::before,*::after { transition-duration:.001ms !important; animation-duration:.001ms !important; } }
  .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; }
  .starbtn { background:none; border:0; color:var(--mut); font-size:16px; line-height:1; cursor:pointer; padding:0; }
  .starbtn:hover, .starbtn.on { color:var(--gold); }
  .condo .chead { display:flex; justify-content:space-between; gap:8px; align-items:flex-start; }
  #shortbtn { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:8px; padding:7px 12px; font-size:12px; cursor:pointer; }
  #shortbtn.shorton { border-color:var(--gold); color:var(--gold); }
  .statsbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:9px 24px; border-bottom:1px solid var(--line); }
  .statsbar:empty { display:none; }
  .statpill { font-size:12px; color:var(--ink); background:var(--card); border:1px solid var(--line); border-radius:8px; padding:4px 10px; }
  .statpill .sl { color:var(--mut); }
  /* 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 .litem a.dl,#modal .maddr a.dl{color:var(--blue);text-decoration:none;} #modal .litem a.dl:hover,#modal .maddr a.dl:hover{text-decoration:underline;}
  #modal .src{font-size:12px;color:var(--mut);margin-top:12px;border-top:1px solid var(--line);padding-top:10px;}
</style>
  <link rel="stylesheet" href="/three-view.css">
  <script src="/three-view.js"></script>
  <script src="/infinite-scroll.js"></script><!-- windowed infinite scroll for grid/table -->
  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
  <style>
  /* Top-left overlap fix (Steve 2026-08-22): the corner-nav site menu (☰) and the nav-agent
     field-panel toggle (▤) both sat at top:10px/left:10px and covered each other. Make the site
     menu icon-only and set the field toggle side-by-side to its right — two clean icons, no overlap. */
  .cnav-burger.left .lbl{display:none;}          /* ☰ becomes an icon, no "Menu" label */
  .cnav-burger.left{padding:8px 10px;}           /* tighten to a square-ish icon button */
  .na-side-toggle{left:56px !important;}         /* ▤ sits right of the ☰, not on top of it */
  </style>
</head>
<body>
<header>
  <h1>🏢 LA Condos — Warrantability</h1>
  <div class="nav"><a href="/">← properties</a><a href="/mls.html">Listings</a><a href="/graphics.html">graphics</a><a href="/fha-loans.html">FHA loans</a>
    <button id="themeToggle" title="Toggle day / night" aria-label="Toggle day / night theme"><span id="themeIcon">☀️</span> <span id="themeLbl">Day</span></button></div>
</header>
<script>
// Day/Night toggle — pre-applied before first paint (see head snippet); persists to localStorage.
(function(){
  var root=document.documentElement;
  // apply() only paints — it does NOT persist (passive loads must not lock in a theme;
  // only an explicit toggle click below saves a pref, so everyone gets the Day default).
  function apply(t){ root.setAttribute('data-theme',t);
    var ic=document.getElementById('themeIcon'), lb=document.getElementById('themeLbl');
    // label shows the CURRENT mode (not the target) so it's never ambiguous
    if(ic) ic.textContent = t==='light'?'☀️':'🌙';
    if(lb) lb.textContent = t==='light'?'Day':'Night';
  }
  var cur; try{ cur=localStorage.getItem('crcp-theme'); }catch(e){}
  apply(cur==='dark'?'dark':'light');
  var btn=document.getElementById('themeToggle');
  if(btn) btn.addEventListener('click',function(){ var nt=root.getAttribute('data-theme')==='light'?'dark':'light'; apply(nt); try{ localStorage.setItem('crcp-theme',nt); }catch(e){} });
})();
</script>
<div class="banner" id="banner">⚖️ Loading warrantability label…</div>

<!-- Non-Warrantable Condos section (Steve 2026-08-19): a dedicated, honest FHA/warrantability
     summary above the grid. Populated from the live classified inventory (DATA) by renderNonWarrant(). -->
<style>
  .nw-sec{margin:14px 22px 0;border:1px solid var(--line);border-radius:14px;background:var(--card);padding:16px 18px}
  .nw-sec h2{margin:0 0 4px;font-size:16px;color:var(--ink);display:flex;align-items:center;gap:8px}
  .nw-sec .lede{color:var(--mut);font-size:12.5px;line-height:1.55;margin:0 0 12px;max-width:900px}
  .nw-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-bottom:12px}
  .nw-tile{border:1px solid var(--line);border-radius:11px;background:var(--bg2);padding:11px 13px;cursor:pointer;transition:border-color .12s}
  .nw-tile:hover{border-color:var(--blue)}
  .nw-tile .n{font-size:22px;font-weight:800;line-height:1.1}
  .nw-tile .lbl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;margin-top:2px}
  .nw-tile .sub{font-size:11px;color:var(--mut);margin-top:4px;line-height:1.4}
  .nw-tile.approved .n{color:var(--acc)} .nw-tile.expired .n{color:var(--gold)}
  .nw-tile.flagged .n{color:var(--red)} .nw-tile.notlisted .n{color:var(--mut)}
  .nw-cta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:6px}
  .nw-btn{background:var(--red);color:#fff;border:0;border-radius:9px;padding:9px 16px;font-size:12.5px;font-weight:700;cursor:pointer}
  .nw-btn.ghost{background:transparent;color:var(--ink);border:1px solid var(--line)}
  .nw-note{font-size:11px;color:var(--mut)}
  .nw-ex{margin-top:12px;border-top:1px solid var(--line);padding-top:10px}
  .nw-ex h3{margin:0 0 7px;font-size:12px;color:var(--mut);text-transform:uppercase;letter-spacing:.4px}
  .nw-row{display:flex;flex-wrap:wrap;gap:4px 12px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.04);font-size:12.5px}
  .nw-row .a{font-weight:600;color:var(--ink)} .nw-row .why{color:var(--gold);font-size:11.5px}
  .nw-row .why.nl{color:var(--mut)}
</style>
<section id="nwSection" class="nw-sec" hidden></section>

<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="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">Drag a row by its ⠿ handle to reorder columns. In List view you can also drag a column header to reorder · its right edge to resize · click to sort.</div>
  </div>
  <div class="rsec"><h4>Warrantability</h4><div class="chips" id="fWarr"></div></div>
  <div class="rsec" id="contactSec" style="display:none"><h4>Outreach</h4><div class="chips" id="fContact"></div></div>
  <div class="rsec"><h4>City</h4><div class="chips" id="fCity"></div></div>
  <div class="rsec" id="firmSec"><h4>Listing firm</h4><div class="chips" id="fFirm"></div></div>
  <div class="rsec" id="bedSec"><h4>Beds (min)</h4><div class="chips" id="fBeds"></div></div>
  <div class="rsec" id="bathSec"><h4>Baths (min)</h4><div class="chips" id="fBaths"></div></div>
  <div class="rsec"><h4>Price</h4><div class="row2"><div class="fld"><label>Min $</label><input type="number" id="pMin" step="50000" placeholder="any"></div><div class="fld"><label>Max $</label><input type="number" id="pMax" step="50000" placeholder="any"></div></div></div>
  <div class="rsec"><h4>HOA / mo</h4><div class="row2"><div class="fld"><label>Min $</label><input type="number" id="hoaMin" step="50" placeholder="any"></div><div class="fld"><label>Max $</label><input type="number" id="hoaMax" step="50" 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="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>
</aside>
<main>
  <div class="topbar">
    <input type="text" id="q" placeholder="search all fields — project, city, zip, address, broker, firm, status… (space = AND)">
    <span class="viewseg tv-seg" id="viewseg"></span>
    <button id="shortbtn" title="Show only your shortlisted (starred) listings — shared with the Explorer & MLS">⭐ Shortlist (0)</button>
    <button id="exportbtn" title="Download the current filtered + sorted set as a CSV">⬇ Export CSV</button>
    <button id="draftbtn" title="Queue DRAFT outreach emails for the current FHA-matched set in the Solicitation Studio — nothing sends">🎯 Draft outreach</button>
    <button id="pageReset" title="Reset all filters, search & sort — back to the full list">↺ Reset</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">
      <option value="">Newest</option>
      <option value="price_desc">Price ↓</option>
      <option value="price_asc">Price ↑</option>
      <option value="pps_desc">$/SF ↓</option>
      <option value="year_desc">Year ↓</option>
      <option value="hoa_asc">HOA ↑</option>
      <option value="proj_az">Project A→Z</option>
      <option value="city_az">City A→Z</option>
      <option value="expiry_soon">FHA expires ↑ (soonest)</option>
    </select>
    <label class="dens">Density <input type="range" id="cols" min="1" max="20" step="1" value="3"></label>
    <span class="count" id="count">…</span>
  </div>
  <div class="statsbar" id="statsbar"></div>
  <div class="gridwrap"><div class="grid" id="grid"><div class="empty">loading…</div></div><div id="condoList"></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);
const $$=s=>Array.from(document.querySelectorAll(s));
const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
const PROXY='FHA/VA-approval-based proxy, NOT lender-verified Fannie/Freddie warrantability.';
const NOW_YEAR=new Date().getFullYear();
// v2 3-view migration (TK-10526): the old bespoke 'list' WAS the dense spreadsheet.
// The new model is grid(cards) / list(compact) / table(dense). Re-point a stored
// legacy 'list' → 'table' exactly once so returning users keep the dense view they had.
(function(){ try{ if(localStorage.getItem('condoView')==='list' && !localStorage.getItem('condoView3')){ localStorage.setItem('condoView','table'); } localStorage.setItem('condoView3','1'); }catch(e){} })();
let DATA=[], CURRENT=[], MODE='db', VIEW=localStorage.getItem('condoView')||'grid', bannerDefault='', TV=null;
let SORTKEY=localStorage.getItem('condoSort')||'';   // hoisted so early apply() during boot never hits TDZ

function statusBadge(s){
  const txt={ fha_approved:'FHA-APPROVED · proxy warrantable', fha_expired:'FHA-EXPIRED · re-cert', heuristic_flag:'FLAGGED · review', not_listed:'NOT LISTED · verify' }[s]||s;
  return `<span class="badge ${s}">${txt}</span>`;
}

// ---- every data point (incl. computed) — each toggleable, drives cards + list columns ----
// Order below is the DEFAULT column order (draggable-reorderable, persisted).
const FIELDS=[
  {k:'project_name',l:'Project',               col:1},
  {k:'warrantable_status', l:'Warrantability', badge:1, col:1},
  // FHA-approval expiration (MM/DD/YYYY) — the date warrantability lapses / lapsed. Surfaced by the
  // server from warrant_source (listings) or expiration_date (FHA directory). Plain string, not date:1
  // (the value is date-only, so the datetime formatter would tack on a bogus "12:00 AM").
  {k:'fha_expiration_date', l:'FHA expires', col:1},
  // Days until FHA approval expires, computed live from fha_expiration_date against today (so it's
  // never stale). Negative = already lapsed N days ago. Rendered as a color-coded badge in fcell
  // (red = expired, gold = ≤90d, green = further out). calc returns a Number so List-sort is numeric.
  {k:'days_to_expiry', l:'Days to expiry', col:1, calc:c=>daysToExp(c)},
  {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},
  // ── Broker / agent CONTACT block (Steve 2026-08-05, TK-10243): every listing carries the full
  // listing-side contact set + the sold-listing buyer side. All backed by /api/condos overlay data
  // (data/condo-brokers.json) — no dead toggles. Email fills from the find-email enrichment; the
  // buyer's agent fills only on SOLD listings from the boughtWith scrape. ──
  {k:'broker_name', l:'Listing agent',         col:1},
  {k:'firm_name',   l:'Firm',                  col:1},
  {k:'agent_phone', l:'Agent phone',           col:1},
  {k:'office_phone',l:'Office phone',          col:1},
  {k:'agent_email', l:'Agent email',           col:1},
  {k:'broker_dre',  l:'DRE#',                  col:1},
  {k:'responsible_broker', l:'Responsible broker', col:1},
  {k:'buyer_agent', l:'Buyer’s agent (sold)', 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();
}
// ── one canonical date+time formatter for EVERY surface — card chip, list cell, and
// detail modal all route through this (Steve HARD rule: created timestamps render date
// AND time, full ISO in title=). Guards Invalid Date / null → '' so callers hide cleanly. ──
function fmtDateTime(v){ if(v==null||v==='') return ''; const d=new Date(v); if(isNaN(d)) return ''; return d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
function isoOf(v){ const d=new Date(v); return isNaN(d)?'':d.toISOString(); }
function dateTimeSpan(v){ const s=fmtDateTime(v); return s?`<span title="${esc(isoOf(v))}">${esc(s)}</span>`:''; }
// ── drill-down: no datum is dead text (Steve HARD rule). A city/firm value on a card or
// list cell is a clickable link that filters the grid to that value — reusing the existing
// F.cities / F.firms facet state so the rail chip lights up too. ──
function drillSpan(kind,val){ if(val==null||val==='') return ''; return `<span class="drill" data-drill="${esc(kind)}" data-val="${esc(val)}" title="Filter to ${esc(val)}">${esc(val)}</span>`; }
function applyDrill(kind,val){ if(val==null||val==='') return;
  if(kind==='city'){ F.cities.add(val); } else if(kind==='firm'){ F.firms.add(val); } else return;
  buildRail(); apply();
}
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||'—');
  // broker_dre resolves BEFORE the null-guard (Cody gate): a listing may carry the license in
  // agents[0].dre while the flat broker_dre is empty — the table must match the card, not show '—'.
  if(f.k==='broker_dre'){ const primary=(v!=null&&v!=='')?v:(c.agents&&c.agents[0]&&c.agents[0].dre);
    if(!primary) return '—';
    const extra=(Array.isArray(c.agents)&&c.agents.length>1)?` <span style="color:var(--mut);font-size:10px" title="${c.agents.length-1} co-agent license(s) — see card/detail">+${c.agents.length-1}</span>`:'';
    if(!/^\d{6,8}$/.test(String(primary))) return esc(String(primary))+extra;   // defensive: only link a real DRE#
    const u=c.dre_url||('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+primary);
    const pn=c.broker_name||(c.agents&&c.agents[0]&&c.agents[0].name)||'';
    const prof=` <a href="/broker.html?dre=${encodeURIComponent(String(primary).replace(/\D/g,''))}${pn?'&name='+encodeURIComponent(pn):''}" title="CRCP broker profile (if in our commercial index)" style="text-decoration:none">👤</a>`;
    return `<a href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">${esc(primary)} ↗</a>${prof}${extra}`; }
  // ── contact-block cells (TK-10243) — resolve nested/flat, phone→tel, email→mailto, sortable text ──
  if(f.k==='agent_phone'){ const p=v||(c.agents&&c.agents[0]&&c.agents[0].phone); return p?`<a href="${telHref(p)}">${esc(p)}</a>`:'—'; }
  if(f.k==='office_phone'){ return v?`<a href="${telHref(v)}">${esc(v)}</a>`:'—'; }
  if(f.k==='agent_email'){ const e=agentEmail(c); return e?`<a href="${mailHref(e)}" title="Email the listing agent">${esc(e)} ✉</a>`:'—'; }
  if(f.k==='responsible_broker'){ const rb=respBroker(c); return rb?esc(rb):'—'; }
  if(f.k==='buyer_agent'){ const ba=buyerAgents(c); if(!ba.length) return '—';
    return ba.map(a=>{ const dre=a.dre?` · DRE# ${esc(a.dre)}`:''; return esc(a.name||'')+(a.firm?' · '+esc(a.firm):'')+dre; }).join('<br>'); }
  // days-to-expiry: color-coded badge (red=lapsed, gold=≤90d, green=further out). Its own null-guard
  // so a listing with no matched FHA cert reads '—' rather than a bogus 0.
  if(f.k==='days_to_expiry'){ if(v==null||v==='') return '—'; const d=+v; const dt=esc(c.fha_expiration_date||'');
    const col=d<0?'var(--red)':(d<=90?'var(--gold)':'var(--acc)');
    const txt=d<0?(-d).toLocaleString()+'d ago':'in '+d.toLocaleString()+'d';
    return `<span title="FHA ${d<0?'expired':'expires'} ${dt}" style="color:${col};font-weight:600;white-space:nowrap">${txt}</span>`; }
  if(v==null||v==='') return '—';
  if(f.money) return '$'+(+v).toLocaleString()+(f.suf||'');
  if(f.num) return (+v).toLocaleString();
  if(f.date){ return dateTimeSpan(v)||'—'; }
  if(f.k==='firm_name') return drillSpan('firm',v);
  if(f.k==='city') return drillSpan('city',v);
  return esc(v)+(f.suf||'');
}

// ---- facet state ----
// unified shortlist — same localStorage key + listing ids as the Explorer & MLS
const SHORT=new Set((function(){ try{ return JSON.parse(localStorage.getItem('cre_shortlist')||'[]'); }catch(e){ return []; } })());
function saveShort(){ try{ localStorage.setItem('cre_shortlist', JSON.stringify([...SHORT])); }catch(e){} }
function toggleStar(id){ SHORT.has(id)?SHORT.delete(id):SHORT.add(id); saveShort(); }
function updateShortBtn(){ const sb=$('#shortbtn'); if(sb){ sb.textContent='⭐ Shortlist ('+SHORT.size+')'; sb.classList.toggle('shorton',F.shortlist); } }
function median(a){ if(!a.length) return null; const s=a.slice().sort((x,y)=>x-y); const m=Math.floor(s.length/2); return s.length%2?s[m]:(s[m-1]+s[m])/2; }
function renderStats(rows){ const sb=$('#statsbar'); if(!sb) return; if(!rows.length){ sb.innerHTML=''; return; }
  const prices=rows.map(c=>+c.price).filter(v=>v>0);
  const pps=rows.map(c=>c.sqft?+c.price/c.sqft:null).filter(v=>v>0);
  const hoas=rows.map(c=>+c.hoa).filter(v=>v>0);
  const yrs=rows.map(c=>+c.year_built).filter(v=>v>1800);
  const pill=(l,v)=>v==null?'':`<span class="statpill"><span class="sl">${l}</span> <b>${v}</b></span>`;
  sb.innerHTML=pill('Count',rows.length)+pill('Median price',prices.length?'$'+Math.round(median(prices)).toLocaleString():null)+pill('Median $/SF',pps.length?'$'+Math.round(median(pps)).toLocaleString():null)+pill('Median HOA',hoas.length?'$'+Math.round(median(hoas)).toLocaleString()+'/mo':null)+pill('Median year',yrs.length?Math.round(median(yrs)):null);
}
const F={ warr:'all', cities:new Set(), firms:new Set(), bedsMin:0, bathsMin:0, shortlist:false, contactReady:false,
  pmin:null,pmax:null, hoamin:null,hoamax:null, sqftMin:null,sqftMax:null, yearMin:null,yearMax:null, ppsfMin:null,ppsfMax:null };
function rng(v,mn,mx){ if(mn!=null){ if(v==null||!(+v>=mn)) return false; } if(mx!=null){ if(v==null||!(+v<=mx)) return false; } return true; }
// Days until the matched FHA cert expires (negative = lapsed N days ago), parsed from the clean
// fha_expiration_date the server surfaces. SINGLE source of truth for the 'Days to expiry' column
// AND the date-based quick-filter chips (⚠ Expiring ≤90d / 🔴 Lapsed ≤180d). null = no FHA match.
function daysToExp(c){
  const m=String(c.fha_expiration_date||'').match(/(\d{2})\/(\d{2})\/(\d{4})/); if(!m) return null;
  const exp=new Date(+m[3],+m[1]-1,+m[2]); exp.setHours(0,0,0,0);
  const t=new Date(); t.setHours(0,0,0,0);
  return Math.round((exp-t)/86400000);
}
// The two computed chips ('expiring90','lapsed180') are subsets of approved/expired keyed off the
// live day-count, so they filter by date rather than by the frozen warrantable_status string.
function passWarrChip(c,ws){
  if(F.warr==='expiring90'){ const d=daysToExp(c); return ws==='fha_approved'&&d!=null&&d>=0&&d<=90; }
  if(F.warr==='expiring180'){ const d=daysToExp(c); return ws==='fha_approved'&&d!=null&&d>=0&&d<=180; }
  if(F.warr==='expiring365'){ const d=daysToExp(c); return ws==='fha_approved'&&d!=null&&d>=0&&d<=365; }
  if(F.warr==='lapsed180'){ const d=daysToExp(c); return ws==='fha_expired'&&d!=null&&d<0&&d>=-180; }
  return F.warr==='all'?true:F.warr==='unwarrantable'?ws!=='fha_approved':ws===F.warr;
}
function passFacets(c){
  const ws=c.warrantable_status||c.warrant_signal;
  if(!passWarrChip(c,ws)) return false;
  if(F.cities.size&&!F.cities.has(c.city)) return false;
  if(F.firms.size&&!F.firms.has(c.firm_name)) return false;
  if(F.bedsMin&&!(+c.beds>=F.bedsMin)) return false;
  if(F.bathsMin&&!(+c.baths>=F.bathsMin)) return false;
  if(!rng(c.price,F.pmin,F.pmax)) return false;
  if(!rng(c.hoa,F.hoamin,F.hoamax)) return false;
  if(!rng(c.sqft,F.sqftMin,F.sqftMax)) return false;
  if(!rng(c.year_built,F.yearMin,F.yearMax)) return false;
  if(!rng(c.sqft?c.price/c.sqft:null,F.ppsfMin,F.ppsfMax)) return false;
  if(F.shortlist && !SHORT.has(c.id)) return false;
  if(F.contactReady && !hasContact(c)) return false;
  return true;
}

// ---- rail builder ----
function buildRail(){
  const wc={},cc={},fc={}; let ec=0,e180=0,e365=0,lc=0;   // approved expiring ≤90/≤180/≤365d (cumulative); lc=expired within last 180d
  DATA.forEach(c=>{ const s=c.warrantable_status||c.warrant_signal||'?'; wc[s]=(wc[s]||0)+1; if(c.city)cc[c.city]=(cc[c.city]||0)+1; if(c.firm_name)fc[c.firm_name]=(fc[c.firm_name]||0)+1;
    const d=daysToExp(c);
    if(s==='fha_approved'&&d!=null&&d>=0){ if(d<=90)ec++; if(d<=180)e180++; if(d<=365)e365++; }
    if(s==='fha_expired'&&d!=null&&d<0&&d>=-180) lc++; });
  const scount={expiring90:ec, expiring180:e180, expiring365:e365, lapsed180:lc};   // computed-chip counts (not keyed by warrantable_status)
  const order=[['all','All'],['fha_approved','FHA-approved'],['expiring90','⚠ Expiring ≤90d'],['expiring180','Expiring ≤6mo'],['expiring365','Expiring ≤12mo'],['fha_expired','FHA-expired'],['lapsed180','🔴 Lapsed ≤180d'],['not_listed','Not-listed'],['heuristic_flag','Flagged'],['unwarrantable','⚑ Unwarrantable · non-FHA']];
  $('#fWarr').innerHTML=order.filter(([v])=>v==='all'||v==='unwarrantable'||(v in scount?scount[v]:wc[v])).map(([v,l])=>{
    const n=v==='all'?DATA.length:v==='unwarrantable'?(DATA.length-(wc.fha_approved||0)):(v in scount?scount[v]:wc[v]);
    return `<span class="chip warr-${v} ${F.warr===v?'active':''}" data-warr="${v}">${l}<span class="ct">${n}</span></span>`;
  }).join('');
  // 📇 Contact-ready toggle — independent of warrantability; auto-hidden when no listing carries a
  // contact (e.g. the FHA-directory fallback mode, whose rows have no broker overlay).
  const cn=DATA.filter(hasContact).length; const cs=$('#contactSec');
  if(cs) cs.style.display = cn ? '' : 'none';
  if(cn) $('#fContact').innerHTML=`<span class="chip ${F.contactReady?'active':''}" data-contact="1" title="Only listings with a reachable agent email or phone — the outreach-ready set">📇 Contact-ready<span class="ct">${cn}</span></span>`;
  $('#fCity').innerHTML=Object.entries(cc).sort((a,b)=>b[1]-a[1]).slice(0,20).map(([k,c])=>`<span class="chip ${F.cities.has(k)?'active':''}" data-cy="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
  const firms=Object.entries(fc).sort((a,b)=>b[1]-a[1]).slice(0,14);
  $('#firmSec').style.display=firms.length?'':'none';
  $('#fFirm').innerHTML=firms.map(([k,c])=>`<span class="chip ${F.firms.has(k)?'active':''}" data-fm="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
  const hasBeds=DATA.some(c=>c.beds!=null), hasBaths=DATA.some(c=>c.baths!=null);
  $('#bedSec').style.display=hasBeds?'':'none'; $('#bathSec').style.display=hasBaths?'':'none';
  $('#fBeds').innerHTML=[0,1,2,3,4].map(n=>`<span class="chip ${F.bedsMin===n?'active':''}" data-beds="${n}">${n===0?'Any':n+'+'}</span>`).join('');
  $('#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=orderedFields().map(f=>`<label class="ftog" draggable="true" data-fk="${f.k}"><span class="fdrag" title="Drag to reorder column">⠿</span><input type="checkbox" data-fk="${f.k}" ${fvis(f.k)?'checked':''}><span class="flbl">${esc(f.l)}</span>${f.col?'<span class="ct" title="Also a List column">▤</span>':''}</label>`).join(''); }

// ---- per-listing broker (with DRE# hooked to the full CA DRE record) + notes ----
let NOTES={};   // {id:{note,updated_at}} loaded once at boot from /api/condo-notes
const telHref=p=>'tel:'+String(p||'').replace(/[^0-9]/g,'');
const mailHref=e=>'mailto:'+String(e||'').trim();
// ── contact resolvers (TK-10243): each falls back through the flat field → primary agent record so
// the List column matches whatever the card shows, and returns '' when genuinely absent (no fake '—'). ──
function agentEmail(c){ return c.agent_email || (c.agents&&c.agents[0]&&c.agents[0].email) || ''; }
// Contact-ready = we have a reachable channel for the listing agent (email or any phone) — the set
// worth filtering to for renewal / re-cert outreach. Powers the 📇 Contact-ready toggle chip.
function hasContact(c){ return !!(agentEmail(c) || c.agent_phone || c.office_phone || (c.agents&&c.agents[0]&&(c.agents[0].phone||c.agents[0].email))); }
// responsible broker carries the brokerage's office mailing ADDRESS (from the CA DRE record)
function respBroker(c){ return c.responsible_broker || (c.agents&&c.agents[0]&&c.agents[0].dre_record&&c.agents[0].dre_record.responsible_broker) || ''; }
// buyer-SIDE agents — SOLD listings only; mirrors agents[]. Filled by the boughtWith scrape
// (buyer_agents[]) with a flat-field fallback. Empty array for active listings (no buyer side yet).
function buyerAgents(c){ return (Array.isArray(c.buyer_agents)&&c.buyer_agents.length)?c.buyer_agents
  : (c.buyer_agent_name?[{name:c.buyer_agent_name,firm:c.buyer_firm_name,dre:c.buyer_agent_dre,phone:c.buyer_agent_phone}]:[]); }
function brokerCell(c){
  // Always render (Steve 2026-08-03: every listing must carry the broker/firm/contact) — not gated
  // on the field-visibility toggles the way the generic rows are.
  if(!(c.broker_name||c.firm_name)){
    return c.source?`<div class="broker" style="color:var(--mut)">🧑‍💼 <a href="${esc(c.source)}" target="_blank" rel="noopener noreferrer">Listed by — view broker on Redfin ↗</a></div>`:'';
  }
  const aph=c.agent_phone?` · <a href="${telHref(c.agent_phone)}">📱 ${esc(c.agent_phone)}</a>`:'';
  const oph=(c.office_phone&&c.office_phone!==c.agent_phone)?` · ☎ ${esc(c.office_phone)}`:'';
  const exp=c.broker_dre_expiration?`<div class="broker" style="color:var(--mut);font-size:10px">DRE exp ${esc(c.broker_dre_expiration)}${c.responsible_broker?' · resp. broker '+esc(c.responsible_broker):''}</div>`:'';
  // Record ALL license numbers on the listing (Steve 2026-08-03): one line per agent (primary +
  // co-listing agent), each DRE# linked to its full CA DRE record. Falls back to the single broker.
  const agents=(Array.isArray(c.agents)&&c.agents.length)?c.agents:[{name:c.broker_name,firm:c.firm_name,dre:c.broker_dre,dre_record:(c.dre_url?{dre_url:c.dre_url}:null)}];
  const dreLink=(a)=>{ if(!a.dre) return ''; const u=(a.dre_record&&a.dre_record.dre_url)||('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+a.dre); return ` · <a href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">DRE# ${esc(a.dre)}</a>`; };
  const aLink=(a)=>a.name?`<a href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open ${esc(a.name)}'s own page — current & past listings">${esc(a.name)} ↗</a>`:'';
  const lines=agents.map((a,i)=>`🧑‍💼 ${aLink(a)}${a.firm?(a.name?' · ':'')+esc(a.firm):''}${dreLink(a)}${i===0?aph+oph:'<span style="color:var(--mut);font-size:10px"> (co-agent)</span>'}`).join('<br>');
  // email line — always shown when present (Steve: every listing carries full contact); mailto-linked.
  const em=agentEmail(c); const emLine=em?`<div class="broker" style="font-size:11px">✉ <a href="${mailHref(em)}">${esc(em)}</a></div>`:'';
  return `<div class="broker">${lines}</div>${emLine}${exp}`;
}
// ── BUYER'S SIDE — the "other side" of a SOLD deal (TK-10243). Always shown on the card when the
// boughtWith scrape captured it; empty (renders nothing) for active listings, which have no buyer
// side yet. Kept visually distinct from the listing side with a 🤝 marker + a dashed divider. ──
function buyerCell(c){
  const ba=buyerAgents(c); if(!ba.length) return '';
  const line=ba.map(a=>{
    const u=a.dre?('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+a.dre):'';
    const dre=a.dre?(u?` · <a href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">DRE# ${esc(a.dre)}</a>`:` · DRE# ${esc(a.dre)}`):'';
    const ph=a.phone?` · <a href="${telHref(a.phone)}">📱 ${esc(a.phone)}</a>`:'';
    const em=a.email?` · <a href="${mailHref(a.email)}">✉ ${esc(a.email)}</a>`:'';
    return `🤝 ${esc(a.name||'')}${a.firm?' · '+esc(a.firm):''}${dre}${ph}${em}`;
  }).join('<br>');
  return `<div class="broker" style="border-top:1px dashed var(--line);margin-top:4px;padding-top:5px"><span style="color:var(--mut);font-size:10px;letter-spacing:.4px">BUYER'S SIDE · sold by</span><br>${line}</div>`;
}
function noteBox(c){
  const v=(NOTES[c.id]&&NOTES[c.id].note)||'';
  return `<div class="notewrap"><textarea class="notebox" data-note="${esc(c.id)}" rows="2" placeholder="✎ Your notes on this listing…">${esc(v)}</textarea><span class="notestatus" data-nstat="${esc(c.id)}"></span></div>`;
}
// Always-on created date+time chip (Steve HARD rule: admin cards must show when the
// record was added, date AND time, visible on the card — not toggle-gated. Mirrors the
// design-curator .when chip). Full ISO in title=. Falls back to last_seen; hidden if neither.
function whenChip(c){
  const t=c.created_at||c.last_seen; const s=fmtDateTime(t); if(!s) return '';
  return `<div class="when" title="${esc(isoOf(t))}" style="font-size:10px;color:var(--mut,#8b949e);margin-top:6px">🕓 Added ${esc(s)}</div>`;
}

// ---- renderers ----
// Contact fields render via brokerCell/buyerCell (always-on) — skip them from the generic card rows
// so a toggled-on column doesn't double-print on the card. They stay live as List columns + toggles.
const CARD_SKIP=new Set(['warrantable_status','city','zip','project_name','warrant_source','created_at',
  'broker_name','firm_name','agent_phone','office_phone','agent_email','broker_dre','responsible_broker','buyer_agent']);
function dbCards(rows){
  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?', '+drillSpan('city',c.city):''} ${esc(c.zip||'')}</div>
    ${whenChip(c)}
    ${fvis('warrantable_status')?statusBadge(c.warrantable_status||c.warrant_signal):''}
    ${vis.map(f=>`<div class="row"><span class="k">${esc(f.l)}</span><span>${fcell(c,f)}</span></div>`).join('')}
    ${brokerCell(c)}
    ${buyerCell(c)}
    ${fvis('warrant_source')&&c.warrant_source?`<div class="src">${esc(c.warrant_source)}</div>`:''}
    ${noteBox(c)}
  </div>`).join('');
}
function dbList(rows){
  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?', '+drillSpan('city',c.city):''}</div></td>`
    +cols.map(f=>`<td>${fcell(c,f)}</td>`).join('')+'</tr>').join('');
  return `<div class="listwrap"><table class="listtbl" data-no-sort><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
}
// paint owns the GRID (cards) + TABLE (dense spreadsheet) views; the COMPACT LIST
// view is owned by ThreeView into #condoList. The active container is shown/hidden
// by ThreeView.render(); paint just fills #grid for grid/table.
let _condoInf=null, _condoRows=[];
function paint(rows){ const g=$('#grid'), lst=$('#condoList');
  if(VIEW==='list'){ if(TV) TV.render(); if(g) g.style.display='none'; if(lst) lst.style.display=''; return; }
  if(lst) lst.style.display='none'; if(g) g.style.display='';
  // Windowed infinite scroll for grid + table (the whole set once froze the page). The helper
  // reveals a batch and loads more as you scroll; reset() repaints from the top on filter/sort change.
  _condoRows=rows;
  if(!_condoInf && window.InfScroll) _condoInf=InfScroll.attach({batch:120,getRows:()=>_condoRows,getMount:()=>$('#grid'),render:paintCondoWindow});
  if(_condoInf) _condoInf.reset(); else paintCondoWindow(rows);   // fallback if helper missing
}
// Render one window (slice) of condo rows into #grid for the current grid/table view.
function paintCondoWindow(win){ const g=$('#grid');
  if(!win.length){ g.className='grid'; g.innerHTML='<div class="empty">no condos match these filters.</div>'; lastColSig=''; return; }
  if(VIEW==='table'){
    g.className='grid listhost'; g.innerHTML=dbList(win);
    // 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(win); 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!=='table' || !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){
    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);
  CURRENT=rows;   // the exact filtered+sorted set on screen — powers CSV export + the draft-outreach action
  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')}`;
  $('#banner').textContent = unw
    ? '⚑ Non-warrantable condos — NOT on the FHA-approved list (expired or never listed). Conventional/FHA lenders decline these — non-QM / portfolio financing territory.'
    : bannerDefault;
  paint(rows);
}

// ---- boot ----
// ── Non-Warrantable Condos section ─────────────────────────────────────────────
// Steve 2026-08-19: "create a unwarrantable condos section with real fha and warrant info."
// HONEST labeling (the classifier's hard rule): "non-warrantable" here = NOT on the current
// HUD FHA-approved list — a financing-eligibility PROXY, not a lender-verified Fannie/Freddie
// warrantability call. We split it into its real buckets so a loan officer sees WHY each is risk.
const nwMoney=n=>(n==null||isNaN(+n))?'—':'$'+Math.round(+n).toLocaleString();
function setWarr(v){ F.warr=v; $$('#fWarr .chip').forEach(x=>x.classList.toggle('active',x.dataset.warr===v)); apply();
  const g=$('#grid'); if(g&&g.scrollIntoView) g.scrollIntoView({behavior:'smooth',block:'start'}); }
function nwReason(c){
  const ws=c.warrantable_status||c.warrant_signal;
  if(ws==='fha_expired'){ const exp=c.fha_expiration_date||''; return {cls:'',txt:'FHA approval LAPSED'+(exp?' '+esc(exp):'')+' — re-certification required'}; }
  if(ws==='heuristic_flag'){ const sig=(c.warrant_signals&&c.warrant_signals.signals)||[]; const n=sig[0]; return {cls:'',txt:esc((n&&n.note)||'hard non-warrantable signal flagged (condotel / investor / cash-only)')}; }
  return {cls:'nl',txt:'Not on the FHA-approved list — verify warrantability with the lender'};
}
function renderNonWarrant(){
  const sec=$('#nwSection'); if(!sec) return;
  if(!DATA.length){ sec.hidden=true; return; }
  const cnt={fha_approved:0,fha_expired:0,heuristic_flag:0,not_listed:0};
  DATA.forEach(c=>{ const s=c.warrantable_status||c.warrant_signal||'not_listed'; if(cnt[s]!=null)cnt[s]++; });
  const total=DATA.length, nonWarr=total-(cnt.fha_approved||0);
  const pct=total?Math.round(1000*nonWarr/total)/10:0;
  // Real HUD-lapsed examples — the most actionable: a project that WAS FHA-approved and lapsed is a
  // re-certification opportunity. Highest-price first (biggest loan). Uses real warrant_source data.
  const expired=DATA.filter(c=>(c.warrantable_status||c.warrant_signal)==='fha_expired')
    .sort((a,b)=>(+b.price||0)-(+a.price||0)).slice(0,5);
  const tile=(cls,n,lbl,sub,warr)=>`<div class="nw-tile ${cls}" data-setwarr="${warr}" title="Show only these"><div class="n">${n.toLocaleString()}</div><div class="lbl">${lbl}</div><div class="sub">${sub}</div></div>`;
  sec.hidden=false;
  sec.innerHTML=
    `<h2>🚫 Non-Warrantable Condos <span style="font-weight:400;font-size:12px;color:var(--mut)">— ${nonWarr.toLocaleString()} of ${total.toLocaleString()} (${pct}%) not FHA-approved</span></h2>`+
    `<p class="lede">A <b>non-warrantable</b> condo can't be financed with a standard conforming/FHA loan, so a buyer needs a portfolio or non-QM product (bigger down, higher rate). This is an <b>FHA/VA-approval-based proxy</b>, <b>not</b> a lender-verified Fannie/Freddie warrantability call — always confirm with the lender. Below, the ${nonWarr.toLocaleString()} non-FHA-approved condos split into their real risk buckets:</p>`+
    `<div class="nw-tiles">`+
      tile('approved',cnt.fha_approved,'FHA-Approved','Financeable (proxy) — on the HUD list','fha_approved')+
      tile('expired',cnt.fha_expired,'FHA-Expired','Was approved, cert lapsed — re-cert needed','fha_expired')+
      tile('notlisted',cnt.not_listed,'Not-Listed','Not on the FHA list — verify w/ lender','not_listed')+
      tile('flagged',cnt.heuristic_flag,'Flagged','Hard non-warrantable signal fired','heuristic_flag')+
    `</div>`+
    `<div class="nw-cta"><button class="nw-btn" id="nwShowAll">Show all ${nonWarr.toLocaleString()} non-warrantable →</button>`+
      `<button class="nw-btn ghost" id="nwShowApproved">Show FHA-approved only</button>`+
      `<span class="nw-note">Click any tile to filter the grid to that bucket.</span></div>`+
    (expired.length?`<div class="nw-ex"><h3>⚠ FHA-approval LAPSED — re-certification opportunities (real HUD list matches)</h3>`+
      expired.map(c=>{ const r=nwReason(c); return `<div class="nw-row"><span class="a">${esc(c.address||c.project_name||'—')}</span><span>${esc(c.city||'')}</span><span>${nwMoney(c.price)}</span><span class="why ${r.cls}">${r.txt}</span></div>`; }).join('')+
      `</div>`:'');
  sec.querySelectorAll('[data-setwarr]').forEach(el=>el.onclick=()=>setWarr(el.dataset.setwarr==='fha_approved'?'fha_approved':el.dataset.setwarr));
  const sa=sec.querySelector('#nwShowAll'); if(sa) sa.onclick=()=>setWarr('unwarrantable');
  const ap=sec.querySelector('#nwShowApproved'); if(ap) ap.onclick=()=>setWarr('fha_approved');
}
async function boot(){
  try{ const r=await (await fetch('/api/condo-notes')).json(); NOTES=r.notes||{}; }catch(_){}
  try{ const r=await (await fetch('/api/condos')).json(); DATA=r.condos||[]; if(r.label) bannerDefault='⚖️ '+r.label; }catch(_){}
  if(!DATA.length){
    MODE='fha';
    try{ const r=await (await fetch('/api/fha-condos?status=all')).json(); DATA=(r.condos||[]).map(c=>({...c,warrantable_status:c.warrant_signal})); bannerDefault='⚖️ '+(( DATA[0]&&DATA[0].label)||PROXY)+' — HUD FHA-approved project directory (no live condo listings loaded; full residential scrape is gated).'; }catch(_){}
  }
  if(!bannerDefault) bannerDefault='⚖️ '+PROXY;
  // deep-link (drill-through from the graphics dashboard): ?status=…&city=…&q=…
  const P=new URLSearchParams(location.search);
  const qs=P.get('status');
  if(qs && ['all','fha_approved','fha_expired','not_listed','heuristic_flag','unwarrantable','expiring90','expiring180','expiring365','lapsed180'].includes(qs)) F.warr=qs;
  const cityParam=P.get('city');
  if(cityParam){
    // FHA-list cities are UPPERCASE, scraped-listing cities are Title Case → resolve case-insensitively
    const want=cityParam.trim().toLowerCase();
    const real=[...new Set(DATA.map(c=>c.city).filter(Boolean))].find(c=>String(c).toLowerCase()===want);
    if(real) F.cities.add(real);
  }
  const qParam=P.get('q');
  if(qParam){ const box=$('#q'); if(box) box.value=qParam; }
  // deep-link sort (e.g. graphics "expiring" tiles → &sort=expiry_soon lands soonest-first)
  const sortParam=P.get('sort');
  if(sortParam && ['price_desc','price_asc','pps_desc','year_desc','hoa_asc','proj_az','city_az','expiry_soon'].includes(sortParam)){
    SORTKEY=sortParam; const sel=$('#csort'); if(sel) sel.value=SORTKEY; try{localStorage.setItem('condoSort',SORTKEY);}catch(_){}
  }
  buildRail(); apply(); renderNonWarrant();
}

// ---- wiring ----
function tog(set,v){ set.has(v)?set.delete(v):set.add(v); }
// per-listing notes — debounced autosave to /api/condo-notes/:id (survives re-render via NOTES cache)
const _noteTimers={};
$('#grid').addEventListener('input',e=>{
  const t=e.target.closest('.notebox'); if(!t) return;
  const id=t.dataset.note, val=t.value;
  const stat=document.querySelector(`.notestatus[data-nstat="${CSS.escape(id)}"]`); if(stat) stat.textContent='…';
  clearTimeout(_noteTimers[id]);
  _noteTimers[id]=setTimeout(async()=>{
    try{ await fetch('/api/condo-notes/'+encodeURIComponent(id),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({note:val})});
      if(val.trim()) NOTES[id]={note:val,updated_at:new Date().toISOString()}; else delete NOTES[id];
      if(stat){ stat.textContent='✓ saved'; setTimeout(()=>{if(stat)stat.textContent='';},1500); }
    }catch(_){ if(stat) stat.textContent='⚠ not saved'; }
  },600);
});
$('#fWarr').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.warr=c.dataset.warr;$$('#fWarr .chip').forEach(x=>x.classList.toggle('active',x.dataset.warr===F.warr));apply();});
$('#fContact').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.contactReady=!F.contactReady;c.classList.toggle('active',F.contactReady);apply();});

// ── #4 CSV export of the CURRENT filter (whatever chips/search/sort are active) ──
function exportCSV(){
  const rows=CURRENT; if(!rows.length){ alert('Nothing to export — the current filter matches 0 listings.'); return; }
  const cols=[['project_name','Project'],['address','Address'],['city','City'],['zip','ZIP'],
    ['warrantable_status','Warrantability'],['fha_expiration_date','FHA expires'],['days_to_expiry','Days to expiry'],
    ['price','Price'],['hoa','HOA/mo'],['beds','Beds'],['baths','Baths'],
    ['broker_name','Listing agent'],['firm_name','Firm'],['agent_email','Agent email'],['agent_phone','Agent phone']];
  const dtf=FIELDS.find(f=>f.k==='days_to_expiry');
  const val=(c,k)=> k==='days_to_expiry' ? (dtf.calc(c)==null?'':dtf.calc(c)) : k==='agent_email' ? agentEmail(c) : (c[k]==null?'':c[k]);
  const q=s=>{s=String(s); return /[",\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;};
  const csv=[cols.map(c=>q(c[1])).join(',')].concat(rows.map(r=>cols.map(c=>q(val(r,c[0]))).join(','))).join('\n');
  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));
  a.download=`crcp-condos-${F.warr}${F.contactReady?'-contact':''}-${new Date().toISOString().slice(0,10)}.csv`;
  document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href);
}
$('#exportbtn') && $('#exportbtn').addEventListener('click',exportCSV);

// ── #2 Renewal targets → Solicitation Studio DRAFTS (draft-only; nothing sends) ──
async function draftOutreach(){
  const rows=CURRENT.filter(c=>daysToExp(c)!=null);   // only FHA-matched listings have a renewal angle
  if(!rows.length){ alert('No FHA-matched listings in the current filter to draft outreach for.\nTry the ⚠ Expiring or 🔴 Lapsed chip first.'); return; }
  const CAP=100, set=rows.slice(0,CAP);
  if(!confirm(`Queue ${set.length} DRAFT outreach email(s) in the Solicitation Studio?\n\nDrafts only — nothing sends. ($0, local)`)) return;
  let ok=0,fail=0;
  for(const c of set){
    const d=daysToExp(c), proj=c.project_name||c.address||'this project';
    const when=d<0?`expired ${(-d).toLocaleString()} days ago`:`expires in ${d.toLocaleString()} days (${c.fha_expiration_date})`;
    const subject=`FHA approval ${d<0?'has lapsed':'is expiring'} — ${proj}`;
    const body=`Hello,\n\nOur records show the FHA project approval for ${proj}${c.city?` in ${c.city}`:''} ${when}. `+
      `${d<0?'Recertifying restores FHA/VA financing eligibility for buyers in the community.':'Recertifying before it lapses keeps FHA/VA buyers eligible and protects unit values.'}\n\n`+
      `We help HOAs navigate the FHA recertification process — happy to walk you through it.\n\n— Designer / CRCP`;
    try{ const r=await fetch('/api/solicitation/queue',{method:'POST',headers:{'Content-Type':'application/json'},
        body:JSON.stringify({tone:'professional',type:'email',audience:'fha-renewal',subject,body})}); r.ok?ok++:fail++; }
    catch(_){ fail++; }
  }
  alert(`Drafted ${ok} outreach email(s) to the Solicitation Studio queue${fail?` (${fail} failed)`:''}.`+
    `${rows.length>CAP?`\n\nCapped at ${CAP}; ${(rows.length-CAP).toLocaleString()} more matched the filter.`:''}`+
    `\n\nAll DRAFT — review + send from the Solicitation Studio. Nothing was sent.`);
}
$('#draftbtn') && $('#draftbtn').addEventListener('click',draftOutreach);
$('#fCity').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.cities,c.dataset.cy);c.classList.toggle('active');apply();});
$('#fFirm').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.firms,c.dataset.fm);c.classList.toggle('active');apply();});
$('#fBeds').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.bedsMin=+c.dataset.beds;$$('#fBeds .chip').forEach(x=>x.classList.toggle('active',+x.dataset.beds===F.bedsMin));apply();});
$('#fBaths').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.bathsMin=+c.dataset.baths;$$('#fBaths .chip').forEach(x=>x.classList.toggle('active',+x.dataset.baths===F.bathsMin));apply();});
const rangeWire=(id,key)=>$('#'+id).addEventListener('input',()=>{F[key]=$('#'+id).value?+$('#'+id).value:null;apply();});
rangeWire('pMin','pmin');rangeWire('pMax','pmax');rangeWire('hoaMin','hoamin');rangeWire('hoaMax','hoamax');
rangeWire('sqMin','sqftMin');rangeWire('sqMax','sqftMax');rangeWire('psfMin','ppsfMin');rangeWire('psfMax','ppsfMax');
rangeWire('yrMin','yearMin');rangeWire('yrMax','yearMax');
$('#fFields').addEventListener('change',e=>{const cb=e.target.closest('input[data-fk]');if(!cb)return;VIS[cb.dataset.fk]=cb.checked;saveVis();apply();});
// ── 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(); apply(); }
  }
  pDragKey=null; });
$('#fFields').addEventListener('dragend',()=>{ $('#fFields').querySelectorAll('.fdragging,.fdropT,.fdropB').forEach(x=>x.classList.remove('fdragging','fdropT','fdropB')); pDragKey=null; });
$('#fAll').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=true);saveVis();buildFieldToggles();apply();});
$('#fNone').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=false);saveVis();buildFieldToggles();apply();});
$('#fDef').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=!!f.def);saveVis();buildFieldToggles();apply();});
$('#q').oninput=()=>{ clearTimeout(window._t); window._t=setTimeout(apply,180); };
// Page-level Reset (Steve 2026-08-22): clear ALL filters + search + sort and strip the URL
// deep-link params (?status=…&city=…&q=…&sort=…) → back to the full default list.
$('#pageReset').addEventListener('click',()=>{
  F.warr='all'; F.cities.clear(); F.firms.clear(); F.bedsMin=0; F.bathsMin=0;
  F.shortlist=false; F.contactReady=false;
  F.pmin=F.pmax=F.hoamin=F.hoamax=F.sqftMin=F.sqftMax=F.yearMin=F.yearMax=F.ppsfMin=F.ppsfMax=null;
  const box=$('#q'); if(box) box.value='';
  document.querySelectorAll('.rail input[type=number]').forEach(i=>{ i.value=''; });
  SORTKEY=''; const sel=$('#csort'); if(sel) sel.value=''; try{ localStorage.removeItem('condoSort'); }catch(_){}
  updateShortBtn();
  try{ history.replaceState(null,'',location.pathname); }catch(_){}
  buildRail(); apply(); renderNonWarrant();
});
// ── sort (Steve rule: every grid gets sort + density; condos already had density) ──
function sortRows(rows){
  const num=(v)=>{const n=+v;return isFinite(n)?n:null;};
  const cmp={
    price_desc:(a,b)=>(num(b.price)??-1)-(num(a.price)??-1),
    price_asc:(a,b)=>(num(a.price)??Infinity)-(num(b.price)??Infinity),
    pps_desc:(a,b)=>((num(b.price)&&num(b.sqft)?b.price/b.sqft:-1))-((num(a.price)&&num(a.sqft)?a.price/a.sqft:-1)),
    year_desc:(a,b)=>(num(b.year_built)??-1)-(num(a.year_built)??-1),
    hoa_asc:(a,b)=>(num(a.hoa)??Infinity)-(num(b.hoa)??Infinity),
    proj_az:(a,b)=>String(a.project_name||a.address||'').localeCompare(String(b.project_name||b.address||'')),
    city_az:(a,b)=>String(a.city||'').localeCompare(String(b.city||'')),
    // FHA expires ↑ (soonest actionable first): upcoming certs by nearness (0,1,2…), THEN already-
    // lapsed (most-recent first), THEN unmatched (no FHA date) last. Ranks via daysToExp().
    expiry_soon:(a,b)=>{const k=c=>{const d=daysToExp(c);return d==null?Infinity:(d>=0?d:1e6-d);};return k(a)-k(b);},
  }[SORTKEY];
  return cmp?rows.slice().sort(cmp):rows;
}
(function(){ const s=$('#csort'); if(s){ s.value=SORTKEY; s.addEventListener('change',()=>{ SORTKEY=s.value; localStorage.setItem('condoSort',SORTKEY); apply(); }); } })();
$('#cols').oninput=e=>{ document.documentElement.style.setProperty('--cols',e.target.value); localStorage.setItem('condoCols',e.target.value); };
const savedCols=localStorage.getItem('condoCols'); if(savedCols){ $('#cols').value=savedCols; document.documentElement.style.setProperty('--cols',savedCols); }
// ── 3-view toggle (grid cards / compact list / dense table) via the shared helper.
// ThreeView owns the segmented buttons + the compact-list renderer into #condoList;
// paint() still owns grid + table. All three modes stay in sync with the page's
// filters (getRows→CURRENT) and column-visibility toggles (fvis). ──
// compact-list cells reuse the page's rich fcell() (mailto / DRE-link / drill-span
// HTML), so the List view matches the cards + table exactly (html:true tells the
// helper the calc value is already-formatted HTML and passes it through unescaped).
// ThreeView owns ONLY the compact-list container (#condoList) + the segmented
// buttons; the page's paint() owns #grid (cards + dense table) show/hide + render.
// onView flips VIEW then re-runs apply() so paint() re-renders grid/table on switch.
TV=ThreeView.mount({
  seg:'#viewseg', listMount:'#condoList',
  storageKey:'condo', defaultView:VIEW, views:['grid','list','table'],
  fields:FIELDS.map(f=>({k:f.k,l:f.l,badge:f.badge,html:true,calc:c=>fcell(c,f)})),
  getRows:()=>CURRENT, fvis:fvis,
  title:c=>({name:c.project_name||c.address||'Condo', sub:[c.address,c.city,c.zip].filter(Boolean).join(', ')}),
  badgeHtml:c=>fvis('warrantable_status')?statusBadge(c.warrantable_status||c.warrant_signal):'',
  onView:v=>{ VIEW=v; apply(); },
  rowClick:c=>{ if(c && c.id!=null) openDetail(c.id); },
});
// 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">'+(function(){const q=[c.address,c.city,c.zip].filter(Boolean).join(', ');const txt=esc(c.address||'')+(c.city?', '+esc(c.city):'')+(c.zip?' '+esc(c.zip):'');return q?`<a class="dl" href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(q)}" target="_blank" rel="noopener noreferrer" title="Open in Google Maps">${txt} 🗺️</a>`:txt;})()+'</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):'')
    +(function(){
      // Record ALL license numbers on the listing: one row per agent (primary + co-listing agent),
      // name→directory, firm→directory, DRE#→full CA DRE record. Falls back to the single broker.
      const ags=(Array.isArray(c.agents)&&c.agents.length)?c.agents:((c.broker_name||c.broker_dre)?[{name:c.broker_name,firm:c.firm_name,dre:c.broker_dre,dre_record:(c.dre_url?{dre_url:c.dre_url}:null)}]:[]);
      return ags.map((a,i)=>{
        const nm=a.name?`<a class="dl" href="/crcp.html?brokerName=${encodeURIComponent(a.name)}">${esc(a.name)}</a>`:'';
        const fm=a.firm?`${a.name?' · ':''}<a class="dl" href="/crcp.html?firm=${encodeURIComponent(a.firm)}">${esc(a.firm)}</a>`:'';
        const u=a.dre?((a.dre_record&&a.dre_record.dre_url)||('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+a.dre)):'';
        const dre=a.dre?` · <a class="dl" href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">DRE# ${esc(a.dre)} ↗</a>`:'';
        // primary agent carries phone(s) + email inline; co-agents show name/firm/DRE
        const ph=(i===0&&c.agent_phone)?` · <a class="dl" href="${telHref(c.agent_phone)}">📱 ${esc(c.agent_phone)}</a>`:'';
        const op=(i===0&&c.office_phone&&c.office_phone!==c.agent_phone)?` · ☎ ${esc(c.office_phone)}`:'';
        const emv=(i===0)?agentEmail(c):(a.email||''); const em=emv?` · <a class="dl" href="${mailHref(emv)}">✉ ${esc(emv)}</a>`:'';
        return line(i===0?'Listed by':'Co-agent', nm+fm+dre+ph+op+em);
      }).join('');
    })()
    +(function(){ const rb=respBroker(c); return rb?line('Responsible broker',esc(rb)):''; })()
    +(function(){
      // BUYER'S SIDE — the other party on a SOLD deal. Name→CRCP directory, DRE→full record.
      const ba=buyerAgents(c); if(!ba.length) return '';
      return ba.map((a,i)=>{
        const nm=a.name?`<a class="dl" href="/crcp.html?brokerName=${encodeURIComponent(a.name)}">${esc(a.name)}</a>`:'';
        const fm=a.firm?`${a.name?' · ':''}<a class="dl" href="/crcp.html?firm=${encodeURIComponent(a.firm)}">${esc(a.firm)}</a>`:'';
        const u=a.dre?('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+a.dre):'';
        const dre=a.dre?` · <a class="dl" href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">DRE# ${esc(a.dre)} ↗</a>`:'';
        const ph=a.phone?` · <a class="dl" href="${telHref(a.phone)}">📱 ${esc(a.phone)}</a>`:'';
        const em=a.email?` · <a class="dl" href="${mailHref(a.email)}">✉ ${esc(a.email)}</a>`:'';
        return line(i===0?'Sold by (buyer’s side)':'Co-agent (buyer)', nm+fm+dre+ph+em);
      }).join('');
    })()
    +line('Added',dateTimeSpan(c.created_at))
    +(c.source&&/^https?:/.test(c.source)?line('Listing',`<a class="dl" href="${esc(c.source)}" target="_blank" rel="noopener noreferrer">🔗 View listing ↗</a>`):'')
    +'</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)
// drill-to-filter also works from the compact-list rows (their cells reuse fcell → drillSpan)
$('#condoList').addEventListener('click',e=>{ const dr=e.target.closest('[data-drill]'); if(dr){ e.stopPropagation(); applyDrill(dr.dataset.drill,dr.dataset.val); } },true);
$('#grid').addEventListener('click',e=>{
  const dr=e.target.closest('[data-drill]');
  if(dr){ e.stopPropagation(); applyDrill(dr.dataset.drill,dr.dataset.val); return; }   // datum drill → filter; the return (same listener) suppresses the modal, stopPropagation guards future sibling listeners
  if(e.target.closest('[data-star]')||e.target.closest('th')||e.target.tagName==='A'||e.target.tagName==='BUTTON'||e.target.tagName==='TEXTAREA'||e.target.closest('.notewrap')) 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'); });
  document.querySelectorAll('.rail .rsec').forEach(s=>{ if(s.id!=='fieldsSec') s.classList.add('collapsed'); }); }
boot();
</script>
  <script src="/col-resize.js" defer></script>
  <script src="/global-search.js" defer></script>
  <script src="/sort-bar.js" defer></script>
<script src="/corner-nav.js" defer></script>
  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>