← back to Commercialrealestate

public/deals-flow.html

539 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>Closed Deal Flow — Public Record · CRCP</title>
<style>
:root{--bg:#0e1116;--card:#161b22;--line:#2a313c;--ink:#e6edf3;--mut:#8b949e;--acc:#3fb950;--blue:#58a6ff;--gold:#ffa600;--violet:#bc8cff;--cols:4}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:-apple-system,Helvetica,Arial,sans-serif;font-size:13px}
header{padding:14px 18px;border-bottom:1px solid var(--line);display:flex;flex-wrap:wrap;gap:10px;align-items:center}
h1{font-size:17px;margin:0}h1 b{color:var(--ink,#fff)}
.badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#0e1116;background:var(--acc);padding:3px 8px;border-radius:6px}
.nav a{color:var(--blue);text-decoration:none;font-size:12px;margin-left:12px}
.shell{display:flex;align-items:flex-start}
.rail{width:230px;flex:0 0 230px;border-right:1px solid var(--line);padding:12px;height:calc(100vh - 52px);overflow:auto;position:sticky;top:0}
.rsec{border-bottom:1px solid var(--line);padding:9px 0}
.rsec h4{margin:0 0 7px;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut)}
.chips{display:flex;flex-wrap:wrap;gap:5px}
.chip{border:1px solid var(--line);border-radius:20px;padding:3px 9px;font-size:11px;cursor:pointer;background:var(--card);user-select:none}
.chip.active{background:var(--blue);color:#0e1116;border-color:var(--blue);font-weight:600}
.chip .ct{color:var(--mut);margin-left:4px;font-size:10px}.chip.active .ct{color:#0e1116}
.rail input[type=number]{width:100%;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:7px 9px;font-size:12px}
.main{flex:1;padding:12px 16px;min-width:0}
.toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:10px}
.search{flex:1;min-width:200px;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:8px 11px;font-size:13px}
select{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:7px 9px;font-size:12px}
.count{color:var(--mut);font-size:12px}
.prov{font-size:11px;color:var(--mut);margin:0 0 12px;line-height:1.5}
.stat{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:9px;padding:7px 12px;margin:0 8px 10px 0}
.stat b{font-size:18px;color:var(--ink,#fff)}.stat span{display:block;font-size:10px;text-transform:uppercase;color:var(--mut);letter-spacing:.4px}
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px}
.dcard{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:13px 14px;display:flex;flex-direction:column;gap:7px}
.dcard .top{display:flex;justify-content:space-between;align-items:flex-start;gap:8px}
.dcard .name{font-weight:600;font-size:13.5px;line-height:1.25}
.dcard a.name{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(88,166,255,.4);cursor:pointer}.dcard a.name:hover{color:var(--blue);border-bottom-color:var(--blue)}
.src{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:2px 6px;border-radius:6px;white-space:nowrap;flex-shrink:0}
.src.county{background:rgba(63,185,80,.16);color:var(--acc)}
.src.edgar{background:rgba(188,140,255,.16);color:var(--violet)}
.src.fha{background:rgba(88,166,255,.16);color:var(--blue)}
.src.ula{background:rgba(255,166,0,.16);color:var(--gold)}
.dcard .price{font-size:20px;font-weight:700;color:var(--gold);font-variant-numeric:tabular-nums}
.dl{display:flex;flex-wrap:wrap;gap:5px 14px;font-size:12px;color:var(--ink)}
.dl .i{color:var(--mut);display:inline-flex;align-items:center;gap:4px}
.dl .i b{color:var(--ink);font-weight:600}
.dcard a.f{color:var(--blue);text-decoration:none;font-size:12px}
.dcard a.f:hover{text-decoration:underline}
/* href-drill atoms: card values link to their deeper/filtered data */
.dl a.ddrill{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(88,166,255,.35)}
.dl a.ddrill:hover{color:var(--blue);border-bottom-color:var(--blue)}
.dl .i a.ddrill b{color:inherit}
.provline{font-size:10px;color:var(--mut);border-top:1px solid #20262f;padding-top:6px;margin-top:1px;display:flex;align-items:center;gap:5px}
.conf{font-size:9.5px;padding:1px 6px;border-radius:20px}
.conf.high{background:rgba(63,185,80,.16);color:var(--acc)} .conf.med{background:rgba(210,153,34,.16);color:var(--gold)} .conf.low{background:rgba(139,148,158,.16);color:var(--mut)}
/* ── breaking-news ticker (horizontal marquee) ── */
.ticker{display:flex;align-items:stretch;background:var(--bg2,#0b0e13);border-bottom:1px solid var(--line);overflow:hidden;height:32px}
.ticker .lbl{flex:0 0 auto;display:flex;align-items:center;gap:6px;background:var(--acc);color:#0b0e13;font-weight:800;font-size:10px;letter-spacing:.6px;text-transform:uppercase;padding:0 13px;white-space:nowrap}
.ticker .lbl::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--bg2,#0b0e13);animation:blip 1.1s steps(1) infinite}
@keyframes blip{50%{opacity:.25}}
.ticker .vp{flex:1;overflow:hidden;position:relative}
.ticker .track{display:inline-flex;align-items:center;height:100%;white-space:nowrap;animation:tick 180s linear infinite;will-change:transform}
.ticker .vp:hover .track{animation-play-state:paused}
.ticker.is-stopped .track{animation-play-state:paused}
.ticker .tk-ctrl{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:34px;background:var(--bg2,#0b0e13);border:none;border-left:1px solid #20262f;color:var(--ink);font-size:13px;line-height:1;cursor:pointer;transition:background .15s,color .15s}
.ticker .tk-ctrl:hover{background:#161b22;color:var(--ink,#fff)}
.ticker.is-stopped #tkCtrl{color:var(--acc)}
/* dismissed state: collapse to a clickable "show" strip — always restorable (no one-way door) */
.ticker.is-dismissed .vp,.ticker.is-dismissed #tkCtrl,.ticker.is-dismissed #tkClose{display:none}
.ticker.is-dismissed{cursor:pointer}
.ticker.is-dismissed .lbl{flex:1;justify-content:flex-start;background:var(--bg2,#0b0e13);color:var(--muted,#8b949e);font-weight:700}
.ticker.is-dismissed .lbl::before{background:var(--acc)}
.ticker.is-dismissed .lbl::after{content:"▸ show";margin-left:8px;color:var(--blue);font-weight:800}
.ticker .it{display:inline-flex;align-items:center;gap:8px;padding:0 20px;font-size:12px;color:var(--ink);border-right:1px solid #20262f}
.ticker .it .t{font-weight:800;font-size:9px;letter-spacing:.4px;padding:2px 6px;border-radius:5px}
.ticker .it .t.listed{background:rgba(63,185,80,.18);color:var(--acc)}
.ticker .it .t.closed{background:rgba(255,166,0,.16);color:var(--gold)}
@keyframes tick{from{transform:translateX(0)}to{transform:translateX(-50%)}}
@media (prefers-reduced-motion:reduce){.ticker .track{animation:none}}
/* ── personalized welcome strip ── */
.persona{background:linear-gradient(120deg,rgba(63,185,80,.10),rgba(88,166,255,.06));border:1px solid var(--line);border-radius:12px;padding:12px 15px;margin-bottom:13px;display:flex;flex-wrap:wrap;gap:11px 14px;align-items:center}
.persona h2{font-size:15px;margin:0;color:var(--ink,#fff);font-weight:600}
.persona .role{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--acc);border:1px solid rgba(63,185,80,.35);border-radius:20px;padding:2px 9px;white-space:nowrap}
.persona .tip{flex-basis:100%;font-size:11.5px;color:var(--mut);margin:-2px 0 0}
.persona .qa{display:flex;flex-wrap:wrap;gap:8px;margin-left:auto}
.persona .qa a{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:7px 11px;font-size:12px;color:var(--blue);text-decoration:none;white-space:nowrap}
.persona .qa a:hover{border-color:var(--blue)}
.persona .edit{font-size:11px;color:var(--mut);cursor:pointer;text-decoration:underline}
/* ── modal (sign-in + role picker) ── */
.modal{position:fixed;inset:0;background:rgba(3,6,10,.68);display:none;align-items:center;justify-content:center;z-index:120}
.modal.on{display:flex}
.modal .box{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:21px 23px;width:350px;max-width:92vw}
.modal h3{margin:0 0 4px;font-size:17px;color:var(--ink,#fff)}
.modal p{margin:0 0 6px;font-size:12px;color:var(--mut);line-height:1.5}
.modal label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--mut);margin:12px 0 4px}
.modal input{width:100%;background:var(--card,#0e1116);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:9px 11px;font-size:13px}
.modal .btn{width:100%;margin-top:16px;background:var(--acc);color:#0b0e13;border:0;border-radius:8px;padding:10px;font-size:13px;font-weight:700;cursor:pointer}
.modal .err{color:#ff7b72;font-size:12px;margin-top:10px;min-height:15px}
.modal .alt{font-size:11px;color:var(--blue);cursor:pointer;margin-top:13px;display:inline-block}
.roleopts{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}
.roleopt{border:1px solid var(--line);border-radius:10px;padding:11px 9px;font-size:12px;cursor:pointer;background:var(--card,#0e1116);text-align:center;line-height:1.3}
.roleopt:hover{border-color:var(--blue)}
.roleopt.sel{border-color:var(--acc);background:rgba(63,185,80,.12)}
.roleopt b{display:block;color:var(--ink,#fff);font-size:12.5px}
.roleopt span{font-size:10px;color:var(--mut)}
.hide{display:none!important}
.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 11px;font-size:12px;cursor:pointer}
.viewseg button:last-child{border-right:0}.viewseg button.on{background:var(--blue);color:#0e1116;font-weight:600}
.dftable{overflow:auto;margin-top:6px}
.dftable table{width:100%;border-collapse:collapse;font-size:12.5px}
.dftable th{text-align:left;color:var(--mut);text-transform:uppercase;font-size:10px;letter-spacing:.4px;padding:7px 9px;border-bottom:1px solid var(--line);white-space:nowrap;position:sticky;top:0;background:var(--card)}
.dftable td{padding:7px 9px;border-bottom:1px solid var(--line)}
.dftable tr:hover td{background:rgba(120,160,255,.06)}
.lst{display:flex;flex-direction:column;margin-top:6px}
.lrow{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;padding:10px 6px;border-bottom:1px solid var(--line);cursor:pointer}
.lrow:hover{background:rgba(120,160,255,.06)}
.lnm{font-weight:700;min-width:210px;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;color:var(--ink)}
.lk{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;margin-right:5px}
</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 -->
  <script src="/infinite-scroll.js"></script><!-- windowed infinite scroll -->
</head><body>
<header>
  <h1><b>Closed Deal Flow</b> · LA Commercial</h1>
  <span class="badge">100% public record</span>
  <div class="nav"><a href="/just-listed.html">Just Listed</a><a href="/fha-leads.html">Assumable Leads</a><a href="/mls.html">Listings</a><a href="/property">🔍 Property Lookup</a><a href="/">Explorer</a></div>
  <span id="auth" style="margin-left:auto;font-size:12px;color:var(--blue);cursor:pointer">Sign in</span>
</header>
<div class="ticker" id="ticker" style="display:none" aria-label="Breaking real estate headlines"><div class="lbl" id="tkLbl">Breaking</div><div class="vp"><div class="track" id="tickTrack"></div></div><button class="tk-ctrl" id="tkCtrl" type="button" aria-pressed="false" aria-label="Stop ticker" title="Stop ticker to read">⏸</button><button class="tk-ctrl" id="tkClose" type="button" aria-label="Hide ticker" title="Hide ticker">✕</button></div>
<div class="shell">
<aside class="rail">
  <div class="rsec" id="savedSec" style="display:none"><h4>My saved searches</h4><div class="chips" id="fSaved"></div></div>
  <div class="rsec"><h4>Source</h4><div class="chips" id="fSrc"></div></div>
  <div class="rsec"><h4>Property type</h4><div class="chips" id="fUse"></div></div>
  <div class="rsec"><h4>Transaction year</h4><div class="chips" id="fYear"></div></div>
  <div class="rsec"><h4>Confidence</h4><div class="chips" id="fConf"></div></div>
  <div class="rsec"><h4>Min price $</h4><input type="number" id="pMin" placeholder="e.g. 5000000" step="1000000"></div>
  <div class="rsec"><h4>Min units</h4><input type="number" id="uMin" placeholder="e.g. 20"></div>
</aside>
<div class="main">
  <div class="persona" id="persona" style="display:none"></div>
  <div id="stats"></div>
  <div class="prov">Every card shows its <b>source</b> (🏛️ LA County Assessor · 📄 SEC EDGAR) — all public-record, resellable, $0. County price = Prop-13 reassessment estimate (assessed value de-escalated to transfer year ≈ sale price). SEC cards are REIT filings that mention an acquisition (price/property pending parse — not yet extracted). County "est" price is an ESTIMATE (assessed value ÷ 1.02^yrs), not a recorded sale price; parcels with a building built after the transfer are excluded. <b style="color:var(--gold)">Resale rights pending legal review</b> (CA R&T §408.3(c) restricts commercial-solicitation use of assessor bulk data) — do not resell until cleared.</div>
  <div class="toolbar">
    <input class="search" id="q" placeholder="search address, company, use, year…">
    <div class="viewseg" id="viewseg"><button data-v="grid" class="on">▦ Grid</button><button data-v="list">☰ List</button><button data-v="table">▤ Table</button></div>
    <select id="sort"></select>
    <label style="font-size:11px;color:var(--mut)">density <input type="range" id="dens" min="2" max="7" step="1"></label>
    <button id="saveBtn" title="Save this filter set — get alerts on new matching deals" style="background:var(--card);border:1px solid var(--line);color:var(--acc);border-radius:8px;padding:7px 10px;font-size:12px;cursor:pointer">★ Save search</button>
    <button id="watchBtn" title="Show only starred deals" style="background:var(--card);border:1px solid var(--line);color:var(--gold);border-radius:8px;padding:7px 10px;font-size:12px;cursor:pointer">☆ Watchlist</button>
    <span class="count" id="count"></span>
  </div>
  <div class="toolbar" style="margin-top:4px;border-top:1px solid var(--line);padding-top:10px">
    <span style="font-size:10px;font-weight:700;letter-spacing:.5px;color:var(--blue);text-transform:uppercase;white-space:nowrap" title="Look up any LA parcel — separate from the deal-grid filter above">🔍 Property lookup</span>
    <input class="search" id="propLookup" placeholder="Any LA address → public-record dossier (assessor + recent sales)" style="flex:1;border-color:var(--blue)" title="Enter a street address; opens the public-record dossier for that parcel — this does NOT filter the grid above" aria-label="Look up any LA property by address">
    <button id="propLookupBtn" style="background:var(--card);border:1px solid var(--blue);color:var(--blue);border-radius:8px;padding:7px 14px;font-size:12px;cursor:pointer">Look up →</button>
  </div>
  <div class="grid" id="grid"></div>
  <div id="tableView" class="dftable hide"><table><thead id="dfthead"></thead><tbody id="dftbody"></tbody></table></div>
  <div id="listView" class="lst hide"></div>
</div></div>

<!-- property-detail drill (public-record dossier) -->
<div class="modal" id="propModal" role="dialog" aria-modal="true" aria-label="Property detail">
  <div class="box" style="max-width:560px;width:100%;max-height:82vh;overflow:auto">
    <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><h3 style="margin:0;flex:1" id="pmTitle">Property detail</h3><span style="cursor:pointer;color:var(--mut);font-size:18px" onclick="document.getElementById('propModal').classList.remove('on')">✕</span></div>
    <div id="pmBody"><span style="color:var(--mut)">Loading…</span></div>
  </div>
</div>

<!-- sign-in modal (username + password) -->
<div class="modal" id="loginModal" role="dialog" aria-modal="true" aria-label="Sign in to CRCP">
  <div class="box">
    <h3>Sign in to CRCP</h3>
    <p>Your personal deal-flow — saved searches, watchlist, and a home tuned to your business.</p>
    <form id="loginForm">
      <label for="liUser">Username</label><input id="liUser" name="username" autocomplete="username" placeholder="frank" aria-label="Username">
      <label for="liPass">Password</label><input id="liPass" name="password" type="password" autocomplete="current-password" placeholder="••••••••" aria-label="Password">
      <div class="err" id="liErr" role="alert" aria-live="polite"></div>
      <button class="btn" id="liBtn" type="submit">Sign in</button>
    </form>
    <span class="alt" id="liMagic" role="button" tabindex="0">Prefer an email magic-link instead?</span>
  </div>
</div>

<!-- first-run industry / role picker -->
<div class="modal" id="roleModal" role="dialog" aria-modal="true" aria-label="Choose your industry">
  <div class="box">
    <h3>What's your business?</h3>
    <p>We'll tune your home page to how you actually work the market.</p>
    <div class="roleopts" id="roleOpts"></div>
    <div class="err" id="roleErr" role="alert" aria-live="polite"></div>
    <button class="btn" id="roleBtn" style="opacity:.5;pointer-events:none">Personalize my CRCP</button>
    <span class="alt" id="roleSkip" role="button" tabindex="0">Skip for now</span>
  </div>
</div>

<script>
const $=s=>document.querySelector(s);
const esc=s=>(s==null?'':String(s)).replace(/&/g,'&amp;').replace(/</g,'&lt;');
const money=n=>n==null||isNaN(+n)?null:'$'+Math.round(+n).toLocaleString();
let DATA=[],q='',sortKey='date',sortDir=-1,view=(localStorage.getItem('dfView')||'grid');
// columns for the table + list views (the grid keeps its rich cards)
const DCOLS=[{k:'name',l:'Deal / Property'},{k:'price',l:'Price',money:true},{k:'units',l:'Units',num:true},{k:'use',l:'Use'},{k:'deal_city',l:'City'},{k:'year',l:'Year'},{k:'source_label',l:'Source'},{k:'confidence',l:'Confidence'},{k:'holder',l:'Holder'}];
const dfmt=(r,c)=>{const v=r[c.k];if(c.money)return money(v)||'—';if(c.num)return v!=null&&v!==''?(+v).toLocaleString():'—';return v==null||v===''?'—':esc(String(v));};
let ME=null, WATCH=new Set(), watchOnly=false;
const dealKey=r=>String(r.ain||r.id||(r.kind+'|'+(r.name||'')+'|'+(r.date||'')));
let dens=(function(){const v=parseInt(localStorage.getItem('dfDens'),10);return(v>=2&&v<=7)?v:4;})();
// exact = ancillary drill fields (deal_city, year_built, holder, units, price, rate, below) that
// aren't rail chips but ARE drillable + URL-addressable. Each is a Set of exact string values.
const F={src:new Set(),use:new Set(),year:new Set(),conf:new Set(),pMin:null,uMin:null,exact:{}};
const tog=(s,v)=>{s.has(v)?s.delete(v):s.add(v);};
const EXACT_FIELDS=['deal_city','year_built','holder','units','price','rate','below']; // r-field getters below
const exactVal=(r,f)=>f==='below'?(r.belowMarket?'1':''):(r[f]==null?'':(f==='rate'?(+r.rate).toFixed(2):String(r[f])));
const togExact=(f,v)=>{const s=F.exact[f]||(F.exact[f]=new Set());s.has(v)?s.delete(v):s.add(v);if(!s.size)delete F.exact[f];};

function pass(r){
  if(watchOnly&&!WATCH.has(dealKey(r)))return false;
  if(F.src.size&&!F.src.has(r.source_label))return false;
  if(F.use.size&&!F.use.has(r.use||'—'))return false;
  if(F.year.size&&!F.year.has(String(r.year||'—')))return false;
  if(F.conf.size&&!F.conf.has(r.confidence||'—'))return false;
  if(F.pMin!=null&&!(+r.price>=F.pMin))return false;
  if(F.uMin!=null&&!(+r.units>=F.uMin))return false;
  for(const f of EXACT_FIELDS){const s=F.exact[f];if(s&&s.size&&!s.has(exactVal(r,f)))return false;}
  if(q){const t=q.toLowerCase();return [r.name,r.use,r.year,r.company,r.source_label].some(v=>v!=null&&String(v).toLowerCase().includes(t));}
  return true;
}
// Read filter state from the URL (?use=&year=&src=&conf=&deal_city=&year_built=&holder=&units=&price=&rate=&below=&pMin=&uMin=&q=)
// so every card drill-link is a real, shareable/bookmarkable view — applied on page load.
function readURLFilters(){
  const u=new URLSearchParams(location.search);let any=false;
  const setChips=(param,set)=>{const v=u.getAll(param);if(v.length){v.forEach(x=>set.add(x));any=true;}};
  setChips('use',F.use);setChips('year',F.year);setChips('src',F.src);setChips('conf',F.conf);
  for(const f of EXACT_FIELDS){const v=u.getAll(f);if(v.length){F.exact[f]=new Set(v);any=true;}}
  if(u.get('pMin')){F.pMin=+u.get('pMin');any=true;}
  if(u.get('uMin')){F.uMin=+u.get('uMin');any=true;}
  if(u.get('q')){q=u.get('q');const qi=$('#q');if(qi)qi.value=q;any=true;}
  return any;
}
function filtered(){
  let rows=DATA.filter(pass);
  rows=rows.slice().sort((a,b)=>{let x=a[sortKey],y=b[sortKey];const xm=(x==null||x===''),ym=(y==null||y==='');
    if(xm&&ym)return 0;if(xm)return 1;if(ym)return -1;
    if(!isNaN(+x)&&!isNaN(+y)){x=+x;y=+y;}else{x=String(x).toLowerCase();y=String(y).toLowerCase();}
    return x<y?-1*sortDir:x>y?sortDir:0;});
  return rows;
}
function chip(obj,attr,lim){return Object.entries(obj).sort((a,b)=>b[1]-a[1]).slice(0,lim||99).map(([k,c])=>`<span class="chip" data-${attr}="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');}
function buildRail(){
  const sc={},uc={},yc={},cc={};
  DATA.forEach(r=>{sc[r.source_label]=(sc[r.source_label]||0)+1; const u=r.use||'—';uc[u]=(uc[u]||0)+1; const y=String(r.year||'—');yc[y]=(yc[y]||0)+1; const cf=r.confidence||'—';cc[cf]=(cc[cf]||0)+1;});
  $('#fSrc').innerHTML=chip(sc,'s');$('#fUse').innerHTML=chip(uc,'u',10);$('#fYear').innerHTML=chip(yc,'y',8);$('#fConf').innerHTML=chip(cc,'c');
}
// href-drill atom: a card data point that SETS an in-memory filter (delegated click) AND is a real,
// shareable href (/deals-flow.html?field=value → readURLFilters applies it on load). No dead-ends.
function drill(field,val,inner,extra){
  if(val==null||val==='')return '';
  const href='/deals-flow.html?'+field+'='+encodeURIComponent(val);
  return `<span class="i" data-drillf="${esc(field)}" data-drillv="${esc(val)}"><a class="ddrill" href="${href}" title="See all where ${esc(field)} = ${esc(val)}"${extra||''}>${inner}</a></span>`;
}
function card(r){
  const isE=r.kind==='reit-filing', isF=r.kind==='fha', isC=r.kind==='county-sale', isU=r.kind==='ula';
  // Source badge → filters by source (delegated), and is a real shareable href.
  const srcBadge=`<a class="src ${isE?'edgar':isF?'fha':isU?'ula':'county'} srcdrill" href="/deals-flow.html?src=${encodeURIComponent(r.source_label)}" data-fs="${esc(r.source_label)}" style="cursor:pointer;text-decoration:none" title="Filter to ${esc(r.source_label)}">${isE?'📄 SEC EDGAR':isF?'🏦 HUD FHA':isU?'🏙️ LA City ULA':'🏛️ Assessor'}</a>`;
  const price=money(r.price);
  const details=[];
  // Every data point drills: numeric/text values link to their filtered slice, or their deeper record.
  if(price&&isE) details.push(drill('price',r.price,`💰 <b>${price}</b>`));
  if(r.rate!=null) details.push(drill('rate',(+r.rate).toFixed(2),`💵 <b>${(+r.rate).toFixed(2)}%</b> rate`));
  if(isF&&r.belowMarket) details.push(drill('below','1',`<span style="color:var(--gold)">◆ assumable (below-market)</span>`));
  if(r.units) details.push(drill('units',r.units,`🏢 <b>${r.units}</b> units`));
  if(r.use) details.push(drill('use',r.use,`🏷️ ${esc(r.use)}`));
  if(r.deal_city) details.push(drill('deal_city',r.deal_city,`📍 ${esc(r.deal_city)}`));
  if(r.year_built) details.push(drill('year_built',r.year_built,`🏗️ built ${r.year_built}`));
  if(r.date) details.push(drill('year',r.year,`📅 ${esc(r.date)}`));
  // ticker → external SEC EDGAR company filings (public); holder → filter by holder.
  if(r.ticker) details.push(`<span class="i">📈 <a class="f" href="https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&company=${encodeURIComponent(r.ticker)}&type=8-K" target="_blank" rel="noopener noreferrer" title="SEC EDGAR filings for ${esc(r.ticker)}">${esc(r.ticker)} ↗</a></span>`);
  if(r.holder) details.push(drill('holder',r.holder,`🏦 ${esc(String(r.holder).slice(0,24))}`));
  const conf=r.confidence?`<a class="conf ${r.confidence} confdrill" href="/deals-flow.html?conf=${esc(r.confidence)}" data-fc="${esc(r.confidence)}" style="text-decoration:none;cursor:pointer" title="Filter to ${esc(r.confidence)} confidence">${r.confidence} confidence</a>`:'';
  const link=r.url?`<a class="f" href="${esc(r.url)}" target="_blank" rel="noopener noreferrer">🔗 SEC filing ↗</a>`:'';
  const bigPrice=(price&&(isC||isF))?`<div class="price">${price}${isC?' <span class="mut" style="font-size:11px">est sale</span>':isF?' <span class="mut" style="font-size:11px">loan</span>':''}</div>`:'';
  // href-drill on the TITLE: county cards → the full parcel/deal DETAIL page (/parcel/<AIN>).
  // EDGAR keeps its filing link; FHA/ULA (no AIN) keep a plain title.
  const ainDigits=r.ain?String(r.ain).replace(/\D/g,''):'';
  const nameHtml=(isC&&ainDigits)?`<a class="name" href="/parcel/${ainDigits}" title="Full parcel/deal detail — assessor + ULA + deed + lending angle (AIN ${ainDigits})">${esc(r.name)}</a>`
    :(isE&&r.url)?`<a class="name" href="${esc(r.url)}" target="_blank" rel="noopener noreferrer" title="Open the SEC filing ↗">${esc(r.name)}</a>`
    :`<div class="name">${esc(r.name)}</div>`;
  // Explicit "detail →" affordance so the drill is discoverable, not just on the title.
  const detailLink=(isC&&ainDigits)?`<a class="f" href="/parcel/${ainDigits}" title="Full parcel/deal detail page">🔍 parcel detail →</a>`:'';
  return `<div class="dcard">
    <div class="top">${nameHtml}<div style="display:flex;gap:6px;align-items:center;flex-shrink:0">${srcBadge}<button class="starbtn" data-k="${esc(dealKey(r))}" title="Star / watchlist" style="background:none;border:0;font-size:15px;line-height:1;cursor:pointer;color:${WATCH.has(dealKey(r))?'var(--gold)':'var(--mut)'}">${WATCH.has(dealKey(r))?'★':'☆'}</button></div></div>
    ${bigPrice}
    <div class="dl">${details.join('')}</div>
    ${detailLink}${link?(detailLink?' · ':'')+link:''}${conf?'<div>'+conf+'</div>':''}
    <div class="provline">📋 ${esc(r.source)}</div>
  </div>`;
}
// Infinite scroll: paint only the windowed rows into the active view; the helper reveals
// more as you scroll (replaces the old hard rows.slice(0,600) cap).
const _EMPTY='<div style="color:var(--mut);padding:40px">No deals match.</div>';
function activeDealMount(){ return view==='grid'?$('#grid'):view==='list'?$('#listView'):$('#tableView'); }
function paintDealRows(win){
  const g=$('#grid'),lv=$('#listView');
  if(view==='grid'){
    g.innerHTML=win.map(card).join('')||_EMPTY;
  } else if(view==='list' && lv){
    lv.innerHTML=win.map(r=>`<div class="lrow drow" data-k="${esc(dealKey(r))}" style="cursor:pointer"><div class="lnm">${esc(r.name||'—')}</div><div class="lmeta">`+DCOLS.slice(1).map(c=>`<span class="lm"><span class="lk">${esc(c.l)}</span>${dfmt(r,c)}</span>`).join('')+`</div></div>`).join('')||_EMPTY;
  } else {
    $('#dfthead').innerHTML='<tr>'+DCOLS.map(c=>`<th data-k="${c.k}" style="cursor:pointer" title="Click to sort">${esc(c.l)}${sortKey===c.k?(sortDir<0?' ↓':' ↑'):''}</th>`).join('')+'</tr>';
    $('#dftbody').innerHTML=win.map(r=>`<tr class="drow" data-k="${esc(dealKey(r))}" style="cursor:pointer">`+DCOLS.map(c=>`<td>${dfmt(r,c)}</td>`).join('')+`</tr>`).join('')||`<tr><td colspan="${DCOLS.length}" style="color:var(--mut);padding:30px">No deals match.</td></tr>`;
  }
}
let _dealInf=null;
function render(){
  document.documentElement.style.setProperty('--cols',dens);
  const rows=filtered();
  const county=rows.filter(r=>r.kind!=='reit-filing');
  const totalVal=county.reduce((s,r)=>s+(+r.price||0),0);
  $('#stats').innerHTML=
    `<div class="stat"><b>${rows.length.toLocaleString()}</b><span>deals shown</span></div>`+
    `<div class="stat"><b style="color:var(--gold)">$${(totalVal/1e9).toFixed(2)}B</b><span>est. value (assessed-derived)</span></div>`+
    `<div class="stat"><b style="color:var(--violet)">${rows.filter(r=>r.kind==='reit-filing').length}</b><span>SEC REIT filings</span></div>`+
    `<div class="stat"><b>${new Set(rows.map(r=>r.year)).size}</b><span>years</span></div>`;
  $('#count').textContent=`${rows.length.toLocaleString()} of ${DATA.length.toLocaleString()}`;
  render._rows=rows;
  const g=$('#grid'),tv=$('#tableView'),lv=$('#listView');
  g.classList.toggle('hide',view!=='grid');
  if(tv) tv.classList.toggle('hide',view!=='table');
  if(lv) lv.classList.toggle('hide',view!=='list');
  // Windowed infinite scroll (replaces the old rows.slice(0,600) cap): reveal a batch,
  // load more as the sentinel scrolls into view. reset() repaints the active view from the top.
  if(!_dealInf) _dealInf=InfScroll.attach({batch:150,getRows:()=>render._rows,getMount:activeDealMount,render:paintDealRows});
  _dealInf.reset();
}
$('#fSrc').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.src,c.dataset.s);c.classList.toggle('active');writeURLFilters(true);render();});
$('#fUse').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.use,c.dataset.u);c.classList.toggle('active');writeURLFilters(true);render();});
$('#fYear').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.year,c.dataset.y);c.classList.toggle('active');writeURLFilters(true);render();});
$('#fConf').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.conf,c.dataset.c);c.classList.toggle('active');writeURLFilters(true);render();});
$('#pMin').addEventListener('input',()=>{F.pMin=$('#pMin').value?+$('#pMin').value:null;writeURLFilters();render();});
$('#uMin').addEventListener('input',()=>{F.uMin=$('#uMin').value?+$('#uMin').value:null;writeURLFilters();render();});
$('#q').addEventListener('input',()=>{q=$('#q').value.trim();writeURLFilters();render();});
$('#sort').addEventListener('change',()=>{sortKey=$('#sort').value;sortDir=(sortKey==='name'||sortKey==='use')?1:-1;render();});
// grid/list/table view toggle
document.querySelectorAll('#viewseg button').forEach(x=>x.classList.toggle('on',x.dataset.v===view));
$('#viewseg').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;document.querySelectorAll('#viewseg button').forEach(x=>x.classList.toggle('on',x===b));view=b.dataset.v;localStorage.setItem('dfView',view);render();});
// table header → sort on any deal field; row → open parcel/filing detail (delegated so it survives re-render)
document.addEventListener('click',e=>{
  const th=e.target.closest('#dfthead th'); if(th){const k=th.dataset.k;if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=(k==='name'||k==='use'||k==='deal_city'||k==='source_label'||k==='confidence')?1:-1;}const so=$('#sort');if(so&&Array.from(so.options).some(o=>o.value===k))so.value=k;render();return;}
  const row=e.target.closest('.drow'); if(row && !e.target.closest('a,button')){const r=(render._rows||[]).find(x=>dealKey(x)===row.dataset.k);if(r){const ain=r.ain?String(r.ain).replace(/\D/g,''):'';if(ain)location.href='/parcel/'+ain;else if(r.url)window.open(r.url,'_blank','noopener');}}
});
$('#dens').addEventListener('input',()=>{dens=+$('#dens').value;localStorage.setItem('dfDens',dens);render();});
$('#sort').innerHTML=[['date','Date'],['price','Price'],['units','Units'],['year','Year'],['name','Name']].map(([k,l])=>`<option value="${k}"${k==='date'?' selected':''}>Sort: ${l}</option>`).join('');
$('#dens').value=dens;
// ── P1 subscription layer: accounts + saved searches + watchlist ──
const api=(u,o)=>fetch(u,Object.assign({headers:{'Content-Type':'application/json'}},o||{})).then(r=>r.json());
// role → home personalization. Each role surfaces the pages that role actually works from.
const ROLE_META={
  loan_officer:{label:'Loan Officer',tip:'Your calls: listing agents on fresh deals, plus non-warrantable & FHA condo products.',qa:[['📇 Listing agents to call','/brokers.html'],['🏢 Non-warrantable / condos','/condos.html'],['🏦 FHA / assumable loans','/fha-loans.html'],['◆ Assumable leads','/fha-leads.html']]},
  listing_agent:{label:'Listing Agent',tip:'Fresh comps and who’s buying in your farm.',qa:[['🏷️ Active listings','/mls.html'],['📊 Closed comps','/deals-flow.html'],['🤝 Buyers & brokers','/brokers.html']]},
  buyers_agent:{label:'Buyer’s Agent',tip:'What’s listed, what closed, and the rent story behind it.',qa:[['🏷️ Active listings','/mls.html'],['📊 Closed deal flow','/deals-flow.html'],['📑 Rent rolls','/rent-rolls.html']]},
  investor:{label:'Investor',tip:'Deal flow, rent upside, and assumable financing in one view.',qa:[['📊 Deal flow','/deals-flow.html'],['📑 Rent rolls','/rent-rolls.html'],['🏦 Assumable financing','/fha-loans.html']]},
  appraiser:{label:'Appraiser',tip:'Closed comps and assessor values for your workups.',qa:[['📊 Closed comps','/deals-flow.html'],['💵 Sales','/sales.html'],['🏛️ Assessor explorer','/']]},
  other:{label:'Member',tip:'',qa:[['📊 Deal flow','/deals-flow.html'],['🏷️ Listings','/mls.html']]}
};
const ROLE_OPTS=[['loan_officer','Loan Officer','mortgage / lending'],['listing_agent','Listing Agent','brokerage — listing'],['buyers_agent','Buyer’s Agent','brokerage — buy-side'],['investor','Investor','principal / capital'],['appraiser','Appraiser','valuation'],['other','Something else','']];
function renderAuth(){const el=$('#auth');if(ME&&ME.email){const who=esc(ME.name||ME.username||ME.email);const rl=ME.role&&ROLE_META[ME.role]?ROLE_META[ME.role].label:'';const co=ME.company?` · <span style="color:var(--gold)">${esc(ME.company)}</span>`:'';el.innerHTML=`<b style="color:var(--ink,#fff)">${who}</b>${co}${rl?` · <span style="color:var(--acc)">${esc(rl)}</span>`:''}${ME.perm==='admin'?` · <a href="/admin.html" style="color:var(--gold)">Admin</a>`:''} · <a id="signOutLink" style="color:var(--blue);cursor:pointer">sign out</a>`;el.querySelector('#signOutLink').onclick=async()=>{await fetch('/auth/logout',{method:'POST'});location.reload();};el.onclick=null;}else{el.textContent='Sign in';el.onclick=openLogin;}}
function renderPersona(){const p=$('#persona');if(!(ME&&ME.email&&ME.role)){p.style.display='none';return;}const m=ROLE_META[ME.role]||ROLE_META.other;const who=esc(ME.name||ME.username||'there');p.style.display='';p.innerHTML=`<h2>Welcome back, ${who}</h2><span class="role">${esc(m.label)}</span><div class="qa">${m.qa.map(([t,h])=>`<a href="${h}">${t}</a>`).join('')}</div>${m.tip?`<p class="tip">${esc(m.tip)}</p>`:''}<span class="edit" id="editRole">change role</span>`;const er=$('#editRole');if(er)er.onclick=openRole;}
async function loadMe(){try{ME=await api('/api/me');}catch(e){ME=null;}renderAuth();renderPersona();$('#savedSec').style.display=(ME&&ME.email)?'':'none';if(ME&&ME.email){await loadSaved();loadWatch();maybeApplyDefault();if(!ME.role)openRole();}}
// ── sign-in modal (username + password) ──
function openLogin(){$('#liErr').textContent='';$('#liUser').value='';$('#liPass').value='';$('#loginModal').classList.add('on');setTimeout(()=>$('#liUser').focus(),40);}
const signIn=openLogin;   // back-compat: save/watch/star gates call signIn() to prompt sign-in
function closeLogin(){$('#loginModal').classList.remove('on');}
async function doLogin(){const username=$('#liUser').value.trim(),password=$('#liPass').value;if(!username||!password){$('#liErr').textContent='Enter username and password.';return;}$('#liErr').textContent='…';const r=await api('/auth/login',{method:'POST',body:JSON.stringify({username,password})}).catch(()=>null);if(r&&r.ok){closeLogin();location.reload();}else{$('#liErr').textContent=(r&&r.error)||'Sign-in failed.';}}
$('#loginForm').addEventListener('submit',e=>{e.preventDefault();doLogin();});
$('#liMagic').onclick=magicSignIn;
$('#liMagic').addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();magicSignIn();}});
$('#loginModal').addEventListener('click',e=>{if(e.target.id==='loginModal')closeLogin();});
document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeLogin();closeRole();const pm=document.getElementById('propModal');if(pm)pm.classList.remove('on');}});
// magic-link fallback (kept for email accounts)
async function magicSignIn(){const email=prompt('Email for a magic-link sign-in:');if(!email)return;const r=await api('/auth/request',{method:'POST',body:JSON.stringify({email})});if(r.magic_link){if(confirm('Dev mode — open your magic-link to sign in now?'))location.href=r.magic_link;}else alert('Check your email for a sign-in link.');}
// ── first-run industry / role picker ──
let _roleSel=null;
function openRole(){_roleSel=(ME&&ME.role)||null;$('#roleErr').textContent='';$('#roleOpts').innerHTML=ROLE_OPTS.map(([k,l,s])=>`<div class="roleopt${k===_roleSel?' sel':''}" data-role="${k}"><b>${l}</b>${s?`<span>${s}</span>`:''}</div>`).join('');const btn=$('#roleBtn');const sync=()=>{btn.style.opacity=_roleSel?'1':'.5';btn.style.pointerEvents=_roleSel?'':'none';};sync();$('#roleOpts').querySelectorAll('.roleopt').forEach(el=>el.onclick=()=>{_roleSel=el.dataset.role;$('#roleOpts').querySelectorAll('.roleopt').forEach(x=>x.classList.toggle('sel',x===el));sync();});$('#roleModal').classList.add('on');}
function closeRole(){$('#roleModal').classList.remove('on');}
async function saveRole(){if(!_roleSel)return;const r=await api('/auth/profile',{method:'POST',body:JSON.stringify({role:_roleSel})}).catch(()=>null);if(r&&r.ok){ME=r.me;closeRole();renderAuth();renderPersona();}else{$('#roleErr').textContent=(r&&r.error)||'Could not save.';}}
$('#roleBtn').onclick=saveRole;
$('#roleSkip').onclick=closeRole;
$('#roleSkip').addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();closeRole();}});
$('#roleModal').addEventListener('click',e=>{if(e.target.id==='roleModal')closeRole();});
// ── breaking-news ticker ──
async function loadTicker(){try{const r=await api('/api/ticker');const items=(r&&r.items)||[];if(!items.length)return;const html=items.map(i=>`<span class="it"><span class="t ${i.kind}">${esc(i.tag)}</span>${esc(i.text)}</span>`).join('');const dup=html.replace(/<span class="it">/g,'<span class="it" aria-hidden="true">');const track=$('#tickTrack');track.innerHTML=html+dup;$('#ticker').style.display='';
  // constant, comfortable read speed (~45px/s) regardless of how many deals loaded
  requestAnimationFrame(()=>{const oneSet=track.scrollWidth/2;const dur=Math.max(60,Math.round(oneSet/45));track.style.animationDuration=dur+'s';});
  }catch(e){/* ticker is non-critical — never let it break the page */}
}
loadTicker();
// ── stop / resume + dismiss (deselect) controls ──
(function(){const t=$('#ticker'),b=$('#tkCtrl'),x=$('#tkClose'),lbl=$('#tkLbl');if(!t)return;
  // stop / resume (freezes in place so it can be read)
  if(b)b.addEventListener('click',()=>{const stopped=t.classList.toggle('is-stopped');b.textContent=stopped?'▶':'⏸';b.setAttribute('aria-pressed',stopped?'true':'false');b.setAttribute('aria-label',stopped?'Resume ticker':'Stop ticker');b.title=stopped?'Resume ticker':'Stop ticker to read';});
  // dismiss (hide) — collapses to a clickable "show" strip; session-scoped so it always returns next visit
  function setHidden(hide){t.classList.toggle('is-dismissed',hide);try{hide?sessionStorage.setItem('crcpTickerHidden','1'):sessionStorage.removeItem('crcpTickerHidden');}catch(e){}}
  if(x)x.addEventListener('click',()=>setHidden(true));
  if(lbl)lbl.addEventListener('click',()=>{if(t.classList.contains('is-dismissed'))setHidden(false);});
  try{if(sessionStorage.getItem('crcpTickerHidden')==='1')t.classList.add('is-dismissed');}catch(e){}
})();
async function loadSaved(){const r=await api('/api/saved-searches');const s=r.saved||[];window._saved=s;$('#fSaved').innerHTML=s.length?s.map(x=>`<span class="chip${x.is_default?' active':''}" data-sid="${x.id}" title="${x.is_default?'default — auto-applied on load · click to apply':'apply this search'}">${x.is_default?'★ ':''}${esc(x.name)}<span class="ct" data-del="${x.id}" title="delete">✕</span></span>`).join(''):'<span style="font-size:11px;color:var(--mut)">none yet — set filters, then ★ Save search</span>';}
async function loadWatch(){const r=await api('/api/watchlist');WATCH=new Set(r.ids||[]);render();}
$('#saveBtn').addEventListener('click',async()=>{if(!ME||!ME.email){signIn();return;}const name=prompt('Name this saved search:','My deal filter');if(name===null)return;const filters={src:[...F.src],use:[...F.use],year:[...F.year],conf:[...F.conf],pMin:F.pMin,uMin:F.uMin,q};await api('/api/saved-searches',{method:'POST',body:JSON.stringify({name,filters})});loadSaved();});
$('#watchBtn').addEventListener('click',()=>{if(!ME||!ME.email){signIn();return;}watchOnly=!watchOnly;$('#watchBtn').style.borderColor=watchOnly?'var(--gold)':'var(--line)';render();});
$('#fSaved').addEventListener('click',async e=>{const del=e.target.closest('[data-del]');if(del){await fetch('/api/saved-searches/'+del.dataset.del,{method:'DELETE'});loadSaved();return;}const c=e.target.closest('[data-sid]');if(!c)return;const s=(window._saved||[]).find(x=>x.id===c.dataset.sid);if(!s)return;window._defaultApplied=true;applySaved(s);});
// Apply a saved search's filters into the live rail state (chips + min-inputs + URL). Shared by the
// saved-chip click AND the on-load default (Steve 2026-08-19: Frank's "4-Unit Multifamily" default).
function applySaved(s){const f=(s&&s.filters)||{};F.src=new Set(f.src||[]);F.use=new Set(f.use||[]);F.year=new Set(f.year||[]);F.conf=new Set(f.conf||[]);F.exact={};F.pMin=(f.pMin==null?null:+f.pMin);F.uMin=(f.uMin==null?null:+f.uMin);q=f.q||'';const qi=$('#q');if(qi)qi.value=q;const um=$('#uMin');if(um)um.value=(f.uMin!=null?f.uMin:'');const pm=$('#pMin');if(pm)pm.value=(f.pMin!=null?f.pMin:'');syncRailActive('s',F.src);syncRailActive('u',F.use);syncRailActive('y',F.year);syncRailActive('c',F.conf);writeURLFilters();render();}
function filtersEmpty(){return !F.src.size&&!F.use.size&&!F.year.size&&!F.conf.size&&!Object.keys(F.exact).length&&F.pMin==null&&F.uMin==null&&!q;}
// On a fresh load with no URL filters, auto-apply the user's default saved search (once).
function maybeApplyDefault(){if(window._defaultApplied||window._hadURLFilters)return;if(!filtersEmpty())return;const def=(window._saved||[]).find(x=>x.is_default);if(!def)return;window._defaultApplied=true;applySaved(def);}
// href-drill rule: card values drill to their filtered view (reuses the rail F filters + keeps rail chips in sync)
function syncRailActive(attr,set){const map={s:'#fSrc',u:'#fUse',y:'#fYear',c:'#fConf'},el=$(map[attr]);if(el)el.querySelectorAll('.chip').forEach(ch=>ch.classList.toggle('active',set.has(ch.dataset[attr])));}
// ── property-detail drill: fetch the free public-record dossier + render inline ──
let _pdSeq=0;   // guards against a stale fetch overwriting a newer one on rapid clicks (Cody Cycle-3)
// Render a dossier object (from /api/property/:ain or a resolved by-address hit) into the modal.
function renderDossier(d,fallbackTitle){
  const body=$('#pmBody'),title=$('#pmTitle'),p=d.parcel||{};
  const row=(k,v)=>(v||v===0)?`<div style="display:flex;gap:8px;padding:3px 0;border-bottom:1px solid var(--line)"><span style="color:var(--mut);min-width:140px;flex-shrink:0">${esc(k)}</span><span>${esc(String(v))}</span></div>`:'';
  const dollar=v=>v?('$'+Number(v).toLocaleString()):'';
  title.textContent=p.address||fallbackTitle||'Property';
  let html='<div style="font-size:12.5px">'
    +row('AIN',p.ain)+row('Address',p.address)+row('Use',p.use_desc||p.use_type)+row('Year built',p.year_built)
    +row('Sq ft',p.sqft)+row('Units',p.units)+row('Beds / baths',(p.bedrooms||p.bathrooms)?((p.bedrooms||'?')+' / '+(p.bathrooms||'?')):'')
    +row('Assessed land',dollar(p.assessed_land))+row('Assessed improvement',dollar(p.assessed_improvement))+row('Assessed total',dollar(p.assessed_total))
    +row('Roll year',p.assessed_as_of)+row('Last transfer recorded',p.last_transfer_recorded);
  if(d.ula_zip_candidates&&d.ula_zip_candidates.length){
    html+=`<div style="margin-top:12px;font-weight:600;color:var(--gold)">Recent $5.3M+ sales in ZIP ${esc(p.zip||'')} (ULA · candidates)</div>`;
    d.ula_zip_candidates.slice(0,6).forEach(c=>{html+=`<div style="padding:3px 0;border-bottom:1px solid var(--line)">${esc(c.date||'')} · <b style="color:var(--gold)">$${Number(c.price).toLocaleString()}</b> · ${esc(c.use||'')}${c.instrument?` · <span class="mut">instr ${esc(c.instrument)}</span>`:''}</div>`;});
    if(d.ula_note)html+=`<div class="mut" style="font-size:11px;margin-top:4px">${esc(d.ula_note)}</div>`;
  }
  if(d.deed&&!d.deed.available){ html+=`<div style="margin-top:12px;font-weight:600">Buyer / seller (recorded deed)</div><div class="mut" style="font-size:11.5px">${esc(d.deed.reason)}</div>`; }
  html+=`<div class="mut" style="font-size:10.5px;margin-top:10px">Sources: ${esc((d.sources||[]).join(' · '))}</div></div>`;
  body.innerHTML=html;
}
async function openPropDetail(ain){
  const seq=++_pdSeq,body=$('#pmBody'),title=$('#pmTitle');
  title.textContent='Property '+ain; body.innerHTML='<span style="color:var(--mut)">Loading public records…</span>'; $('#propModal').classList.add('on');
  let d; try{ d=await api('/api/property/'+encodeURIComponent(ain)); }catch(err){ if(seq===_pdSeq)body.innerHTML='<span style="color:var(--bad,#f85149)">Lookup failed: '+esc(err.message)+'</span>'; return; }
  if(seq!==_pdSeq)return;
  if(!d||!d.found){ body.innerHTML='<span class="mut">No assessor record for AIN '+esc(ain)+'.</span>'; return; }
  renderDossier(d,'AIN '+ain);
}
// Look up ANY LA property by address → dossier (or a pick-list when multiple parcels match).
async function openPropDetailByAddress(qstr){
  const q=String(qstr||'').trim(); if(!q)return;
  const seq=++_pdSeq,body=$('#pmBody'),title=$('#pmTitle');
  title.textContent='Property lookup'; body.innerHTML='<span style="color:var(--mut)">Searching public records for “'+esc(q)+'”…</span>'; $('#propModal').classList.add('on');
  let d; try{ d=await api('/api/property/by-address?q='+encodeURIComponent(q)); }catch(err){ if(seq===_pdSeq)body.innerHTML='<span style="color:var(--bad,#f85149)">Lookup failed: '+esc(err.message)+'</span>'; return; }
  if(seq!==_pdSeq)return;
  if(!d||!d.found){ body.innerHTML='<span class="mut">No LA County parcel matched “'+esc(q)+'”. Try a house number + street (e.g. 4150 Madison Ave).</span>'; return; }
  if(d.multiple&&Array.isArray(d.candidates)){
    title.textContent='Matches for “'+q+'”';
    body.innerHTML='<div class="mut" style="margin-bottom:8px">'+d.candidates.length+' parcels matched — pick one:</div>'+d.candidates.map(c=>`<div class="pdpick" data-ain="${esc(String(c.ain||'').replace(/\D/g,''))}" style="cursor:pointer;padding:7px 9px;border:1px solid var(--line);border-radius:8px;margin-bottom:6px"><b>${esc(c.address||c.ain)}</b><div class="mut" style="font-size:11px">${esc(c.use_desc||c.use_type||'')} · AIN ${esc(c.ain)}</div></div>`).join('');
    return;
  }
  renderDossier(d,q);
}
$('#pmBody').addEventListener('click',e=>{const pk=e.target.closest('.pdpick');if(pk)openPropDetail(pk.dataset.ain);});
$('#propLookupBtn').addEventListener('click',()=>openPropDetailByAddress($('#propLookup').value));
$('#propLookup').addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();openPropDetailByAddress(e.target.value);}});
$('#propModal').addEventListener('click',e=>{if(e.target.id==='propModal')e.currentTarget.classList.remove('on');});
// keep the URL in sync with the in-memory filter state so an in-page drill is still shareable/back-able.
function writeURLFilters(push){
  const u=new URLSearchParams();
  F.use.forEach(v=>u.append('use',v));F.year.forEach(v=>u.append('year',v));F.src.forEach(v=>u.append('src',v));F.conf.forEach(v=>u.append('conf',v));
  for(const f of EXACT_FIELDS){const s=F.exact[f];if(s)s.forEach(v=>u.append(f,v));}
  if(F.pMin!=null)u.set('pMin',F.pMin);if(F.uMin!=null)u.set('uMin',F.uMin);if(q)u.set('q',q);
  const s=u.toString();history[push?'pushState':'replaceState']({},'',s?('?'+s):location.pathname);
}
$('#grid').addEventListener('click',async e=>{
  // let modified clicks (cmd/ctrl/middle/shift) follow the real href → open the deeper view in a new tab
  if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1)return;
  // Star button (must run before the drill catch since it has no href).
  const b=e.target.closest('.starbtn');
  if(b){e.preventDefault();if(!ME||!ME.email){signIn();return;}const r=await api('/api/watchlist',{method:'POST',body:JSON.stringify({id:b.dataset.k})});WATCH=new Set(r.ids||[]);render();return;}
  // Title / SEC / parcel-detail links: real navigations — let them happen (do NOT intercept).
  if(e.target.closest('a.name,a.f'))return;
  // Rail-chip drill fields (use/src/conf) — filter in place + keep rail chips + URL in sync.
  const fu=e.target.closest('[data-drillf]');
  if(fu){e.preventDefault();const f=fu.dataset.drillf,v=fu.dataset.drillv;
    if(f==='use'){tog(F.use,v);syncRailActive('u',F.use);}
    else if(EXACT_FIELDS.includes(f)){togExact(f,v);}
    else return; // unknown → let href navigate
    writeURLFilters(true);render();return;}
  const fs=e.target.closest('.srcdrill');if(fs){e.preventDefault();tog(F.src,fs.dataset.fs);syncRailActive('s',F.src);writeURLFilters(true);render();return;}
  const fc=e.target.closest('.confdrill');if(fc){e.preventDefault();tog(F.conf,fc.dataset.fc);syncRailActive('c',F.conf);writeURLFilters(true);render();return;}
});
// back/forward re-applies the URL filter state.
window.addEventListener('popstate',()=>{F.use.clear();F.year.clear();F.src.clear();F.conf.clear();F.exact={};F.pMin=F.uMin=null;q='';const qi=$('#q');if(qi)qi.value='';readURLFilters();syncRailActive('s',F.src);syncRailActive('u',F.use);syncRailActive('y',F.year);syncRailActive('c',F.conf);render();});
Promise.all([
  fetch('/data/deals-flow.json').then(r=>r.json()).catch(()=>({deals:[]})),
  fetch('/data/edgar-reit-deals.json').then(r=>r.json()).catch(()=>({deals:[]})),
  fetch('/data/fha-loans.json').then(r=>r.json()).catch(()=>({rows:[]})),
  // LA City Measure ULA transfer-tax sales — CURRENT (2025/2026) $5.3M+ LA City CRE deals with
  // exact prices, the near-real-time complement to the 1-2yr-lagged assessor roll. Already in grid
  // shape (see scripts/fetch-ula-deals.js), so it rides in with a minimal pass-through map.
  fetch('/data/ula-la-deals.json').then(r=>r.json()).catch(()=>({deals:[]}))
]).then(([county,edgar,fha,ula])=>{
  const c=(county.deals||[]).map(d=>({...d,kind:'county-sale',name:d.address||d.ain,source_label:'LA County Assessor'}));
  const e=(edgar.deals||[]).map(d=>({...d,kind:'reit-filing',name:d.company,
    use:d.property?('REIT: '+String(d.property).slice(0,44)):(d.price?'REIT acquisition':'REIT filing · price pending'),
    // Map the SEC filing date to `year` too — without this the current 2025/2026 REIT deals had a
    // date but no year, so they fell into the "—" (unknown-year) bucket instead of the 2025/2026
    // Transaction-Year chips (they were in the grid, just invisible to the year filter).
    deal_city:d.deal_city||null,date:d.filed,year:(parseInt(String(d.filed||'').slice(0,4))||null),url:d.filing_url,source_label:'SEC EDGAR'}));
  // FHA-financed properties as a co-source (city/zip only — rides alongside, not a per-parcel join).
  const f=(fha.rows||[]).filter(r=>r.state==='CA'&&r.rate!=null).map(d=>({
    kind:'fha',name:d.property,use:'FHA-financed multifamily',price:d.originalAmount,units:d.units,
    year:String(d.originationDate||'').slice(0,4)||null,date:d.originationDate,rate:d.rate,belowMarket:d.belowMarket,
    holder:d.holder,deal_city:d.city,source_label:'HUD FHA',
    source:'HUD/FHA Multifamily Insured Mortgages (public, resellable)'}));
  // ULA records already carry kind/name/use/price/date/year/deal_city/source_label — pass through.
  const u=(ula.deals||[]).map(d=>({...d,kind:d.kind||'ula',source_label:d.source_label||'LA City ULA'}));
  DATA=c.concat(e).concat(f).concat(u);
  buildRail();
  // Apply any URL filter state (from a shared/bookmarked card drill-link) before the first render,
  // then reflect it into the rail chips so the active filters are visible. Record whether the URL
  // carried filters so loadMe() knows whether to auto-apply the user's default saved search on load.
  window._hadURLFilters=readURLFilters();
  syncRailActive('s',F.src);syncRailActive('u',F.use);syncRailActive('y',F.year);syncRailActive('c',F.conf);
  render();loadMe();
}).catch(()=>{$('#count').textContent='failed to load deal data';});
</script><script src="/corner-nav.js" defer></script>
  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
  <script src="/filter-modal.js" defer></script><!-- responsive filters: collapsed rail (desktop) / modal (mobile) -->
  <script src="/crcp-theme.js" defer></script><!-- CRCP day/night toggle (auto-injects into header) -->
</body></html>