← back to Commercialrealestate

public/index.html

1876 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230e1116'/%3E%3Ctext x='8' y='12' font-size='11' text-anchor='middle' fill='%233fb950'%3E%24%3C/text%3E%3C/svg%3E">
<title>LA County CRE — Investment Explorer</title>
<style>
  :root { --cols:3; --bg:#0e1116; --card:#161b22; --line:#2a313c; --ink:#e6edf3; --mut:#8b949e; --acc:#3fb950; --warn:#d29922; --bad:#f85149; --blue:#58a6ff; --drawerW:316px; }
  * { box-sizing:border-box; }
  :focus-visible { outline:2px solid var(--blue); outline-offset:2px; border-radius:4px; }
  @media (prefers-reduced-motion: reduce){ *,*::before,*::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; scroll-behavior:auto !important; } }
  body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }

  /* ---- top bar ---- */
  header { display:flex; align-items:center; gap:14px; padding:12px 20px; border-bottom:1px solid var(--line); position:sticky; top:0; z-index:30; background:rgba(14,17,22,.97); backdrop-filter:blur(8px); }
  .burger { display:inline-flex; align-items:center; gap:8px; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:9px 13px; font-size:15px; cursor:pointer; font-weight:600; }
  .burger:hover { border-color:var(--blue); }
  .fcount { display:none; background:var(--blue); color:#04122b; font-size:11px; font-weight:700; border-radius:20px; padding:1px 7px; }
  .fcount.on { display:inline-block; }
  .htitle h1 { margin:0; font-size:18px; letter-spacing:.2px; }
  .htitle .sub { color:var(--mut); font-size:12px; }
  header .spacer { margin-left:auto; }
  /* nav-link cluster; on wide screens it sits inline right-aligned, on tablets/phones it becomes a swipeable strip (see @media 1024) */
  .topnav { display:flex; align-items:center; flex-wrap:nowrap; }
  .seg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
  .seg button { background:var(--card); color:var(--ink); border:0; border-right:1px solid var(--line); padding:8px 11px; font-size:12px; cursor:pointer; }
  .seg button:last-child { border-right:0; }
  .seg button.active { background:#1f6feb; color:#fff; }

  /* ---- left drawer ---- */
  #scrim { position:fixed; inset:0; background:rgba(0,0,0,.5); opacity:0; pointer-events:none; transition:opacity .2s; z-index:38; }
  #scrim.on { opacity:1; pointer-events:auto; }
  #drawer { position:fixed; top:0; left:0; height:100vh; width:var(--drawerW); background:#0b0e13; border-right:1px solid var(--line); transform:translateX(-100%); transition:transform .22s ease; z-index:40; overflow-y:auto; padding:0 0 40px; }
  #drawer.open { transform:translateX(0); box-shadow:6px 0 30px rgba(0,0,0,.4); }
  .dhead { display:flex; align-items:center; justify-content:space-between; padding:16px 18px 10px; position:sticky; top:0; background:#0b0e13; border-bottom:1px solid var(--line); z-index:2; }
  .dhead b { font-size:15px; } .dhead .x { background:none; border:0; color:var(--mut); font-size:18px; cursor:pointer; }
  .fsec { padding:14px 18px; border-bottom:1px solid var(--line); }
  .fsec 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:18px; padding:5px 11px; font-size:12px; cursor:pointer; user-select:none; }
  .chip:hover { color:var(--ink); }
  .chip.active { border-color:var(--blue); color:var(--blue); background:rgba(88,166,255,.08); }
  .chip .ct { color:var(--mut); font-size:10px; margin-left:4px; }
  .chip.zero { opacity:.4; }
  .row2 { display:flex; gap:8px; }
  .fld { flex:1; }
  .fld label { display:block; font-size:10px; color:var(--mut); margin-bottom:3px; }
  input[type=text], input[type=number], select { width:100%; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:8px 10px; font-size:13px; }
  input[type=range] { width:100%; accent-color:var(--blue); }
  .rangeval { font-size:11px; color:var(--blue); float:right; }
  .toggle { display:flex; align-items:center; gap:8px; font-size:13px; cursor:pointer; }
  .resetbtn { width:calc(100% - 36px); margin:14px 18px 0; background:transparent; color:var(--bad); border:1px solid #7d2b28; border-radius:9px; padding:9px; font-size:13px; cursor:pointer; }

  /* ---- results bar + grid ---- */
  .bar { display:flex; flex-wrap:wrap; align-items:center; gap:14px; padding:11px 20px; border-bottom:1px solid var(--line); position:sticky; top:57px; background:rgba(14,17,22,.95); backdrop-filter:blur(6px); z-index:20; }
  .bar .count { font-size:13px; color:var(--mut); } .bar .count b { color:var(--ink); }
  .bar label { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.5px; }
  .bar .right { margin-left:auto; display:flex; align-items:center; gap:12px; }
  .grid { display:grid; grid-template-columns:repeat(var(--cols),minmax(0,1fr)); gap:22px; padding:30px 32px 44px; }
  @media (max-width:900px){ .grid{ grid-template-columns:repeat(2,minmax(0,1fr));} }
  /* ---- iPad / tablet / narrow-desktop band (≤1024): keep header ONE fixed row so sticky offsets hold,
         turn the nav-link cluster into a swipeable horizontal strip instead of clipping off-screen. ---- */
  @media (max-width:1024px){
    .grid{ grid-template-columns:repeat(2,minmax(0,1fr)); }  /* docked-drawer band: 2 cols so cards aren't crushed (iPad landscape) */
    header{ flex-wrap:nowrap; gap:10px; }
    header .spacer{ display:none; }                    /* nav no longer right-pushed; topnav flexes to fill */
    .htitle{ min-width:0; flex:0 1 auto; }             /* let the title shrink instead of shoving nav off-screen */
    .htitle h1{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
    .htitle .sub{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }  /* keep title block 2 lines → header stays ~57px so sticky bar/drawer offsets hold */
    .topnav{ flex:1 1 auto; min-width:0; overflow-x:auto; overflow-y:hidden;
             -webkit-overflow-scrolling:touch; scrollbar-width:none; }  /* iOS momentum-scroll, hidden scrollbar */
    .topnav::-webkit-scrollbar{ display:none; }
    .topnav > a, .topnav > .seg{ flex:0 0 auto; }      /* items keep intrinsic width; strip scrolls, nothing clips */
    .topnav > a{ padding:8px 0; }                      /* taller touch target for finger taps */
    html, body{ overflow-x:hidden; max-width:100%; }   /* no horizontal page scroll on iPad (html is the scroller) */
    .bar{ max-width:100%; top:auto; }                   /* sort/density strip: no hairline overflow; un-stick so it can't tuck under the taller (burger+title) iPad header */
  }
  @media (max-width:620px){ .grid{ grid-template-columns:1fr;} }
  .card { background:var(--card); border:2px solid #fff; border-radius:14px; padding:24px; display:flex; flex-direction:column; gap:10px; position:relative; transition:box-shadow .2s,border-color .2s; }
  .card.uc { opacity:.62; }
  .card.flash { border-color:var(--blue); box-shadow:0 0 0 3px rgba(88,166,255,.35); }
  .top { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; }
  .rank { font-size:12px; color:var(--mut); }
  .score { font-size:30px; font-weight:700; line-height:1; }
  .score small { font-size:11px; color:var(--mut); font-weight:400; display:block; text-align:right; }
  .addr { font-weight:600; font-size:15px; }
  .city { color:var(--mut); font-size:13px; }
  .badges { display:flex; flex-wrap:wrap; gap:6px; }
  .b { font-size:11px; padding:3px 8px; border-radius:20px; border:1px solid var(--line); color:var(--mut); }
  .b.type { color:var(--blue); border-color:#284b7a; }
  .b.deal-badge { font-weight:700; cursor:help; }
  .b.ok { color:var(--acc); border-color:#1f6f37; }
  .b.warn { color:var(--warn); border-color:#6b5320; }
  .b.bad { color:var(--bad); border-color:#7d2b28; }
  .b.rec-Buy { color:var(--acc); border-color:#1f6f37; font-weight:600; }
  .b.rec-Watch { color:var(--warn); border-color:#6b5320; font-weight:600; }
  .b.rec-Pass { color:var(--bad); border-color:#7d2b28; font-weight:600; }
  .metrics { display:grid; grid-template-columns:1fr 1fr; gap:6px 14px; font-size:13px; border-top:1px solid var(--line); border-bottom:1px solid var(--line); padding:10px 0; }
  .metrics div { display:flex; justify-content:space-between; }
  .metrics .k { color:var(--mut); }
  .metrics .v { font-variant-numeric:tabular-nums; font-weight:600; }
  .pos { color:var(--acc); } .neg { color:var(--bad); } .na { color:var(--mut); }
  .thesis { font-size:13px; color:#cdd6e0; }
  .lst { font-size:12px; color:var(--mut); margin:2px 0 0; padding-left:16px; }
  .src { font-size:12px; } .src a { color:var(--blue); text-decoration:none; }
  .lcbtn,.histbtn { background:transparent; color:var(--warn); border:1px dashed #6b5320; border-radius:14px; padding:2px 8px; font-size:11px; cursor:pointer; }
  .histbtn { color:var(--blue); border-color:#284b7a; }
  .lcout,.histout { font-size:11px; color:var(--mut); margin-top:6px; }
  .lcout a { color:var(--blue); text-decoration:none; }
  .lcout .lc-item { padding:4px 0; border-top:1px solid var(--line); }
  .small { font-size:11px; color:var(--mut); }
  .statusline { font-size:12px; font-weight:600; font-variant-numeric:tabular-nums; padding:5px 9px; border-radius:8px; border:1px solid var(--line); background:rgba(255,255,255,.02); }
  .statusline.ok { color:var(--acc); border-color:#1f6f37; }
  .statusline.warn { color:var(--warn); border-color:#6b5320; }
  .statusline.bad { color:var(--bad); border-color:#7d2b28; }
  .statusline.mut { color:var(--mut); }
  .statusline.none { color:var(--warn); border-color:#6b5320; border-style:dashed; }
  .foot { color:var(--mut); font-size:12px; padding:0 20px 8px; }
  .empty { padding:60px 20px; text-align:center; color:var(--mut); }

  /* ---- Broker-history + prior-broker flag ---- */
  .brokerflag { display:inline-flex; align-items:center; gap:5px; font-size:11px; padding:3px 9px; border-radius:20px; background:rgba(255,159,9,.10); color:#ff9f09; border:1px solid rgba(255,159,9,.35); font-weight:700; cursor:help; margin-top:2px; }
  .txhist { font-size:11px; color:var(--mut); margin-top:4px; line-height:1.5; }
  .txhist .tx-row { display:flex; gap:8px; align-items:baseline; padding:2px 0; border-bottom:1px dashed rgba(255,255,255,.05); }
  .txhist .tx-row:last-child { border-bottom:0; }
  .txhist .tx-date { color:var(--blue); font-variant-numeric:tabular-nums; min-width:72px; }
  .txhist .tx-ev { flex:1; }
  .txhist .tx-price { color:var(--acc); font-weight:600; font-variant-numeric:tabular-nums; }
  .brokerline { font-size:11px; padding:4px 9px; background:rgba(88,166,255,.06); border-radius:7px; border:1px solid rgba(88,166,255,.15); color:#a5c8ff; margin-top:3px; }
  .brokerline strong { color:var(--blue); }

  /* ---- docked chat ---- */
  #chatwrap { border-top:1px solid var(--line); background:#0b0e13; padding:16px 20px 40px; }
  #chatwrap h2 { font-size:15px; margin:0 0 2px; }
  #chatwrap .hint { color:var(--mut); font-size:12px; margin-bottom:10px; }
  #log { display:flex; flex-direction:column; gap:10px; max-height:42vh; overflow-y:auto; padding:4px 2px; }
  .msg { max-width:80%; padding:9px 13px; border-radius:12px; font-size:14px; line-height:1.45; white-space:pre-wrap; word-wrap:break-word; }
  .msg.u { align-self:flex-end; background:#1f6feb; color:#fff; border-bottom-right-radius:3px; }
  .msg.a { align-self:flex-start; background:var(--card); border:1px solid var(--line); border-bottom-left-radius:3px; }
  .msg.a code { background:#0b0e13; padding:1px 5px; border-radius:4px; }
  .msg.sys { align-self:center; color:var(--mut); font-size:12px; background:transparent; }
  .actiontag { display:inline-block; margin-top:6px; font-size:11px; color:var(--acc); border:1px solid #1f6f37; border-radius:6px; padding:2px 7px; }
  #chatform { display:flex; gap:10px; margin-top:12px; }
  #q { flex:1; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:11px 13px; font-size:14px; }
  #send { background:#1f6feb; color:#fff; border:0; border-radius:10px; padding:0 20px; font-size:14px; font-weight:600; cursor:pointer; }
  #send:disabled { opacity:.5; cursor:default; }
  .examples { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
  .ex { font-size:12px; color:var(--blue); border:1px dashed #284b7a; border-radius:16px; padding:4px 10px; cursor:pointer; background:transparent; }

  /* ---- view toggle (grid/list) ---- */
  .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; }
  /* ---- field show/hide toggles: one CARD per field, click to toggle on/off ---- */
  .fieldtoggles { display:flex; flex-wrap:wrap; gap:6px; max-height:360px; overflow-y:auto; padding:2px 4px 2px 0; }
  .ftog { display:inline-flex; align-items:center; gap:5px; font-size:12px; color:var(--mut); cursor:pointer; padding:5px 9px;
          border:1px solid var(--line); border-radius:9px; background:var(--card); user-select:none; transition:border-color .12s,color .12s,background .12s; }
  .ftog input { display:none; }                                   /* card IS the toggle; checkbox drives state via :has() */
  .ftog::before { content:'○'; font-size:11px; color:var(--mut); }
  .ftog:hover { border-color:var(--blue); color:var(--ink); }
  .ftog:has(input:checked) { border-color:var(--blue); color:var(--blue); background:rgba(88,166,255,.10); }
  .ftog:has(input:checked)::before { content:'●'; color:var(--blue); }
  .ftog .colbadge { color:var(--mut); font-size:10px; }
  .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); }
  /* ---- list view ---- */
  .listhost { display:block; padding:20px 20px 30px; }
  .listwrap { overflow-x:auto; border:2px solid #fff; border-radius:12px; }
  .listtbl { width:100%; border-collapse:collapse; font-size:12.5px; white-space:nowrap; }
  .listtbl th, .listtbl td { text-align:right; padding:9px 13px; border-top:1px solid var(--line); }
  .listtbl th { position:sticky; top:0; background:#0b0e13; color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; border-top:0; z-index:1; }
  .listtbl td.stick, .listtbl th.stick { text-align:left; position:sticky; left:0; background:var(--card); }
  .listtbl th.stick { background:#0b0e13; z-index:2; }
  .listtbl th.sortable { cursor:pointer; } .listtbl th.sortable:hover { color:var(--ink); }
  .listtbl tr.lrow:hover td { background:rgba(88,166,255,.06); }
  .listtbl tr.lrow:hover td.stick { background:#1b222c; }
  .listtbl .la { font-weight:600; color:var(--ink); text-decoration:none; }
  .listtbl .la:hover { color:var(--blue); }
  .listtbl .lc { color:var(--mut); font-size:11px; }
  .listtbl .flink { color:var(--ink); text-decoration:none; border-bottom:1px dotted #3a4453; }
  .listtbl .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
  .listtbl tr.lrow { cursor:pointer; }
  .listtbl .exptog { display:inline-block; width:11px; color:var(--mut); font-size:10px; transition:transform .15s; }
  .listtbl tr.lrow.open td.stick { background:#1b222c; }
  .listtbl tr.lexp > td { padding:0; border-top:0; }
  .listtbl tr.lexp .lexpbody { max-height:0; overflow:hidden; transition:max-height .25s ease; background:#0c1017; }
  .listtbl tr.lexp.open .lexpbody { max-height:1200px; padding:12px 16px; border-top:1px solid var(--line); }
  .lexpbody .src { text-align:left; white-space:normal; font-size:12px; }
  .lexpbody .deep { text-align:left; white-space:normal; margin-top:8px; display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
  .lexpbody .deeplbl { color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; margin-right:2px; }
  .lexpbody .deep a, .lexpbody .deep button { font-size:11px; padding:3px 9px; border-radius:14px; border:1px solid #2a3340; background:#141a22; color:var(--ink); text-decoration:none; cursor:pointer; }
  .lexpbody .deep a:hover, .lexpbody .deep button:hover { border-color:var(--blue); color:var(--blue); }
  .lexpbody .lcout, .lexpbody .histout { text-align:left; white-space:normal; }
  /* Agent · contact column cell */
  .listtbl td .agentcell { text-align:left; white-space:normal; min-width:170px; display:flex; flex-direction:column; gap:3px; }
  .agentcell .amut { color:var(--mut); font-style:italic; font-size:11px; }
  .agentcell .ainrow { display:flex; flex-wrap:wrap; gap:8px; }
  .agentcell .ain { color:var(--mut); text-decoration:none; font-size:11px; }
  .agentcell .ain:hover { color:var(--blue); }
  .agentcell .chiprow:empty { display:none; }
  .agentcell .achip { display:inline-block; font-size:10px; color:var(--mut); background:#141a22; border:1px solid #2a3340; border-radius:10px; padding:1px 6px; margin-right:4px; }
  .agentcell .achip.done { color:#3fb950; border-color:#264f2e; }
  .agentcell .acts { display:flex; flex-wrap:wrap; gap:5px; margin-top:2px; }
  .abtn { font-size:10.5px; padding:2px 8px; border-radius:12px; border:1px solid #2a3340; background:#141a22; color:var(--ink); cursor:pointer; }
  .abtn:hover { border-color:var(--blue); color:var(--blue); }
  .abtn.contactbtn { border-color:#2f5233; color:#8fdfa0; }
  .abtn.markbtn { border-color:#284b7a; color:#8ec0ff; }
  /* CRM block inside the inline panel */
  .lexpbody .crm { margin-top:10px; border-top:1px solid var(--line); padding-top:10px; text-align:left; }
  .crm-info { display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
  .crm-info .crmf { background:#0b0e13; border:1px solid #2a3340; border-radius:8px; color:var(--ink); font-size:12px; padding:5px 8px; min-width:150px; }
  .crm-info .crmf:focus { outline:none; border-color:var(--blue); }
  .crm-acts { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-top:8px; }
  .crm-letters { margin-top:10px; }
  .crm-letters .lett { border:1px solid #232b36; border-radius:8px; padding:7px 9px; margin-top:6px; background:#0b0e13; }
  .crm-letters .letth { display:flex; align-items:center; gap:8px; font-size:12px; }
  .crm-letters .letts { color:var(--mut); font-size:10px; }
  .crm-letters .letdel { margin-left:auto; background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:12px; }
  .crm-letters .letdel:hover { color:var(--bad,#f85149); }
  .crm-letters .lettb { white-space:pre-wrap; font-size:11.5px; color:var(--mut); margin-top:4px; }
  /* Compose-letter modal */
  .composeM { position:fixed; inset:0; background:rgba(0,0,0,.55); display:none; align-items:center; justify-content:center; z-index:9000; }
  .composeM.on { display:flex; }
  .composeCard { background:var(--card,#11161d); border:1px solid #2a3340; border-radius:14px; width:min(620px,92vw); max-height:88vh; overflow:auto; padding:16px; box-shadow:0 12px 40px rgba(0,0,0,.5); }
  .composeH { display:flex; align-items:center; font-size:14px; margin-bottom:8px; }
  .composeH .composeX { margin-left:auto; background:transparent; border:0; color:var(--mut); font-size:16px; cursor:pointer; }
  .composeGate { font-size:11px; color:#e3b341; background:#241d0e; border:1px solid #5a4a1e; border-radius:8px; padding:6px 9px; margin-bottom:10px; }
  .composeCard input, .composeCard textarea { width:100%; box-sizing:border-box; background:#0b0e13; border:1px solid #2a3340; border-radius:8px; color:var(--ink); font-size:13px; padding:8px 10px; margin-bottom:8px; font-family:inherit; }
  .composeCard textarea { resize:vertical; line-height:1.5; }
  .composeActs { display:flex; gap:8px; justify-content:flex-end; }
  .composeActs button { font-size:12px; padding:6px 14px; border-radius:10px; border:1px solid #2a3340; background:#141a22; color:var(--ink); cursor:pointer; }
  .composeActs button.primary { border-color:#2f5233; color:#8fdfa0; }
  .composeActs button.gated { border-color:#5a4a1e; color:#e3b341; }
  .composeActs button.gated:hover:not([disabled]) { border-color:#e3b341; }
  .composeActs button[disabled] { opacity:.4; cursor:not-allowed; }
  /* Tabbed agent window */
  .composeCard.tabwin { width:min(680px,94vw); }
  .composeH .winmeta { color:var(--mut); font-size:11px; font-weight:400; margin-left:10px; }
  .tabbar { display:flex; gap:2px; border-bottom:1px solid var(--line); margin:4px 0 12px; flex-wrap:wrap; }
  .tabt { background:transparent; border:0; border-bottom:2px solid transparent; color:var(--mut); font-size:12.5px; padding:7px 12px; cursor:pointer; }
  .tabt:hover { color:var(--ink); }
  .tabt.active { color:var(--ink); border-bottom-color:var(--blue); font-weight:600; }
  .tabpane { display:none; }
  .tabpane.active { display:block; }
  .tabmut { color:var(--mut); font-size:12px; font-style:italic; padding:8px 2px; }
  .cm-row { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:10px; }
  .tabpane .ainrow { display:flex; flex-wrap:wrap; gap:14px; margin:8px 0; }
  .tabpane .ain { color:var(--mut); text-decoration:none; font-size:12px; }
  .tabpane .ain:hover { color:var(--blue); }
  .findgrid { display:flex; flex-direction:column; gap:7px; }
  .findlink { display:inline-block; background:#141a22; border:1px solid #2a3340; border-radius:8px; color:var(--ink); text-decoration:none; font-size:12.5px; padding:8px 11px; cursor:pointer; text-align:left; }
  .findlink:hover { border-color:var(--blue); color:var(--blue); }
  .findlink.findloop { padding:8px 11px; }
  .findlink.findloop a { color:inherit; text-decoration:none; }
  .crmToast { position:fixed; bottom:22px; left:50%; transform:translateX(-50%) translateY(12px); background:#11161d; border:1px solid #2a3340; color:var(--ink); font-size:12.5px; padding:9px 16px; border-radius:20px; opacity:0; transition:opacity .2s,transform .2s; z-index:9500; pointer-events:none; }
  .crmToast.on { opacity:1; transform:translateX(-50%) translateY(0); }
  /* Card-view agent-contact block (every card, every asset type) */
  .card .cardagent { margin-top:8px; border-top:1px solid var(--line); padding-top:8px; font-size:12px; }
  .card .cardagent .ca-top { display:flex; flex-wrap:wrap; align-items:center; gap:6px; }
  .card .cardagent .ainrow { display:flex; flex-wrap:wrap; gap:10px; margin-top:3px; }
  .card .cardagent .ain { color:var(--mut); text-decoration:none; font-size:11px; }
  .card .cardagent .ain:hover { color:var(--blue); }
  .card .cardagent .acts { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
  /* Full contact modal info row */
  .composeCard .cm-info { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-bottom:10px; }
  .composeCard .cm-info .crmf { background:#0b0e13; border:1px solid #2a3340; border-radius:8px; color:var(--ink); font-size:12px; padding:6px 8px; min-width:140px; flex:1 1 140px; margin:0; }
  .composeCard .cm-info .crmf:focus { outline:none; border-color:var(--blue); }
  .composeH .flink { font-size:11px; font-weight:400; margin-left:8px; }
  /* Inline (no-outside-link) agent bits */
  .aname { font-weight:600; color:var(--ink); }
  .flink { cursor:pointer; }
  .listtbl td .flink { border-bottom:1px dotted #3a4453; }
  .listtbl td .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
  .copyb { background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:11px; padding:0 3px; border-radius:4px; }
  .copyb:hover { color:var(--blue); }
  .crm-note, .lexpbody .crm-note { margin-top:6px; font-size:12px; color:var(--mut); white-space:pre-wrap; background:#0c1017; border:1px solid #232b36; border-radius:8px; padding:6px 9px; text-align:left; }
  .composeCard .cm-notes { margin-bottom:10px; }
  .composeCard .cm-notes .deeplbl { margin-bottom:4px; }
  .composeCard .cm-notes textarea { width:100%; box-sizing:border-box; background:#0b0e13; border:1px solid #2a3340; border-radius:8px; color:var(--ink); font-size:12.5px; padding:7px 9px; font-family:inherit; resize:vertical; line-height:1.5; }
  .composeCard .cm-notes textarea:focus { outline:none; border-color:var(--blue); }
  /* ---- Amazon-style docked left filter rail (desktop) ---- */
  .shell { display:flex; align-items:flex-start; }
  main { flex:1 1 auto; min-width:0; }
  @media (min-width:901px){
    #burger { display:none; }
    #scrim { display:none !important; }
    #drawer { position:sticky; top:57px; height:calc(100vh - 57px); transform:none; flex:0 0 var(--drawerW); box-shadow:none; }
    #drawer .dhead .x { display:none; }
  }
  /* collapsible filter tabs */
  .fsec > h4 { cursor:pointer; display:flex; align-items:center; justify-content:space-between; user-select:none; }
  .fsec > h4::after { content:'▾'; color:var(--mut); font-size:11px; transition:transform .15s; margin-left:8px; }
  .fsec.collapsed > h4::after { transform:rotate(-90deg); }
  .fsec.collapsed > *:not(h4) { display:none !important; }
  .assessline { border-top:1px dashed rgba(255,255,255,.06); padding-top:6px; margin-top:2px; }
  /* active-filter summary bar */
  .activebar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:10px 20px; border-bottom:1px solid var(--line); background:rgba(88,166,255,.03); }
  .activebar:empty { display:none; }
  .activebar .lbl { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.5px; }
  .afchip { display:inline-flex; align-items:center; gap:6px; background:rgba(88,166,255,.10); border:1px solid rgba(88,166,255,.4); color:var(--blue); border-radius:16px; padding:4px 10px; font-size:12px; cursor:pointer; }
  .afchip:hover { background:rgba(88,166,255,.2); } .afchip .xx { opacity:.65; font-weight:700; }
  .afreset { background:transparent; border:1px solid #7d2b28; color:var(--bad); border-radius:14px; padding:4px 11px; font-size:12px; cursor:pointer; margin-left:auto; }
  .afreset:hover { background:rgba(248,81,73,.1); }
  /* shortlist stars */
  .starbtn { background:none; border:0; color:var(--mut); font-size:18px; line-height:1; cursor:pointer; padding:0; }
  .starbtn:hover, .starbtn.on { color:var(--warn); }
  .copybtn { background:none; border:0; color:var(--mut); font-size:14px; line-height:1; cursor:pointer; padding:0; }
  .copybtn:hover { color:var(--blue); }
  .listtbl .starbtn { font-size:14px; margin-right:5px; vertical-align:middle; }
  #shortbtn.shorton { background:rgba(210,153,34,.14); border-color:var(--warn); color:var(--warn); }
  /* aggregate stats strip */
  .statsbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:9px 20px; border-bottom:1px solid var(--line); }
  .statsbar:empty { display:none; }
  .statpill { font-size:12px; color:var(--ink); background:var(--card); border:1px solid var(--line); border-radius:8px; padding:4px 10px; }
  .statpill .sl { color:var(--mut); } .statpill b { font-variant-numeric:tabular-nums; }
  .heatlegend { display:none; align-items:center; gap:5px; font-size:11px; color:var(--mut); }
  .heatlegend.on { display:inline-flex; }
  .heatlegend .bar { width:56px; height:8px; border-radius:4px; background:linear-gradient(90deg,hsl(0,70%,45%),hsl(60,70%,45%),hsl(120,70%,45%)); }
  .showmore-wrap { grid-column:1/-1; text-align:center; padding:20px 0 6px; }
  .showmore { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:10px; padding:9px 20px; font-size:13px; cursor:pointer; }
  .showmore:hover { border-color:var(--blue); }
  .quickstart { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin:16px 0; }
  .qchip { background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:18px; padding:7px 14px; font-size:13px; cursor:pointer; }
  .qchip:hover { border-color:var(--blue); color:var(--blue); }
  .savedrow { display:flex; gap:6px; align-items:center; margin-bottom:5px; }
  .savedname { flex:1; text-align:left; background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:8px; padding:6px 9px; font-size:12px; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .savedname:hover { border-color:var(--blue); color:var(--blue); }
  .saveddel { background:none; border:0; color:var(--mut); cursor:pointer; font-size:13px; }
  .saveddel:hover { color:var(--bad); }
  #savebtn { width:100%; background:transparent; border:1px dashed #284b7a; color:var(--blue); border-radius:8px; padding:7px; font-size:12px; cursor:pointer; margin-top:4px; }
  .savedempty { color:var(--mut); font-size:11px; }
  /* compare overlay */
  #cmpov { display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:60; align-items:flex-start; justify-content:center; padding:40px 14px; overflow:auto; }
  #cmpov.on { display:flex; }
  #cmpmodal { width:min(1100px,97vw); background:var(--card); border:1px solid var(--line); border-radius:15px; padding:20px 22px; position:relative; }
  .cmpx { position:absolute; top:12px; right:14px; background:none; border:0; color:var(--mut); font-size:22px; cursor:pointer; }
  .cmptbl { width:100%; border-collapse:collapse; font-size:13px; }
  .cmptbl th, .cmptbl td { padding:8px 11px; border-top:1px solid var(--line); text-align:right; white-space:nowrap; }
  .cmptbl thead th { position:sticky; top:0; background:var(--card); text-align:right; vertical-align:bottom; color:var(--ink); }
  .cmptbl .cmpk, .cmptbl thead th.cmpk { text-align:left; color:var(--mut); position:sticky; left:0; background:var(--card); }
  .cmptbl thead th .cmpsub { color:var(--mut); font-size:11px; font-weight:400; }
  .cmphint { color:var(--mut); padding:24px 4px; }
  .cmptools { display:flex; gap:8px; justify-content:flex-end; margin-bottom:10px; }
  .cmptools button { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:8px; padding:6px 12px; font-size:12px; cursor:pointer; }
  .cmptools button:hover { border-color:var(--blue); }
  @media print {
    body.printing-compare * { visibility:hidden; }
    body.printing-compare #cmpov, body.printing-compare #cmpov * { visibility:visible; }
    body.printing-compare #cmpov { position:absolute; inset:0; background:#fff; display:block; padding:0; overflow:visible; }
    body.printing-compare #cmpmodal { width:100%; box-shadow:none; border:0; background:#fff; color:#000; }
    body.printing-compare #cmpmodal h3 { color:#000; }
    body.printing-compare .cmpx, body.printing-compare .no-print { display:none !important; }
    body.printing-compare .cmptbl th, body.printing-compare .cmptbl td { color:#000; border-color:#999; }
    body.printing-compare .cmptbl thead th, body.printing-compare .cmptbl .cmpk { background:#fff; color:#000; }
    body.printing-compare .cmpsub { color:#444; }
  }
</style>
</head>
<body>
<header>
  <button class="burger" id="burger" aria-label="Open filters">☰ Filters <span class="fcount" id="fcount">0</span></button>
  <div class="htitle">
    <h1>LA County CRE — Investment Explorer</h1>
    <div class="sub" id="sub">loading…</div>
  </div>
  <div class="spacer"></div>
  <div class="topnav">
  <a href="/crcp.html" style="color:var(--warn);text-decoration:none;font-size:13px;margin-right:12px" title="Commercial Real Estate Control Panel (live)">🛰️ CRCP</a>
  <a href="/map.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All properties on a map, colored by type, with overhead aerial photos">🗺️ Map</a>
  <a href="/mls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="MLS-style spreadsheet + grid — all property data, sortable & searchable">📋 MLS</a>
  <a href="/deals.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Liquid-glass deal-intelligence charts">◆ Deals</a>
  <a href="/rent-rolls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Rent-roll → DSCR underwriting">📑 Rent Rolls</a>
  <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All brokers/agents — grid + list, sortable by every field">👥 Broker grid</a>
  <a href="/brokers.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent relationship mind-map">🕸️ Broker map</a>
  <a href="/graphics.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / firm / warrantability charts">📊 Graphics</a>
  <a href="/condos.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LA condos — FHA warrantability">🏢 Condos</a>
  <a href="/fha-loans.html" style="color:var(--gold,#e9c46a);text-decoration:none;font-size:13px;margin-right:12px" title="HUD/FHA multifamily loans originally ≤3.5% — historic-low-rate flagged">◆ FHA Loans</a>
  <a href="#brokers" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent contacts — name, company, phone, email">👤 Brokers</a>
  <a href="/linkedin.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LinkedIn follow-list">🔗 LinkedIn</a>
  <a href="/licensed.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Licensed directory — RE agents, firms, title & escrow + firm IG">📋 Licensed</a>
  <span class="seg" id="scenario">
    <button data-s="25" class="active">25% down</button>
    <button data-s="30">30% down</button>
    <button data-s="cash">All cash</button>
  </span>
  </div><!-- /.topnav -->
</header>

<div id="scrim"></div>
<div class="shell">
<aside id="drawer">
  <div class="dhead"><b>Filters</b><button class="x" id="dclose" aria-label="Close">✕</button></div>

  <div class="fsec"><h4>Saved searches</h4><div id="savedList"></div><button id="savebtn">💾 Save current view</button></div>
  <div class="fsec"><h4>Asset type</h4><div class="chips" id="fType"></div></div>
  <div class="fsec"><h4>Listing firm</h4><div class="chips" id="fFirm"></div></div>
  <div class="fsec"><h4>Submarket / city</h4>
    <input type="text" id="fCity" placeholder="search city or street… e.g. Encino, Ventura Blvd">
    <div class="chips" id="fCityChips" style="margin-top:8px"></div>
  </div>
  <div class="fsec"><h4>Price</h4>
    <div class="row2"><div class="fld"><label>Min $</label><input type="number" id="fPriceMin" placeholder="any" step="50000"></div>
    <div class="fld"><label>Max $</label><input type="number" id="fPriceMax" placeholder="any" step="50000"></div></div>
  </div>
  <div class="fsec"><h4>Cap rate <span class="rangeval" id="capVal">any</span></h4>
    <input type="range" id="fCap" min="0" max="10" step="0.25" value="0">
    <div class="small">minimum broker cap rate (0 = any)</div>
  </div>
  <div class="fsec"><h4>Units / size</h4>
    <div class="row2"><div class="fld"><label>Min units</label><input type="number" id="fUnits" placeholder="any" min="0" step="1"></div>
    <div class="fld"><label>Built after</label><input type="number" id="fYear" placeholder="any" min="1900" max="2026" step="1"></div></div>
  </div>
  <div class="fsec"><h4>Status &amp; budget</h4>
    <div class="chips" id="fStatus">
      <span class="chip" data-st="all">All</span>
      <span class="chip" data-st="active">Active only</span>
    </div>
    <label class="toggle" style="margin-top:10px"><input type="checkbox" id="fAfford"> In budget ($400k down) only</label>
  </div>
  <div class="fsec"><h4>Sort &amp; density</h4>
    <select id="sort">
      <option value="deal">Deal Score ↓</option>
      <option value="composite">Best Investment (blended)</option>
      <option value="coc">Cash-on-Cash ↓</option>
      <option value="yield">Rent Yield (rent÷price) ↓</option>
      <option value="cap">Cap Rate ↓</option>
      <option value="dscr">DSCR ↓</option>
      <option value="priceAsc">Price ↑</option>
      <option value="priceDesc">Price ↓</option>
      <option value="cashNeeded">Cash Needed ↑</option>
      <option value="unitsDesc">Units ↓</option>
      <option value="rentRoi">Pro-forma Rent ROI ↓</option>
    </select>
    <div style="margin-top:10px"><label class="small">Cards per row (grid) <span class="rangeval" id="densVal">3</span></label>
      <input type="range" id="density" min="1" max="10" step="1" value="3"></div>
  </div>
  <div class="fsec"><h4>Fields — show / hide</h4>
    <div class="fieldtoggles" id="fFields"></div>
    <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fDef">Default</button></div>
    <div class="small" style="margin-top:6px">▤ = also a column in List view</div>
  </div>
  <button class="resetbtn" id="reset">Reset all filters</button>
</aside>

<main>
  <!-- ===== Brokers & agents — front-page contacts (name / company / phone / email) ===== -->
  <style>
    #brokers{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:14px 16px;margin-bottom:18px;}
    #brokers h2{margin:0 0 2px;font-size:14px;text-transform:uppercase;letter-spacing:.6px;color:var(--warn);}
    #brokers .bksub{color:var(--mut);font-size:12px;margin-bottom:12px;}
    #bktbl{width:100%;border-collapse:collapse;font-size:13px;}
    #bktbl td,#bktbl th{text-align:left;padding:7px 8px;border-top:1px solid var(--line);}
    #bktbl th{color:var(--mut);font-size:11px;text-transform:uppercase;letter-spacing:.4px;border-top:0;}
    #bktbl tr.bkrow{cursor:pointer;} #bktbl tr.bkrow:hover td{background:rgba(88,166,255,.06);}
    #bktbl a{color:var(--blue);text-decoration:none;} #bktbl .miss{color:#6e7681;}
    .bkatype{display:inline-block;font-size:10px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;padding:1px 6px;border-radius:9px;margin-left:6px;}
    .bkatype.res{background:rgba(88,166,255,.16);color:var(--blue);} .bkatype.com{background:rgba(210,153,34,.16);color:var(--warn);}
    #bkov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:60;align-items:flex-start;justify-content:center;padding:40px 14px;overflow-y:auto;}
    #bkov.on{display:flex;}
    #bkmodal{width:min(720px,96vw);background:var(--card);border:1px solid var(--line);border-radius:15px;padding:20px 22px;position:relative;}
    #bkmodal h3{margin:0 0 2px;font-size:20px;} #bkmodal .mfirm{color:var(--warn);font-size:13px;}
    #bkmodal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
    #bkmodal .ci{display:flex;gap:16px;flex-wrap:wrap;margin:10px 0;font-size:14px;} #bkmodal .ci a{color:var(--blue);text-decoration:none;}
    #bkmodal .sec{margin-top:14px;} #bkmodal .sec h4{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);}
    #bkmodal .litem{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid var(--line);font-size:13px;} #bkmodal .litem .m{color:var(--mut);}
    .bkbtn{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:7px 12px;font-size:13px;cursor:pointer;margin-right:8px;} .bkbtn:hover{border-color:var(--blue);}
    .gate{font-size:12px;color:var(--mut);margin-top:8px;}
    .lead{border:1px solid var(--line);border-radius:11px;margin-bottom:8px;overflow:hidden;background:rgba(255,255,255,.012);}
    .lead .lhead{display:flex;align-items:center;gap:12px;padding:11px 14px;cursor:pointer;}
    .lead .lhead:hover{background:rgba(88,166,255,.06);}
    .lead .rk{font-weight:700;color:var(--warn);width:24px;text-align:center;font-variant-numeric:tabular-nums;}
    .lead .who{flex:1;min-width:0;} .lead .who .nm{font-weight:600;} .lead .who .fm{color:var(--mut);font-size:12px;}
    .lead .fit{font-size:11px;font-weight:700;color:var(--acc);background:rgba(63,185,80,.13);border:1px solid rgba(63,185,80,.35);border-radius:8px;padding:2px 8px;white-space:nowrap;}
    .lead .meta{color:var(--mut);font-size:12px;white-space:nowrap;}
    .lead .car{color:var(--mut);transition:transform .15s;} .lead.open .car{transform:rotate(90deg);}
    .lead .body{display:none;padding:4px 14px 12px;border-top:1px solid var(--line);}
    .lead.open .body{display:block;}
    .lead .acts{display:flex;gap:8px;margin:10px 0 6px;flex-wrap:wrap;}
    .figwrap{margin:8px 0 4px;display:flex;flex-direction:column;gap:4px;}
    .fighd{font-size:11px;color:var(--mut);letter-spacing:.02em;margin-bottom:1px;}
    .figli{display:flex;gap:9px;align-items:center;padding:5px 7px;border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.02);text-decoration:none;color:inherit;}
    .figli:hover{border-color:var(--blue);}
    .figli-static{cursor:default;} .figli-static:hover{border-color:var(--line);}
    .figt{width:38px;height:38px;flex:0 0 38px;border-radius:6px;object-fit:cover;background:var(--line);display:inline-block;}
    .figb{display:flex;flex-direction:column;min-width:0;gap:1px;}
    .figc{font-size:12px;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:min(70vw,560px);}
    .figm{font-size:10.5px;color:var(--mut);font-variant-numeric:tabular-nums;}
    .mls{width:100%;border-collapse:collapse;font-size:12.5px;}
    .mls th,.mls td{text-align:left;padding:5px 7px;border-top:1px solid var(--line);} .mls th{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.4px;border-top:0;}
    .mls td.n{text-align:right;font-variant-numeric:tabular-nums;} .mls tr.mrow{cursor:pointer;} .mls tr.mrow:hover td{background:rgba(210,153,34,.07);}
    .mls .hist{color:var(--blue);font-size:11px;}
    .phitem{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid var(--line);font-size:13px;} .phitem .m{color:var(--mut);}
  </style>
  <section id="brokers">
    <h2>🎯 Top brokers</h2>
    <div class="bksub">Brokers ranked by the quality and mix of their <b>current listings</b>. Click a broker to expand their listings (MLS-style) + a <b>Sold</b> button. Each property → <b>100-yr history by address</b> (no owner names). Contact details on the ✉ button.</div>
    <div id="bkleads"><div class="gate">loading ranked brokers…</div></div>
  </section>
  <div id="bkov"><div id="bkmodal"><button class="x" aria-label="Close" onclick="document.getElementById('bkov').classList.remove('on')">✕</button><div id="bkmbody"></div></div></div>
  <script>
  (function(){
    const bq=s=>document.querySelector(s);
    const money=n=>n?'$'+Number(n).toLocaleString():'—';
    const esc=s=>(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
    const atype=t=>t==='residential'?'<span class="bkatype res">Residential</span>':'<span class="bkatype com">Commercial</span>';
    const fdate=s=>{ if(!s) return ''; try{ return new Date(s).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }catch(e){ return ''; } };
    async function openContact(name,id){
      const ov=bq('#bkov'), mb=bq('#bkmbody'); ov.classList.add('on'); mb.innerHTML='<div class="gate">loading '+esc(name)+'…</div>';
      const qs=id?'id='+encodeURIComponent(id):'name='+encodeURIComponent(name);
      let d; try{ d=await (await fetch('/api/broker?'+qs)).json(); }catch(e){ mb.innerHTML='<div class="gate">error</div>'; return; }
      if(d.error){ mb.innerHTML='<div class="gate">'+esc(d.error)+'</div>'; return; }
      const b=d.broker, w=b.website?(/^https?:/.test(b.website)?b.website:'https://'+b.website):null;
      mb.innerHTML='<h3>'+esc(b.name)+atype(b.agent_type)+'</h3><div class="mfirm">'+esc(b.firm||'—')+(b.title?' · '+esc(b.title):'')+'</div>'
        +'<div class="ci">'
        +(b.phone?'<a href="tel:'+esc(b.phone)+'">☎ '+esc(b.phone)+'</a>':'<span class="gate">☎ —</span>')
        +(b.email?'<a href="mailto:'+esc(b.email)+'">✉ '+esc(b.email)+'</a>':'<span class="gate">✉ —</span>')
        +(w?'<a href="'+esc(w)+'" target="_blank" rel="noopener noreferrer">🌐 website</a>':'')+'</div>'
        +'<div class="sec"><h4>Current listings ('+d.current.length+')</h4>'+(d.current.length?d.current.map(l=>'<div class="litem"><span>'+esc(l.address)+', '+esc(l.city)+(l.zip?' '+esc(l.zip):'')+'</span><span class="m">'+esc(l.type)+' · '+money(l.price)+(l.units?' · '+l.units+'u':'')+(l.cap_rate?' · '+l.cap_rate+'% cap':'')+(l.listed_at?' · listed '+fdate(l.listed_at):'')+'</span></div>').join(''):'<div class="gate">none in our set</div>')+'</div>'
        +'<div class="sec"><h4>Expired / off-market ('+((d.expired&&d.expired.length)||0)+')</h4>'+((d.expired&&d.expired.length)?d.expired.map(x=>'<div class="litem"><span>'+esc(x.address)+', '+esc(x.city)+'</span><span class="m">'+money(x.list_price)+' → sold '+money(x.sold_price)+' · '+fdate(x.sold_date)+(x.mls?' · MLS '+esc(x.mls):'')+(x.url?' · <a href="'+esc(x.url)+'" target="_blank" rel="noopener noreferrer">↗</a>':'')+'</span></div>').join(''):'<div class="gate">'+esc(d.expiredNote||'none')+'</div>')+'</div>'
        +'<div class="sec"><h4>Sales — area sold ('+((d.comps&&d.comps.length)||0)+')</h4>'+((d.comps&&d.comps.length)?d.comps.map(c=>'<div class="litem"><span>'+esc(c.address)+', '+esc(c.city)+(c.beds?' · '+c.beds+'bd':'')+(c.baths?'/'+c.baths+'ba':'')+(c.sqft?' · '+Number(c.sqft).toLocaleString()+'sf':'')+'</span><span class="m">'+money(c.sold_price)+' · '+fdate(c.sold_date)+(c.mls?' · MLS '+esc(c.mls):'')+(c.url?' · <a href="'+esc(c.url)+'" target="_blank" rel="noopener noreferrer">↗</a>':'')+'</span></div>').join(''):'<div class="gate">no area sales</div>')+(d.compsNote?'<div class="gate">'+esc(d.compsNote)+'</div>':'')+'</div>';
    }
    bq('#bkov').addEventListener('click',e=>{ if(e.target.id==='bkov') e.currentTarget.classList.remove('on'); });
    const moneyK=n=>{ if(n==null) return '—'; n=+n; return n>=1e6?'$'+(n/1e6).toFixed(2)+'M':'$'+Math.round(n/1e3)+'k'; };
    // 100-year property history popup — address-only, NO owner/resident names (by design).
    async function propHistory(address,city){
      const ov=bq('#bkov'), mb=bq('#bkmbody'); ov.classList.add('on'); mb.innerHTML='<div class="gate">loading history for '+esc(address)+'…</div>';
      let d; try{ d=await (await fetch('/api/property-history?address='+encodeURIComponent(address)+'&city='+encodeURIComponent(city||''))).json(); }catch(e){ mb.innerHTML='<div class="gate">error</div>'; return; }
      const a=d.assessor||d.assessorArchive;
      let h='<h3>'+esc(address)+'</h3><div class="mfirm">'+esc(city||'')+' · property history (address only — no names)</div>';
      if(a){ h+='<div class="sec"><h4>Assessor parcel'+(a.roll_year?' ('+esc(a.roll_year)+' roll)':'')+'</h4><div class="ci">'
        +(a.year_built?'<span>🏗 Built '+a.year_built+'</span>':'')
        +(a.bedrooms!=null?'<span>🛏 '+a.bedrooms+' bd</span>':'')+(a.bathrooms!=null?'<span>🛁 '+a.bathrooms+' ba</span>':'')
        +(a.sqft_main?'<span>📐 '+Number(a.sqft_main).toLocaleString()+' sqft</span>':'')+(a.units?'<span>🏠 '+a.units+' units</span>':'')
        +((a.use_desc2||a.use_desc1||a.use_description)?'<span>🏷 '+esc(a.use_desc2||a.use_desc1||a.use_description)+'</span>':'')
        +((a.roll_total_value||a.total_value)?'<span>💰 assessed '+money(a.roll_total_value||a.total_value)+'</span>':'')+'</div></div>'; }
      h+='<div class="sec"><h4>Sold history ('+d.sales.length+')</h4>'+(d.sales.length?d.sales.map(x=>'<div class="phitem"><span>'+esc(x.address)+(x.city?', '+esc(x.city):'')+'</span><span class="m">'+money(x.sold_price)+' · '+String(x.sold_date||'').slice(0,10)+'</span></div>').join(''):'<div class="gate">no sold records on file for this address</div>')+'</div>';
      if(d.permits&&d.permits.length) h+='<div class="sec"><h4>Permits ('+d.permits.length+')</h4>'+d.permits.map(p=>'<div class="phitem"><span>'+esc(p.permit_type||p.use_description||'permit')+'</span><span class="m">'+esc(String(p.issue_date||'').slice(0,10))+(p.valuation?' · '+money(p.valuation):'')+'</span></div>').join('')+'</div>';
      if(d.events&&d.events.length) h+='<div class="sec"><h4>Place history</h4>'+d.events.map(ev=>'<div class="phitem"><span>'+esc(ev.summary||ev.kind)+'</span><span class="m">'+esc(String(ev.valid_from||'').slice(0,10))+'</span></div>').join('')+'</div>';
      if(!a && !d.sales.length && !(d.permits||[]).length) h+='<div class="gate">No assessor record matched this address in the LA County parcel roll (2.4M parcels), and no sold or permit history is on file. Try the street number plus street name exactly as it appears on the deed.</div>';
      mb.innerHTML=h;
    }
    // Sold / area history for a lead (per-broker closings aren't in our data; show area sold comps).
    async function brokerSold(id,btn){
      const box=btn.closest('.body').querySelector('.soldbox'); box.innerHTML='<div class="gate">loading sold history…</div>';
      try{ const d=await (await fetch('/api/broker?id='+id)).json(); const sold=d.comps||[];
        box.innerHTML='<div class="gate">Per-broker closings need a gated detail-scrape; showing recent <b>area sold comps</b> in this broker’s cities ('+sold.length+'):</div>'
          +(sold.length?sold.map(c=>'<div class="phitem"><span>'+esc(c.address)+', '+esc(c.city)+'</span><span class="m">'+money(c.sold_price)+' · '+String(c.sold_date||'').slice(0,10)+'</span></div>').join(''):'<div class="gate">none</div>');
      }catch(e){ box.innerHTML='<div class="gate">'+esc(e.message)+'</div>'; }
    }
    // Firm Instagram (Business Discovery — public firm business accounts only).
    // Loads the last-3-posts cache + firm->handle map, builds a normalized index.
    window.FIRM_IG={}; window.FIRM_IG_IDX={};
    const firmIgReady=Promise.all([
      fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
      fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}}))
    ]).then(([cache,map])=>{ window.FIRM_IG=cache||{}; const idx={};
      for(const [h,al] of Object.entries((map.handles)||{})) for(const a of al) idx[a]=h; window.FIRM_IG_IDX=idx; }).catch(()=>{});
    const normFirm=s=>String(s||'').toLowerCase().replace(/[^a-z0-9]/g,'');
    function firmHandle(firm){ const n=normFirm(firm); if(!n) return null;
      if(window.FIRM_IG_IDX[n]) return window.FIRM_IG_IDX[n];
      for(const [a,h] of Object.entries(window.FIRM_IG_IDX)){ if(n===a || (a.length>=5 && n.includes(a)) || (a.length>=3 && n.startsWith(a))) return h; } return null; }
    function firmIgStrip(firm){ const h=firmHandle(firm); if(!h) return '';
      const c=window.FIRM_IG[h]; if(!c||!c.posts||!c.posts.length) return '';
      const items=c.posts.slice(0,3).map(p=>{ const cap=esc((p.caption||'').replace(/\s+/g,' ').slice(0,90))||'(no caption)';
        const when=p.timestamp?String(p.timestamp).slice(0,10):''; const vid=p.media_type==='VIDEO'?'▶ ':'';
        const thumb=p.image?'<img class="figt" loading="lazy" src="'+esc(p.image)+'" alt="">':'<span class="figt"></span>';
        return '<a class="figli" href="'+esc(p.permalink)+'" target="_blank" rel="noopener noreferrer">'+thumb+'<span class="figb"><span class="figc">'+vid+cap+' ↗</span><span class="figm">'+(when?when+' · ':'')+'♥ '+(p.likes||0)+' · 💬 '+(p.comments||0)+'</span></span></a>'; }).join('');
      return '<div class="figwrap"><div class="fighd">📷 @'+esc(h)+' · firm’s latest 3</div>'+items+'</div>'; }
    // Ranked top brokers — expandable to MLS-style current listings + Sold + per-address history.
    fetch('/api/leads/top?limit=40').then(r=>r.json()).then(async d=>{
      await firmIgReady;
      const leads=d.leads||[];
      if(!leads.length){ bq('#bkleads').innerHTML='<div class="gate">no brokers (broker DB unavailable)</div>'; return; }
      bq('#bkleads').innerHTML=leads.map((l,i)=>{
        const rows=(l.listings_json||[]).map(p=>'<tr class="mrow" data-addr="'+esc(p.address)+'" data-city="'+esc(p.city||'')+'">'
          +'<td>'+esc(p.address)+'<div style="color:var(--mut);font-size:11px">'+esc(p.city||'')+(p.zip?' '+esc(p.zip):'')+'</div></td>'
          +'<td>'+esc(p.type||'—')+'</td><td class="n">'+money(p.price)+'</td><td class="n">'+(p.units||'—')+'</td>'
          +'<td class="n">'+(p.cap_rate!=null?p.cap_rate+'%':'—')+'</td><td class="hist">📜 history</td></tr>').join('');
        return '<div class="lead" data-id="'+l.broker_id+'" data-name="'+esc(l.name)+'">'
          +'<div class="lhead"><span class="rk">'+(i+1)+'</span>'
          +'<div class="who"><span class="nm">'+esc(l.name)+'</span>'+atype(l.agent_type)+'<div class="fm">'+esc(l.firm||'—')+'</div></div>'
          +'<span class="fit">fit '+l.fit_score+'</span><span class="meta">'+l.listings+' listings · '+moneyK(l.total_value)+'</span><span class="car">▸</span></div>'
          +'<div class="body"><div class="acts"><button class="bkbtn" data-act="contact">✉ Contact</button>'
          +'<button class="bkbtn" data-act="sold">🔴 Sold / area history</button></div>'
          +window.firmIgInline(l.firm)
          +'<table class="mls"><thead><tr><th>Address</th><th>Type</th><th>Price</th><th>Units</th><th>Cap</th><th></th></tr></thead><tbody>'+rows+'</tbody></table>'
          +'<div class="soldbox"></div></div></div>';
      }).join('');
      bq('#bkleads').onclick=e=>{
        const lead=e.target.closest('.lead'); if(!lead) return;
        const mrow=e.target.closest('.mrow'); if(mrow){ propHistory(mrow.dataset.addr, mrow.dataset.city); return; }
        const act=e.target.closest('[data-act]'); if(act){ if(act.dataset.act==='contact') openContact(lead.dataset.name, lead.dataset.id); else brokerSold(lead.dataset.id, act); return; }
        if(e.target.closest('.lhead')) lead.classList.toggle('open');
      };
    }).catch(()=>{ bq('#bkleads').innerHTML='<div class="gate">broker DB unavailable</div>'; });
  })();
  </script>
  <div class="bar">
    <span class="count" id="count">…</span>
    <span class="viewseg" id="viewseg">
      <button data-view="grid" class="active">▦ Grid</button>
      <button data-view="list">☰ List</button>
    </span>
    <div class="right">
      <button id="shortbtn" class="ex" title="Show only your shortlisted (starred) listings">⭐ Shortlist (0)</button>
      <button id="cmpbtn" class="ex" title="Compare your shortlisted listings side by side">⚖ Compare</button>
      <button id="csvbtn" class="ex" title="Download the current filtered + sorted view (visible columns) as CSV">⬇ CSV</button>
      <button id="linkbtn" class="ex" title="Copy a shareable link to this exact filtered view">🔗 Copy link</button>
      <select id="heat" title="Color cards/rows by a metric — red (low) → green (high) over the filtered set" style="width:auto"><option value="none">Heat: off</option><option value="deal">Heat: Deal score</option><option value="cap">Heat: Cap rate</option><option value="coc">Heat: Cash-on-cash</option></select>
      <span id="heatlegend" class="heatlegend">low <span class="bar"></span> high · <span class="hl"></span></span>
      <label>Sort</label>
      <select id="sortTop" style="width:auto;min-width:180px"></select>
    </div>
  </div>
  <div class="activebar" id="activebar"></div>
  <div class="statsbar" id="statsbar"></div>
  <div class="grid" id="grid"></div>
  <div class="foot" id="foot"></div>
</main>
</div><!-- /shell -->

<div id="chatwrap">
  <h2>💬 Ask the analyst &nbsp;<span class="small">— grounded in this dataset · free (Claude CLI)</span></h2>
  <div class="hint">Ask questions <b>or tell it to drive the view</b>: “show only multifamily under $1M at 30% down sorted by cash-on-cash”, “take me to the Reseda mixed-use”.</div>
  <div id="log"></div>
  <div class="examples" id="examples"></div>
  <form id="chatform" autocomplete="off">
    <input id="q" placeholder="Ask about the deals, or tell me to filter / sort / change financing…">
    <button id="send" type="submit">Send</button>
  </form>
</div>

<div id="cmpov"><div id="cmpmodal"><button class="cmpx" aria-label="Close" onclick="document.getElementById('cmpov').classList.remove('on')">✕</button><h3 style="margin:0 0 10px">⚖ Compare shortlist</h3><div id="cmpbody"></div></div></div>
<script src="finance.js"></script>
<script src="deal-score.js"></script>
<script>
const $ = s => document.querySelector(s);
const $$ = s => Array.from(document.querySelectorAll(s));
const fmt = n => n==null ? '—' : '$'+Math.round(n).toLocaleString();
const pct = n => n==null ? '—' : n+'%';
const cls = (v, good, ok) => v==null?'na':(v>=good?'pos':(v>=ok?'':'neg'));

// ── Dated status line (HARD RULE: every property shows EITHER a current listing
// with its date OR historical data with dates — never a blank/undated status) ──
const MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
function fmtMonYear(d){ return d ? MON[d.getMonth()]+' '+d.getFullYear() : null; }
function fmtFullDate(d){ return d ? MON[d.getMonth()]+' '+d.getDate()+', '+d.getFullYear() : null; }
// Pull the most reliable known date for a listing: the refresh/source stamp in the
// note (ISO YYYY-MM-DD or "Mon D, YYYY" or MM/DD/YYYY), else the dataset's sourced date.
function listingDate(p){
  const t = p.upside_note || '';
  let m = t.match(/(20\d\d)-(\d\d)-(\d\d)/);                                 // 2026-06-27
  if (m) return { d:new Date(+m[1], +m[2]-1, +m[3]), exact:true };
  m = t.match(/\b([A-Z][a-z]{2,8})\s+(\d{1,2}),?\s+(20\d\d)/);              // June 21, 2026
  if (m){ const mi=MON.findIndex(x=>m[1].slice(0,3)===x); if(mi>=0) return { d:new Date(+m[3], mi, +m[2]), exact:true }; }
  m = t.match(/\b(\d{1,2})\/(\d{1,2})\/(20\d\d)/);                          // 06/27/2026
  if (m) return { d:new Date(+m[3], +m[1]-1, +m[2]), exact:true };
  // Fallback: the whole dataset's sourcing date (meta.sourced begins YYYY-MM-DD).
  const src = (DATA && DATA.meta && DATA.meta.sourced || '').match(/(20\d\d)-(\d\d)-(\d\d)/);
  if (src) return { d:new Date(+src[1], +src[2]-1, +src[3]), exact:false };
  return null;
}
// The guaranteed status line. Current listing → "Listed <date>". Off-market states
// (Under Contract / Pending / Sold) → labeled with date as historical. No date at
// all → an explicit "no recorded history" label, never a blank.
function statusLine(p){
  const ld = listingDate(p);
  const dateTxt = ld ? (ld.exact ? fmtFullDate(ld.d) : fmtMonYear(ld.d)+' (dataset)') : null;
  const title = ld ? ' title="'+(ld.exact?'Listing data as of '+ld.d.toISOString().slice(0,10):'No per-listing date; using dataset source date '+ld.d.toISOString().slice(0,10))+'"' : '';
  let verb, klass;
  if (/Under Contract|Pending|Escrow/i.test(p.status)) { verb='In escrow / '+p.status; klass='warn'; }
  else if (/Sold|Closed/i.test(p.status))             { verb='Last sold'; klass='mut'; }
  else if (/OUT OF BUDGET/i.test(p.status))           { verb='Listed (over budget)'; klass='bad'; }
  else                                                { verb='Listed'; klass='ok'; }
  if (!dateTxt) return `<div class="statusline none" title="No current listing date and no recorded sale history in this dataset">🕓 ${verb} · no recorded date · no sale history on file</div>`;
  const priced = p.price ? ' '+fmt(p.price) : '';
  return `<div class="statusline ${klass}"${title}>🕓 ${verb}${priced} · as of ${dateTxt}</div>`;
}

const SCEN = { '25':{budget:400000,downPct:25,ratePct:6.75,amortYears:30,closingPct:2.5},
               '30':{budget:400000,downPct:30,ratePct:6.75,amortYears:30,closingPct:2.5},
               'cash':{budget:400000,downPct:100,ratePct:0,amortYears:30,closingPct:2.5} };

let DATA=null, scenario='25', history=[];
window.DEMO = {};  // US Census ACS demographics by lowercased city (data/demographics.json)
function demoLine(p){
  const d = window.DEMO[(p.city||'').toLowerCase()]; if(!d) return '';
  const bits=[];
  if(d.renter_pct!=null) bits.push(`${d.renter_pct}% renters`);
  if(d.median_gross_rent) bits.push(`$${d.median_gross_rent.toLocaleString()} med rent`);
  if(d.median_income) bits.push(`$${Math.round(d.median_income/1000)}k income`);
  if(d.population) bits.push(`${Math.round(d.population/1000)}k pop`);
  return bits.length ? `<div class="small" title="US Census ACS 2022 — ${p.city} (ZCTA ${d.zcta})">📊 ${bits.join(' · ')}</div>` : '';
}
// Neighborhood-based rent estimate from Census median gross rent. std = market median × units
// (conservative in-place proxy); proforma = +12% market upside. Honest estimate, NOT an appraisal/1007.
function rentEstimate(p){
  const d = window.DEMO[(p.city||'').toLowerCase()]; if(!d || !d.median_gross_rent) return null;
  const base = d.median_gross_rent, units = p.units || 1;
  const std = Math.round(base*units), pro = Math.round(std*1.12);
  const annualPro = pro*12;
  return { std, pro, base, units, zcta:d.zcta, grossYieldPro: p.price? +(100*annualPro/p.price).toFixed(2):null };
}
function rentLine(p){
  const r = rentEstimate(p); if(!r) return '';
  // Plausibility guard: LA gross yields run ~5-10%. >18% almost always means a bad units count, not a deal.
  const fishy = r.grossYieldPro && r.grossYieldPro > 18;
  const yld = r.grossYieldPro ? ` · ${r.grossYieldPro}% gross yld (pro)${fishy?` <span class="b warn" title="Implausibly high — likely a bad unit count on the listing; verify.">⚠ verify units</span>`:''}` : '';
  return `<div class="small" title="Estimate from Census median gross rent ($${r.base.toLocaleString()}/unit, ${p.city} ZCTA ${r.zcta}) × ${r.units} unit${r.units>1?'s':''}. Not an appraisal — DSCR screen only.">💵 Est. rent: <b style="color:var(--ink)">$${r.std.toLocaleString()}/mo</b> std · <b style="color:var(--acc)">$${r.pro.toLocaleString()}/mo</b> proforma${yld}</div>`;
}
// For ranking, treat implausible (>18%) pro-forma yields as low-confidence so bad-units outliers don't top the list.
function rentRoiScore(p){ const r=rentEstimate(p); if(!r||!r.grossYieldPro) return -1e9; return r.grossYieldPro>18 ? -1 : r.grossYieldPro; }

// ===== Field show/hide + list/grid view + extra data points =====
const esc = s => String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
// Every data field the UI can render. card=shows on the grid card · col=available as a List column · def=default-on.
const FIELDS = [
  {k:'score',      label:'Investment score',  card:1, col:1, def:1},
  {k:'deal',       label:'Deal score',        card:1, col:1, def:1},
  {k:'agent',      label:'Agent · contact',   card:1, col:1, def:1},
  {k:'firm',       label:'Listing firm',      card:1, col:1, def:1},
  {k:'type',       label:'Asset type',        card:1, col:1, def:1},
  {k:'units',      label:'Units',             card:1, col:1, def:1},
  {k:'price',      label:'Price',             card:1, col:1, def:1},
  {k:'ppu',        label:'Price / unit',      card:1, col:1, def:1},
  {k:'pricePerSf', label:'Price / SF',        card:1, col:1, def:0},
  {k:'cap',        label:'Cap rate',          card:1, col:1, def:1},
  {k:'verified',   label:'Cap-verified flag', card:1, col:0, def:1},
  {k:'yield',      label:'Rent yield',        card:1, col:1, def:1},
  {k:'grm',        label:'GRM (rent mult.)',  card:1, col:1, def:0},
  {k:'yearBuilt',  label:'Year built',        card:1, col:1, def:0},
  {k:'dom',        label:'Days on market',    card:1, col:1, def:0},
  {k:'zip',        label:'ZIP',               card:0, col:1, def:0},
  {k:'status',     label:'Status badge',      card:1, col:1, def:1},
  {k:'afford',     label:'Affordability',     card:1, col:1, def:1},
  {k:'rec',        label:'AI recommendation', card:1, col:1, def:0},
  {k:'statusline', label:'Listing-date line', card:1, col:0, def:1},
  {k:'brokerflag', label:'Prior-broker flag', card:1, col:0, def:1},
  {k:'txhist',     label:'Transaction history',card:1,col:0, def:0},
  {k:'brokerline', label:'Current broker',    card:1, col:1, def:1},
  {k:'coc',        label:'Cash-on-cash',      card:1, col:1, def:1},
  {k:'dscr',       label:'DSCR',              card:1, col:1, def:1},
  {k:'noi',        label:'Est. NOI',          card:1, col:1, def:1},
  {k:'debt',       label:'Debt service /yr',  card:1, col:1, def:1},
  {k:'cashNeeded', label:'Cash needed',       card:1, col:1, def:1},
  {k:'cashFlow',   label:'Cash flow /yr',     card:1, col:1, def:1},
  {k:'thesis',     label:'AI thesis',         card:1, col:0, def:1},
  {k:'risks',      label:'Risks',             card:1, col:0, def:0},
  {k:'upside',     label:'Upside note',       card:1, col:0, def:1},
  {k:'demo',       label:'Demographics',      card:1, col:0, def:1},
  {k:'rent',       label:'Rent estimate',     card:1, col:1, def:1},
  {k:'links',      label:'Listing links',     card:1, col:0, def:1},
  {k:'beds',       label:'Beds (condo)',       card:1, col:1, def:0},
  {k:'baths',      label:'Baths (condo)',      card:1, col:1, def:0},
  {k:'sqft',       label:'Sq Ft',              card:1, col:1, def:0},
  {k:'hoa',        label:'HOA / mo (condo)',   card:1, col:1, def:0},
  {k:'warr',       label:'Warrantability',     card:1, col:1, def:0},
  {k:'broker',     label:'Broker (condo)',     card:1, col:1, def:0},
  {k:'assessed',   label:'Assessed value ✦',   card:1, col:0, def:0, assessor:1},
  {k:'landimp',    label:'Land / Improvement ✦',card:1, col:0, def:0, assessor:1},
  {k:'usecode',    label:'Assessor use code ✦', card:1, col:0, def:0, assessor:1},
  {k:'priorsale',  label:'Prior sale ✦',        card:1, col:0, def:0, assessor:1},
];
const VIS = (function(){ let s={}; try{ s=JSON.parse(localStorage.getItem('cre_fields')||'{}'); }catch(e){} const o={}; FIELDS.forEach(f=>o[f.k]=(f.k in s)?!!s[f.k]:!!f.def); return o; })();
function vis(k){ return !!VIS[k]; }
function saveVis(){ try{ localStorage.setItem('cre_fields', JSON.stringify(VIS)); }catch(e){} }
let view = localStorage.getItem('cre_view') || 'grid';
let colSort = null;  // {key, dir} — click-to-sort on a List column; overrides the sort dropdown when set
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(); }
let heat = localStorage.getItem('cre_heat') || 'none';
function heatVal(p){ const f=p.finance||{}; return heat==='deal'?(p.deal&&p.deal.score):heat==='cap'?p.cap_rate:heat==='coc'?f.coc:null; }
function heatColor(t){ t=Math.max(0,Math.min(1,t)); return `hsl(${Math.round(t*120)},70%,45%)`; }
function applyHeat(rows){
  const lg=$('#heatlegend'); if(lg){ lg.classList.toggle('on',heat!=='none'); if(heat!=='none'){ lg.querySelector('.hl').textContent={deal:'Deal score',cap:'Cap rate',coc:'Cash-on-cash'}[heat]; } }
  if(heat==='none') return;
  const vals=rows.map(heatVal).filter(v=>v!=null&&isFinite(v)); if(!vals.length) return;
  const min=Math.min(...vals), max=Math.max(...vals), span=(max-min)||1;
  const listMode=$('#grid').classList.contains('listhost');
  rows.forEach(p=>{ const v=heatVal(p);
    const el=listMode?document.querySelector('.listtbl tr[data-id="'+p.id+'"] td.stick'):document.getElementById('card-'+p.id);
    if(!el) return;
    if(v==null||!isFinite(v)){ if(listMode) el.style.boxShadow=''; else el.style.borderLeft=''; return; }
    const col=heatColor((v-min)/span);
    if(listMode) el.style.boxShadow='inset 5px 0 0 '+col; else el.style.borderLeft='6px solid '+col;
  });
}
function buildFieldToggles(){
  const host=$('#fFields'); if(!host) return;
  host.innerHTML = FIELDS.map(f=>`<label class="ftog"><input type="checkbox" data-fk="${f.k}" ${vis(f.k)?'checked':''}><span>${f.label}</span>${f.col?'<span class="colbadge" title="Also shows as a column in List view">▤</span>':''}</label>`).join('');
}
// ---- extra computed data points ----
function ppu(p){ return (p.price&&p.units)?Math.round(p.price/p.units):null; }
function ppsf(p){ return (p.price&&p.sqft)?Math.round(p.price/p.sqft):null; }
function grm(p){ const r=rentEstimate(p); if(!r||!r.std) return null; const annual=r.std*12; return annual?+(p.price/annual).toFixed(1):null; }
function daysOnMarket(p){ const ld=listingDate(p); if(!ld||!ld.exact) return null; const d=Math.round((Date.now()-ld.d.getTime())/86400000); return d>=0?d:null; }
function shortStatus(p){ if(/Under Contract|Pending|Escrow/i.test(p.status)) return p.status; if(/Sold|Closed/i.test(p.status)) return 'Sold'; if(/OUT OF BUDGET/i.test(p.status)) return 'Over budget'; return 'Active'; }
// ---- list-view renderer ----
function listCols(){ return FIELDS.filter(f=>f.col && vis(f.k)); }
function colCell(k,p){ const f=p.finance||{};
  switch(k){
    case 'score': return p.composite??'—';
    case 'deal': return (p.deal&&p.deal.score!=null)?p.deal.score:'—';
    case 'agent': return agentContactCell(p);
    case 'firm': return firmCell(p);
    case 'type': return esc(p.type||'—');
    case 'units': return p.units||'—';
    case 'price': return fmt(p.price);
    case 'ppu': { const v=ppu(p); return v?'$'+v.toLocaleString():'—'; }
    case 'pricePerSf': { const v=ppsf(p); return v?'$'+v.toLocaleString():'—'; }
    case 'cap': return p.cap_rate!=null?p.cap_rate+'%':'—';
    case 'yield': return f.grossYield?f.grossYield+'%':'—';
    case 'grm': { const v=grm(p); return v!=null?v:'—'; }
    case 'yearBuilt': return p.year_built||'—';
    case 'dom': { const v=daysOnMarket(p); return v!=null?v+'d':'—'; }
    case 'zip': return p.zip||'—';
    case 'status': return esc(shortStatus(p));
    case 'afford': return f.affordable?'✓ in budget':'✗ over';
    case 'rec': return (p.qwen&&p.qwen.recommendation)||'—';
    case 'brokerline': { const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()]; const nm=(bh&&bh.current_broker)?(bh.current_broker.name||bh.current_broker.firm):null; return nm?agentCell(nm,(bh.current_broker&&bh.current_broker.firm)||firmLabel(p)):'—'; }
    case 'coc': return f.coc!=null?f.coc+'%':'—';
    case 'dscr': return f.dscr??'—';
    case 'noi': return fmt(f.noi);
    case 'debt': return fmt(f.annualDebtService);
    case 'cashNeeded': return fmt(f.cashNeeded);
    case 'cashFlow': return fmt(f.cashFlow);
    case 'rent': { const r=rentEstimate(p); return r?'$'+r.std.toLocaleString():'—'; }
    case 'beds': return p.beds??'—';
    case 'baths': return p.baths??'—';
    case 'sqft': return p.sqft?(+p.sqft).toLocaleString():'—';
    case 'hoa': return p.hoa?'$'+(+p.hoa).toLocaleString():'—';
    case 'warr': return esc(p.warrantable_status||'—');
    case 'broker': return p.broker_name?agentCell(p.broker_name,firmLabel(p)):'—';
    default: return '—';
  }
}
function renderList(rows){
  const cols=listCols();
  const arrow=k=>colSort&&colSort.key===k?(colSort.dir>0?' ▲':' ▼'):'';
  const head='<th scope="col" class="stick sortable" data-sortk="addr" title="Sort by address">Property'+arrow('addr')+'</th>'+cols.map(c=>`<th scope="col" class="sortable" data-sortk="${c.k}" title="Sort by ${esc(c.label)}">${esc(c.label)}${arrow(c.k)}</th>`).join('');
  const body=rows.map(p=>`<tr class="lrow" data-id="${p.id}" title="Click to open the agent window (contact · letters · find · comps · history)">`
    +`<td class="stick"><span class="exptog" aria-hidden="true" title="Open agent window">⧉</span><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button> <span class="la">${esc(p.address)}</span><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div></td>`
    +cols.map(c=>`<td>${colCell(c.k,p)}</td>`).join('')+`</tr>`).join('');
  return `<div class="listwrap"><table class="listtbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
}
// The inline panel that opens under a list row: deep property/agent searches + the agent CRM
// (editable contact info, mark-contacted, saved-letter history, compose). Reuses the same
// #lc-/#hist- containers the card view uses so fetchLiveComps/fetchHistory work unchanged.
function expandBody(p){
  const a=agentInfo(p), firm=a.firm, id=p.id;
  // In-app actions ONLY — no external links. Live comps + Property history render inline in the
  // panel below; "More from this firm" filters the grid in place.
  const deep=[];
  deep.push(`<button class="lcbtn" data-id="${id}" title="Pull fresh comps inline (~$0.03)">🔄 Live comps</button>`);
  deep.push(`<button class="histbtn" data-id="${id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (inline, free)">📜 Property history</button>`);
  if(firm&&firm!=='Unknown') deep.push(`<button data-firmfilter="${esc(firm).replace(/"/g,'&quot;')}" title="Show only ${esc(firm)} listings">▦ More from ${esc(firm)}</button>`);
  // Agent summary + notes preview. Full editing (info, notes, letters, draft) lives in the inline
  // agent window that ✎ Open launches — no external search, no new tabs.
  const summary=`<div class="crm-summary">${a.name?`<span class="aname">${esc(a.name)}</span> `:'<span class="amut">no agent on file — add via ✎ Open</span> '}${contactBits(a)}`
    +`${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}${a.letters.length?`<span class="achip">✎ ${a.letters.length} saved</span>`:''}</div>`
    +(a.notes?`<div class="crm-note">🗒 ${esc(a.notes)}</div>`:'');
  return `<div class="src">${deep.join(' ')}</div>`
    +`<div class="crm">${summary}
        <div class="crm-acts">
          <button class="abtn contactbtn" data-contact="${id}">✎ Open agent window</button>
          <button class="abtn markbtn" data-mark="${id}">${a.contacted_at?'↻ Update contacted date':'✓ Mark contacted'}</button>
        </div>
      </div>`
    +`<div class="lcout" id="lc-${id}"></div><div class="histout" id="hist-${id}"></div>`;
}
// raw (unformatted) value for a field, for generic column sorting
function fieldVal(p,k){ const f=p.finance||{};
  switch(k){
    case 'addr': return p.address; case 'score': return p.composite; case 'deal': return p.deal&&p.deal.score;
    case 'agent': { const a=agentInfo(p); return a.name||('~'); } // contactless rows sort last (~ > letters)
    case 'firm': return firmLabel(p); case 'type': return p.type; case 'units': return p.units; case 'price': return p.price;
    case 'ppu': return ppu(p); case 'pricePerSf': return ppsf(p); case 'cap': return p.cap_rate; case 'yield': return f.grossYield;
    case 'grm': return grm(p); case 'yearBuilt': return p.year_built; case 'dom': return daysOnMarket(p); case 'zip': return p.zip;
    case 'status': return shortStatus(p); case 'afford': return f.affordable?1:0; case 'rec': return p.qwen&&p.qwen.recommendation;
    case 'coc': return f.coc; case 'dscr': return f.dscr; case 'noi': return f.noi; case 'debt': return f.annualDebtService;
    case 'cashNeeded': return f.cashNeeded; case 'cashFlow': return f.cashFlow;
    case 'rent': { const r=rentEstimate(p); return r?r.std:null; }
    case 'beds': return p.beds; case 'baths': return p.baths; case 'sqft': return p.sqft; case 'hoa': return p.hoa;
    case 'warr': return p.warrantable_status; case 'broker': return p.broker_name;
    default: return null;
  }
}
function sortRows(r){
  if(colSort){ const k=colSort.key, d=colSort.dir;
    return r.sort((a,b)=>{ let x=fieldVal(a,k), y=fieldVal(b,k);
      const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
      if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
      if(x<y) return -d; if(x>y) return d; return 0; });
  }
  return r.sort(SORTS[$('#sort').value]||SORTS.composite);
}

// ---- Unified Deal Score (0-100) — DealScore.compute() over the signals we already have. ----
// Gathers: pro-forma gross yield (Census rent estimate), cap rate, in-place DSCR (finance.js),
// a PROXY DSCR when no cap is disclosed (est NOI from pro-forma rent vs the active-scenario debt
// service), and the assumable-FHA/VA bonus. Transparent + labeled; never NaN. Cached on the row.
function dealScore(p){
  const r = rentEstimate(p);
  const grossYieldPro = (r && r.grossYieldPro != null && isFinite(r.grossYieldPro)) ? r.grossYieldPro : null;
  // Proxy DSCR only when there is NO in-place cap (else the real DSCR carries it). Estimate in-place
  // NOI as pro-forma annual gross × 0.6 (a 40% commercial expense ratio) and compare to debt service.
  let proxyDscr = null;
  const f = p.finance;
  if (p.cap_rate == null && r && r.pro && f && f.annualDebtService > 0){
    const estNoi = r.pro * 12 * 0.6;
    proxyDscr = +(estNoi / f.annualDebtService).toFixed(2);
  }
  return DealScore.compute({
    grossYieldPro,
    cap_rate: p.cap_rate,
    verified: p.verified,
    dscr: f ? f.dscr : null,
    proxyDscr,
    assum: p.assum,           // present only if the geocode pipeline tagged this listing (heuristic)
    fhaApproved: p.fhaApproved
  });
}
// LoopNet cross-reference for EVERY listing — free deep-links ($0, no scrape). Mirrors scripts/sources/loopnet.js.
const LN_TYPE={'Multifamily':'multifamily','Office':'office','Retail':'retail','Industrial':'industrial','Mixed-use':'mixed-use','Mixed Use':'mixed-use','Land':'land'};
function loopnetLink(p){
  const sl=s=>String(s||'').toLowerCase().trim().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
  const city=p.city?`${sl(p.city)}-ca`:'los-angeles-ca';
  const ptype=LN_TYPE[p.type]||'';
  // exact: site-search resolves the specific LoopNet listing for this address if it exists.
  const exact=`https://www.google.com/search?q=${encodeURIComponent(`site:loopnet.com "${[p.address,p.city].filter(Boolean).join(' ')}"`)}`;
  const browse=ptype?`https://www.loopnet.com/search/${ptype}/${city}/for-sale/`:`https://www.loopnet.com/search/commercial-real-estate/${city}/for-sale/`;
  return `<a href="${exact}" target="_blank" rel="noopener noreferrer" title="Find this exact property on LoopNet (CoStar) — free search, opens in a new tab">LoopNet ↗</a> <a href="${browse}" target="_blank" rel="noopener noreferrer" class="small" title="Browse comparable ${p.type||'commercial'} for-sale on LoopNet in ${p.city||'LA'}" style="opacity:.7">(comps)</a>`;
}
// Rich filter state
const F = { types:new Set(), firms:new Set(), cityQuery:'', priceMin:null, priceMax:null,
            capMin:0, unitsMin:null, yearMin:null, status:'all', afford:false, unwarrantable:false, nonqm:false, shortlist:false };

function recompute(){
  const A = SCEN[scenario];
  DATA.ranked.forEach(p=>{
    p.finance = CREFin.finance(p, A);
    const fsc = CREFin.financeScore(p, p.finance);
    p.financeScore = fsc.score; p.financeConf = fsc.conf;
    p.composite = CREFin.composite(p, p.finance, fsc.score, p.qwenScore);
    p.deal = dealScore(p);   // {score, parts, confidence, coverage} — cached for badge + sort + tooltip
  });
  DATA.ranked.sort((a,b)=>b.composite-a.composite);
  DATA.ranked.forEach((p,i)=>p.rank=i+1);
}

function firmLabel(p){
  if(p.firm) return p.firm;
  const d=(p.source||'').replace(/^https?:\/\/(www\.)?/,'').split('/')[0]||'';
  const map={'crexi.com':'Crexi','loopnet.com':'LoopNet','compass.com':'Compass','remax.com':'RE/MAX',
    'coldwellbankerhomes.com':'Coldwell Banker','marcusmillichap.com':'Marcus & Millichap',
    'lee-associates.com':'Lee & Associates','colliers.com':'Colliers','cbre.com':'CBRE','jll.com':'JLL'};
  if(map[d]) return map[d];
  return d ? d.split('.')[0].replace(/^./,c=>c.toUpperCase()) : 'Unknown';
}

// ── Agent-contact CRM (front-end) ──────────────────────────────────────────────────
// CONTACTS[listingId] = {name,phone,email,firm,contacted_at,letters:[]}. Loaded once at
// boot from /api/contacts; every save re-writes the record here so the grid re-renders
// without a reload. Sending a letter is NOT done here — letters are SAVED only.
window.CONTACTS = window.CONTACTS || {};
fetch('/api/contacts').then(r=>r.json()).then(j=>{ window.CONTACTS=j.contacts||{}; if(window.DATA_READY) render(); }).catch(()=>{});

// ── Firm Instagram (global, link-free) ── each brokerage firm's last 3 IG posts,
// rendered INLINE under the agent-contact card (no outside links — Business
// Discovery, thumbnails cached same-origin). Loads once at boot, re-renders on ready.
window.FIRM_IG = window.FIRM_IG || {}; window.FIRM_IG_IDX = window.FIRM_IG_IDX || {};
Promise.all([
  fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
  fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}}))
]).then(([cache,map])=>{ window.FIRM_IG=cache||{}; const idx={};
  for(const [h,al] of Object.entries((map.handles)||{})) for(const a of al) idx[a]=h; window.FIRM_IG_IDX=idx;
  if(window.DATA_READY) render(); }).catch(()=>{});
window.firmHandleOf=function(firm){ const n=String(firm||'').toLowerCase().replace(/[^a-z0-9]/g,''); if(!n) return null;
  if(window.FIRM_IG_IDX[n]) return window.FIRM_IG_IDX[n];
  for(const [a,h] of Object.entries(window.FIRM_IG_IDX)){ if(n===a||(a.length>=5&&n.includes(a))||(a.length>=3&&n.startsWith(a))) return h; } return null; };
window.firmIgInline=function(firm){ const h=window.firmHandleOf(firm); if(!h) return '';
  const c=window.FIRM_IG[h]; if(!c||!c.posts||!c.posts.length) return '';
  const e=s=>String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]));
  const items=c.posts.slice(0,3).map(p=>{ const cap=e((p.caption||'').replace(/\s+/g,' ').slice(0,90))||'(no caption)';
    const when=p.timestamp?String(p.timestamp).slice(0,10):''; const vid=p.media_type==='VIDEO'?'▶ ':'';
    const thumb=p.image?'<img class="figt" loading="lazy" src="'+e(p.image)+'" alt="">':'<span class="figt"></span>';
    return '<div class="figli figli-static">'+thumb+'<span class="figb"><span class="figc">'+vid+cap+'</span><span class="figm">'+(when?when+' · ':'')+'♥ '+(p.likes||0)+' · 💬 '+(p.comments||0)+'</span></span></div>'; }).join('');
  return '<div class="figwrap"><div class="fighd">📷 @'+e(h)+' · firm’s latest 3</div>'+items+'</div>'; };
// Best-known contact info for a listing: the saved CRM record wins, else what the scrape gave us.
function agentInfo(p){
  const c=window.CONTACTS[p.id]||{};
  const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
  const dName=p.broker_name||(bh&&bh.current_broker&&(bh.current_broker.name||bh.current_broker.firm))||'';
  return { name:c.name||dName||'', phone:c.phone||p.broker_phone||'', email:c.email||p.broker_email||'',
           firm:c.firm||firmLabel(p), contacted_at:c.contacted_at||null, notes:c.notes||'', letters:(c.letters||[]) };
}
function gsearch(q){ return 'https://www.google.com/search?q='+encodeURIComponent(q); } // retained; no longer linked out
function safeUrl(u){ u=String(u==null?'':u); return /^https?:\/\//i.test(u)?u:'#'; }
// Firm cell — clicking filters the grid to that firm IN-APP (no external link).
function firmCell(p){ const n=firmLabel(p); if(!n||n==='Unknown') return esc(n||'—');
  return `<span class="flink" data-firmfilter="${esc(n).replace(/"/g,'&quot;')}" title="Show only ${esc(n)} listings">${esc(n)}</span>`; }
// Agent name — plain in-app text (no external search).
function agentCell(name){ return name?esc(name):'—'; }
// Inline phone/email: shown as text with a copy button — no tel:/mailto: that launch outside apps.
function contactBits(a){
  const tel=a.phone?`<span class="ain">📞 ${esc(a.phone)} <button class="copyb" data-copyval="${esc(a.phone)}" title="Copy phone">⧉</button></span>`:'';
  const eml=a.email?`<span class="ain">✉ ${esc(a.email)} <button class="copyb" data-copyval="${esc(a.email)}" title="Copy email">⧉</button></span>`:'';
  return (tel||eml)?`<div class="ainrow">${tel} ${eml}</div>`:'';
}
// The Agent · contact column cell: name + copyable phone/email + Open/Mark buttons + chips.
function agentContactCell(p){
  const a=agentInfo(p);
  const nm=a.name?`<span class="aname">${esc(a.name)}</span>`:`<span class="amut">no agent on file</span>`;
  const contacted=a.contacted_at?`<span class="achip done" title="Marked contacted ${esc(a.contacted_at)}">✓ ${fmtWhen(a.contacted_at)}</span>`:'';
  const lc=a.letters.length?`<span class="achip" title="${a.letters.length} saved letter(s)">✎ ${a.letters.length}</span>`:'';
  const nt=a.notes?`<span class="achip" title="Has notes">🗒 notes</span>`:'';
  const btns=`<span class="acts">`
    +`<button class="abtn contactbtn" data-contact="${p.id}" title="Open the inline agent window — notes, letters, draft">✎ Open</button>`
    +`<button class="abtn markbtn" data-mark="${p.id}" title="${a.contacted_at?'Update the contacted date':'Mark contacted (saves the date)'}">${a.contacted_at?'↻':'✓'} Contacted</button>`
    +`</span>`;
  return `<div class="agentcell">${nm}${contactBits(a)}<div class="chiprow">${contacted}${lc}${nt}</div>${btns}</div>`;
}
function fmtWhen(iso){ try{ return new Date(iso).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}); }catch(_){ return String(iso||'').slice(0,10); } }

// Deal-Score badge + plain-text breakdown tooltip (explains WHY the score is what it is).
function dealBadge(p){
  const d = p.deal; if(!d || d.score==null) return `<span class="b" title="Not enough signal to score this deal (no cap rate, rent estimate, or DSCR available).">⬡ Deal —</span>`;
  const col = DealScore.scoreColor(d.score);
  const lines = d.parts.map(pt=>`• ${pt.label}: ${Math.round(pt.score)}/100 (${pt.conf}, weight ${Math.round(pt.weight)})`).join('\n');
  const tip = `Deal Score ${d.score}/100 — weighted blend of the signals below, re-normalized over what's present (missing signals drop out, never NaN).\nConfidence: ${d.confidence}${d.coverage!=null?` (core coverage ${Math.round(d.coverage*100)}%)`:''}.\n\n${lines}\n\nA screen, not an appraisal. Verify NOI + units in DD.`;
  return `<span class="b deal-badge" style="color:${col};border-color:${col}" title="${tip.replace(/"/g,'&quot;')}">⬡ Deal ${d.score}${d.confidence==='low'||d.confidence==='thin'?' ?':''}</span>`;
}
function card(p){
  const f=p.finance, uc=/Under Contract|Pending/i.test(p.status);
  const firmB = `<span class="b" title="Listing source / brokerage firm">🏢 ${firmLabel(p)}</span>`;
  const aff = f.affordable ? `<span class="b ok">✓ $${(f.cashNeeded/1000).toFixed(0)}k in budget</span>`
                           : `<span class="b bad">✗ needs $${(f.cashNeeded/1000).toFixed(0)}k</span>`;
  const rec = p.qwen && p.qwen.recommendation ? `<span class="b rec-${p.qwen.recommendation}">${p.qwen.recommendation}</span>` : '';
  const statusB = uc ? `<span class="b warn">${p.status}</span>` : (/OUT OF BUDGET/i.test(p.status)?`<span class="b bad">over budget</span>`:`<span class="b ok">Active</span>`);
  const capB = p.cap_rate!=null
     ? `<span class="b">${p.cap_rate}% cap${p.cap_rate_projected?` →${p.cap_rate_projected}%*`:''}</span>`
     : `<span class="b warn">cap n/d</span>`;
  const verB = p.verified===true ? `<span class="b ok" title="In-place NOI confirmed against MLS">✓ cap verified</span>`
     : (p.cap_rate!=null ? `<span class="b" title="Broker-stated cap, not independently verified">broker cap</span>`
     : `<span class="b warn" title="No in-place income disclosed">⚠ no income data</span>`);
  // Broker-history enrichment — keyed by listing id then by address fallback
  const bh = window.BROKER_HIST[p.id] || window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
  const brokerFlagBadge = bh && bh.broker_flag
    ? `<span class="brokerflag" title="${esc(bh.flag_reason||'')}">⚑ Prior broker on file</span>`
    : '';
  function txHistHTML(bh){
    if(!bh || !bh.tx_history || !bh.tx_history.length) return '';
    const rows = bh.tx_history.map(t=>{
      const price = t.price ? `<span class="tx-price">$${Number(t.price).toLocaleString()}</span>` : '';
      const src = t.tier==='A' ? ' <span style="color:var(--acc);font-size:9px">[A]</span>' : ' <span style="color:var(--mut);font-size:9px">[B]</span>';
      return `<div class="tx-row"><span class="tx-date">${esc(String(t.date||'').slice(0,10))}</span><span class="tx-ev">${esc(t.event||'')}${src}</span>${price}</div>`;
    }).join('');
    // Also show the last deed recording from assessor if we have AIN
    const ain = bh.ain ? `<div style="font-size:10px;color:var(--mut);margin-top:2px">AIN ${bh.ain} · built ${bh.assessor_year_built||'—'} · ${bh.assessor_units||'?'} units (assessor)</div>` : '';
    return `<div class="txhist">${rows}${ain}</div>`;
  }
  function brokerLineHTML(bh){
    if(!bh || !bh.current_broker) return '';
    const b = bh.current_broker;
    if(!b.name && !b.firm) return '';
    const nameStr = b.name ? `<strong>${esc(b.name)}</strong>` : '';
    const firmStr = b.firm ? esc(b.firm) : '';
    const dreStr  = b.dre  ? ` · DRE ${esc(b.dre)}` : '';
    const phoneStr = b.phone ? ` · ${esc(b.phone)}` : '';
    const mlsStr  = b.mls  ? ` · MLS ${esc(b.mls)}` : '';
    const parts = [nameStr, firmStr].filter(Boolean).join(' · ');
    const tierBadge = b.tier==='A' ? ' <span style="color:var(--acc);font-size:9px">[A]</span>' : '';
    return `<div class="brokerline">🧑‍💼 ${parts}${dreStr}${phoneStr}${mlsStr}${tierBadge}</div>`;
  }
  return `<div class="card ${uc?'uc':''}" id="card-${p.id}">
    <div class="top">
      <div><div class="rank">#${p.rank} · score</div><div class="addr">${p.address}</div><div class="city">${p.city}, CA ${p.zip||''}</div></div>
      <div style="display:flex;align-items:flex-start;gap:8px">
        <button class="copybtn" data-copy="${p.id}" aria-label="Copy listing summary" title="Copy summary to clipboard">📋</button>
        <button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist this listing">★</button>
        ${vis('score')?`<div class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' · ai '+p.qwenScore:''}</small></div>`:''}
      </div>
    </div>
    <div class="badges">${vis('deal')?dealBadge(p):''}${vis('firm')?firmB:''}${vis('type')?`<span class="b type">${p.type}</span>`:''}${vis('units')?`<span class="b">${p.units} unit${p.units>1?'s':''}</span>`:''}${vis('price')?`<span class="b">${fmt(p.price)}</span>`:''}${vis('ppu')&&ppu(p)&&!p.isCondo?`<span class="b" title="Price per unit">$${ppu(p).toLocaleString()}/unit</span>`:''}${vis('pricePerSf')&&ppsf(p)?`<span class="b" title="Price per building SF">$${ppsf(p).toLocaleString()}/sf</span>`:''}${vis('cap')?capB:''}${vis('verified')?verB:''}${vis('yield')&&f.grossYield?`<span class="b ok" title="Annual gross rent ÷ price">${f.grossYield}% rent yld</span>`:''}${vis('grm')&&grm(p)!=null?`<span class="b" title="Gross rent multiplier — price ÷ annual gross rent (lower = cheaper vs rent)">GRM ${grm(p)}</span>`:''}${vis('yearBuilt')&&p.year_built?`<span class="b" title="Year built">🏗 ${p.year_built}</span>`:''}${vis('dom')&&daysOnMarket(p)!=null?`<span class="b" title="Days since listed">${daysOnMarket(p)}d on mkt</span>`:''}${vis('beds')&&p.beds!=null?`<span class="b">${p.beds} bd</span>`:''}${vis('baths')&&p.baths!=null?`<span class="b">${p.baths} ba</span>`:''}${vis('sqft')&&p.sqft?`<span class="b">${(+p.sqft).toLocaleString()} sf</span>`:''}${vis('hoa')&&p.hoa?`<span class="b" title="HOA dues">$${(+p.hoa).toLocaleString()}/mo</span>`:''}${vis('warr')&&p.warrantable_status?`<span class="b ${p.warrantable_status==='fha_approved'?'ok':'warn'}" title="Condo warrantability (FHA-approval proxy)">${p.warrantable_status==='fha_approved'?'✓ warrantable':'⚑ '+p.warrantable_status}</span>`:''}${vis('broker')&&p.broker_name?`<span class="b" title="Listing broker">🧑‍💼 ${esc(p.broker_name)}</span>`:''}${vis('status')?statusB:''} ${vis('afford')?aff:''} ${vis('rec')?rec:''}</div>
    ${vis('brokerflag')?brokerFlagBadge:''}
    ${vis('statusline')?statusLine(p):''}
    ${vis('txhist')?txHistHTML(bh):''}
    ${vis('brokerline')?brokerLineHTML(bh):''}
    ${(vis('coc')||vis('dscr')||vis('noi')||vis('debt')||vis('cashNeeded')||vis('cashFlow'))?`<div class="metrics">
      ${vis('coc')?`<div><span class="k">Cash-on-cash</span><span class="v ${cls(f.coc,6,3)}">${pct(f.coc)}</span></div>`:''}
      ${vis('dscr')?`<div><span class="k">DSCR</span><span class="v ${cls(f.dscr,1.25,1.0)}">${f.dscr??'—'}</span></div>`:''}
      ${vis('noi')?`<div><span class="k">Est. NOI</span><span class="v">${fmt(f.noi)}</span></div>`:''}
      ${vis('debt')?`<div><span class="k">${scenario==='cash'?'Debt svc':'Debt svc/yr'}</span><span class="v">${fmt(f.annualDebtService)}</span></div>`:''}
      ${vis('cashNeeded')?`<div><span class="k">Cash needed</span><span class="v ${f.affordable?'pos':'neg'}">${fmt(f.cashNeeded)}</span></div>`:''}
      ${vis('cashFlow')?`<div><span class="k">Cash flow/yr</span><span class="v ${cls(f.cashFlow,1,0)}">${fmt(f.cashFlow)}</span></div>`:''}
    </div>`:''}
    ${vis('thesis')&&p.qwen?.thesis?`<div class="thesis">${esc(p.qwen.thesis)}</div>`:''}
    ${vis('risks')&&p.qwen?.risks?.length?`<div><span class="small">Risks</span><ul class="lst">${p.qwen.risks.map(r=>`<li>${esc(r)}</li>`).join('')}</ul></div>`:''}
    ${vis('upside')&&p.upside_note?`<div class="small">💡 ${esc(p.upside_note)}</div>`:''}
    ${vis('demo')?demoLine(p):''}
    ${vis('rent')?rentLine(p):''}
    ${anyAssessorVisible()?`<div class="assessline small" data-addr="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}">📋 assessor…</div>`:''}
    ${cardAgentBlock(p)}
  </div>`;
}
// Per-listing agent-contact block for the CARD view — every card (any asset type) gets the same
// contact modal + mark-contacted as the list rows. Compact: name/phone/email + chips + buttons.
function cardAgentBlock(p){
  const a=agentInfo(p), id=p.id;
  const nm=a.name?`<span class="aname">${esc(a.name)}</span>`:`<span class="amut">no agent on file</span>`;
  const chips=`${a.contacted_at?`<span class="achip done" title="Contacted ${esc(a.contacted_at)}">✓ ${fmtWhen(a.contacted_at)}</span>`:''}${a.letters.length?`<span class="achip" title="${a.letters.length} saved letter(s)">✎ ${a.letters.length}</span>`:''}${a.notes?`<span class="achip" title="Has notes">🗒 notes</span>`:''}`;
  return `<div class="cardagent" id="cardagent-${id}">`
    +`<div class="ca-top">🧑‍💼 ${nm} ${chips}</div>`
    +contactBits(a)
    +window.firmIgInline(a.firm)
    +`<div class="acts"><button class="abtn contactbtn" data-contact="${id}" title="Open the inline agent window — notes, letters, draft">✎ Open</button>`
    +`<button class="abtn markbtn" data-mark="${id}" title="${a.contacted_at?'Update contacted date':'Mark contacted (saves the date)'}">${a.contacted_at?'↻':'✓'} Contacted</button></div>`
    +`</div>`;
}

// ---- Non-QM target set: unwarrantable condos + small multifamily (1–9u) + mixed-use ----
function isNonQM(p){
  const t=(p.type||'').toLowerCase();
  if(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved') return true;
  if(/multi/.test(t) && p.units>=1 && p.units<=9) return true;
  if(/mixed/.test(t)) return true;
  return false;
}
// ---- lazy assessor enrichment (LA County parcel roll via /api/property-history, free) ----
function anyAssessorVisible(){ return FIELDS.some(f=>f.assessor && vis(f.k)); }
window.ASSESS = window.ASSESS || {};
let assessObs = null;
function assessLine(d){
  const a=d&&(d.assessor||d.assessorArchive); const bits=[];
  if(vis('assessed')){ const v=a&&(a.roll_total_value||a.total_value); if(v) bits.push('💰 assessed $'+(+v).toLocaleString()); }
  if(vis('landimp')&&a){ const l=a.roll_land_value, im=a.roll_imp_value; if(l||im) bits.push('land $'+(+(l||0)).toLocaleString()+' / imp $'+(+(im||0)).toLocaleString()); }
  if(vis('usecode')&&a){ const u=a.use_desc2||a.use_desc1||a.use_description; if(u) bits.push('🏷 '+u); }
  if(vis('priorsale')&&d&&d.sales&&d.sales.length){ const s=d.sales[0]; bits.push('last sold $'+(+(s.sold_price||0)).toLocaleString()+' · '+String(s.sold_date||'').slice(0,10)); }
  return bits.length ? ('📋 '+bits.join(' · ')) : '<span class="na">📋 no assessor record on file</span>';
}
function enrichAssessor(){
  if(assessObs){ assessObs.disconnect(); assessObs=null; }
  if(!anyAssessorVisible()) return;
  const els=[...document.querySelectorAll('.assessline')];
  const queue=[]; let active=0; const MAX=6;
  function pump(){ while(active<MAX && queue.length){ fetchOne(queue.shift()); } }
  function fetchOne(el){
    const addr=el.dataset.addr, city=el.dataset.city, key=(addr+'|'+city).toLowerCase();
    if(window.ASSESS[key]!==undefined){ el.innerHTML=assessLine(window.ASSESS[key]); pump(); return; }
    active++;
    fetch('/api/property-history?address='+encodeURIComponent(addr)+'&city='+encodeURIComponent(city||''))
      .then(r=>r.json()).then(d=>{ window.ASSESS[key]=d; el.innerHTML=assessLine(d); })
      .catch(()=>{ el.innerHTML='<span class="na">📋 assessor lookup failed</span>'; })
      .finally(()=>{ active--; pump(); });
  }
  // As cards approach the viewport, queue them for enrichment (concurrency-capped).
  assessObs=new IntersectionObserver((ents,obs)=>{ ents.forEach(en=>{ if(en.isIntersecting){ obs.unobserve(en.target); queue.push(en.target); pump(); } }); },{rootMargin:'400px'});
  els.forEach(el=>assessObs.observe(el));
  // Eager first batch so the top of the grid always resolves (no dependence on a scroll event).
  els.slice(0,30).forEach(el=>{ assessObs.unobserve(el); queue.push(el); }); pump();
}

// ---- the rich filter pass ----
function applyFilters(list){
  return list.filter(p=>{
    if(F.types.size && !F.types.has(p.type)) return false;
    if(F.firms.size && !F.firms.has(firmLabel(p))) return false;
    if(F.cityQuery){ const hay=((p.city||'')+' '+(p.address||'')).toLowerCase(); if(!hay.includes(F.cityQuery)) return false; }
    if(F.priceMin!=null && p.price<F.priceMin) return false;
    if(F.priceMax!=null && p.price>F.priceMax) return false;
    if(F.capMin>0 && !(p.cap_rate!=null && p.cap_rate>=F.capMin)) return false;
    if(F.unitsMin!=null && !(p.units>=F.unitsMin)) return false;
    if(F.yearMin!=null && !(p.year_built && p.year_built>=F.yearMin)) return false;
    if(F.status==='active' && /Under Contract|Pending|OUT OF BUDGET/i.test(p.status)) return false;
    if(F.afford && !p.finance.affordable) return false;
    if(F.unwarrantable && !(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved')) return false;
    if(F.nonqm && !isNonQM(p)) return false;
    if(F.shortlist && !SHORT.has(p.id)) return false;
    return true;
  });
}
// Match everything EXCEPT one facet dimension — powers live facet counts (Amazon-style narrowing).
function passExcept(p, skip){
  if(skip!=='type' && F.types.size && !F.types.has(p.type)) return false;
  if(skip!=='firm' && F.firms.size && !F.firms.has(firmLabel(p))) return false;
  if(skip!=='city' && F.cityQuery){ const hay=((p.city||'')+' '+(p.address||'')).toLowerCase(); if(!hay.includes(F.cityQuery)) return false; }
  if(F.priceMin!=null && p.price<F.priceMin) return false;
  if(F.priceMax!=null && p.price>F.priceMax) return false;
  if(F.capMin>0 && !(p.cap_rate!=null && p.cap_rate>=F.capMin)) return false;
  if(F.unitsMin!=null && !(p.units>=F.unitsMin)) return false;
  if(F.yearMin!=null && !(p.year_built && p.year_built>=F.yearMin)) return false;
  if(F.status==='active' && /Under Contract|Pending|OUT OF BUDGET/i.test(p.status)) return false;
  if(F.afford && !p.finance.affordable) return false;
  if(F.unwarrantable && !(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved')) return false;
  if(F.nonqm && !isNonQM(p)) return false;
  if(F.shortlist && !SHORT.has(p.id)) return false;
  return true;
}
function updateFacetCounts(){
  const tc={}, fc={}, cc={};
  DATA.ranked.forEach(p=>{
    if(passExcept(p,'type')) tc[p.type]=(tc[p.type]||0)+1;
    if(passExcept(p,'firm')){ const fl=firmLabel(p); fc[fl]=(fc[fl]||0)+1; }
    if(p.city && passExcept(p,'city')) cc[p.city]=(cc[p.city]||0)+1;
  });
  const paint=(sel,attr,map)=>$$(sel).forEach(c=>{ const key=c.dataset[attr]; if(key==null) return; const ct=c.querySelector('.ct'); const n=map[key]||0; if(ct) ct.textContent=n; c.classList.toggle('zero', n===0 && !c.classList.contains('active')); });
  paint('#fType .chip','t',tc); paint('#fFirm .chip','fm',fc); paint('#fCityChips .chip','cy',cc);
}
function activeFilterCount(){
  let n=0;
  if(F.types.size) n++; if(F.firms.size) n++; if(F.cityQuery) n++;
  if(F.priceMin!=null||F.priceMax!=null) n++; if(F.capMin>0) n++;
  if(F.unitsMin!=null) n++; if(F.yearMin!=null) n++;
  if(F.status!=='all') n++; if(F.afford) n++; if(F.unwarrantable) n++; if(F.nonqm) n++; if(F.shortlist) n++;
  return n;
}

const SORTS={ deal:(a,b)=>dealNum(b)-dealNum(a), composite:(a,b)=>b.composite-a.composite, coc:(a,b)=>num(b.finance.coc)-num(a.finance.coc),
  yield:(a,b)=>num(b.finance.grossYield)-num(a.finance.grossYield),
  cap:(a,b)=>num(b.cap_rate)-num(a.cap_rate), dscr:(a,b)=>num(b.finance.dscr)-num(a.finance.dscr),
  priceAsc:(a,b)=>a.price-b.price, priceDesc:(a,b)=>b.price-a.price,
  cashNeeded:(a,b)=>a.finance.cashNeeded-b.finance.cashNeeded, unitsDesc:(a,b)=>num(b.units)-num(a.units),
  rentRoi:(a,b)=>rentRoiScore(b)-rentRoiScore(a) };
const num=v=>v==null?-1e9:v;
const dealNum=p=>(p.deal && p.deal.score!=null) ? p.deal.score : -1e9;  // unscored sink to the bottom

// ---- active-filter summary bar (removable chips + reset) ----
function renderActiveBar(){
  const bar=$('#activebar'); if(!bar) return; const chips=[];
  F.types.forEach(t=>chips.push(['type:'+t, t]));
  F.firms.forEach(f=>chips.push(['firm:'+f, '🏢 '+f]));
  if(F.cityQuery) chips.push(['city', 'city ~ “'+F.cityQuery+'”']);
  if(F.priceMin!=null) chips.push(['priceMin', '≥ $'+(+F.priceMin).toLocaleString()]);
  if(F.priceMax!=null) chips.push(['priceMax', '≤ $'+(+F.priceMax).toLocaleString()]);
  if(F.capMin>0) chips.push(['capMin', 'cap ≥ '+F.capMin+'%']);
  if(F.unitsMin!=null) chips.push(['unitsMin', '≥ '+F.unitsMin+' units']);
  if(F.yearMin!=null) chips.push(['yearMin', 'built ≥ '+F.yearMin]);
  if(F.status!=='all') chips.push(['status', 'active only']);
  if(F.afford) chips.push(['afford', 'in budget']);
  if(F.unwarrantable) chips.push(['unwarrantable', '⚑ unwarrantable']);
  if(F.nonqm) chips.push(['nonqm', '◆ non-QM']);
  if(F.shortlist) chips.push(['shortlist', '⭐ shortlist']);
  bar.innerHTML = chips.length
    ? '<span class="lbl">Filtering</span>'+chips.map(([k,l])=>`<span class="afchip" data-clear="${k.replace(/"/g,'&quot;')}">${esc(l)} <span class="xx">✕</span></span>`).join('')+'<button class="afreset" id="afreset">Reset all ✕</button>'
    : '';
}
function clearFilter(spec){
  if(spec.startsWith('type:')){ F.types.delete(spec.slice(5)); if(!F.types.has('Condo')) F.unwarrantable=false; }
  else if(spec.startsWith('firm:')){ F.firms.delete(spec.slice(5)); }
  else if(spec==='city'){ F.cityQuery=''; if($('#fCity')) $('#fCity').value=''; }
  else if(spec==='priceMin'){ F.priceMin=null; if($('#fPriceMin')) $('#fPriceMin').value=''; }
  else if(spec==='priceMax'){ F.priceMax=null; if($('#fPriceMax')) $('#fPriceMax').value=''; }
  else if(spec==='capMin'){ F.capMin=0; if($('#fCap')){ $('#fCap').value=0; $('#capVal').textContent='any'; } }
  else if(spec==='unitsMin'){ F.unitsMin=null; if($('#fUnits')) $('#fUnits').value=''; }
  else if(spec==='yearMin'){ F.yearMin=null; if($('#fYear')) $('#fYear').value=''; }
  else if(spec==='status'){ F.status='all'; }
  else if(spec==='afford'){ F.afford=false; if($('#fAfford')) $('#fAfford').checked=false; }
  else if(spec==='unwarrantable'){ F.unwarrantable=false; }
  else if(spec==='nonqm'){ F.nonqm=false; }
  else if(spec==='shortlist'){ F.shortlist=false; }
  syncChips(); render();
}
// ---- shareable URL state (every filter/sort/view/scenario ↔ query string) ----
function stateToURL(){
  const p=new URLSearchParams();
  if(F.types.size) p.set('t',[...F.types].join(','));
  if(F.firms.size) p.set('fm',[...F.firms].join('~'));
  if(F.cityQuery) p.set('city',F.cityQuery);
  if(F.priceMin!=null) p.set('pmin',F.priceMin);
  if(F.priceMax!=null) p.set('pmax',F.priceMax);
  if(F.capMin>0) p.set('cap',F.capMin);
  if(F.unitsMin!=null) p.set('u',F.unitsMin);
  if(F.yearMin!=null) p.set('yr',F.yearMin);
  if(F.status!=='all') p.set('st',F.status);
  if(F.afford) p.set('aff','1');
  if(F.unwarrantable) p.set('unw','1');
  if(F.nonqm) p.set('nq','1');
  if(scenario!=='25') p.set('sc',scenario);
  const so=$('#sort')&&$('#sort').value; if(so && so!=='deal') p.set('sort',so);
  if(colSort) p.set('cs',colSort.key+':'+(colSort.dir>0?'a':'d'));
  if(heat!=='none') p.set('ht',heat);
  if(view!=='grid') p.set('v',view);
  const qs=p.toString();
  // NOTE: the app declares a local `history` (chat log) that shadows window.history — must qualify.
  try{ window.history.replaceState(null,'',location.pathname+(qs?('?'+qs):'')); }catch(e){}
}
function urlToState(){
  const p=new URLSearchParams(location.search); let any=false;
  if(p.get('t')){ F.types=new Set(p.get('t').split(',').filter(Boolean)); any=true; }
  if(p.get('fm')){ F.firms=new Set(p.get('fm').split('~').filter(Boolean)); any=true; }
  if(p.get('city')){ F.cityQuery=p.get('city'); if($('#fCity'))$('#fCity').value=F.cityQuery; any=true; }
  if(p.get('pmin')){ F.priceMin=+p.get('pmin'); if($('#fPriceMin'))$('#fPriceMin').value=F.priceMin; any=true; }
  if(p.get('pmax')){ F.priceMax=+p.get('pmax'); if($('#fPriceMax'))$('#fPriceMax').value=F.priceMax; any=true; }
  if(p.get('cap')){ F.capMin=+p.get('cap'); if($('#fCap')){$('#fCap').value=F.capMin; $('#capVal').textContent=F.capMin+'%+';} any=true; }
  if(p.get('u')){ F.unitsMin=+p.get('u'); if($('#fUnits'))$('#fUnits').value=F.unitsMin; any=true; }
  if(p.get('yr')){ F.yearMin=+p.get('yr'); if($('#fYear'))$('#fYear').value=F.yearMin; any=true; }
  if(p.get('st')){ F.status=p.get('st'); any=true; }
  if(p.get('aff')==='1'){ F.afford=true; if($('#fAfford'))$('#fAfford').checked=true; any=true; }
  if(p.get('unw')==='1'){ F.unwarrantable=true; any=true; }
  if(p.get('nq')==='1'){ F.nonqm=true; any=true; }
  if(p.get('sc')){ scenario=p.get('sc'); }
  if(p.get('sort')&&$('#sort')){ $('#sort').value=p.get('sort'); if($('#sortTop'))$('#sortTop').value=p.get('sort'); }
  if(p.get('v')){ view=p.get('v'); }
  if(p.get('cs')){ const [k,d]=p.get('cs').split(':'); colSort={key:k,dir:d==='a'?1:-1}; }
  if(p.get('ht')){ heat=p.get('ht'); }
  return any;
}
function csvEsc(v){ v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"'; }
function exportCSV(){
  const cols=listCols();
  const rows=sortRows(applyFilters(DATA.ranked.slice()));
  const head=['Address','City','ZIP'].concat(cols.map(c=>c.label));
  const lines=[head.map(csvEsc).join(',')];
  rows.forEach(p=>{ const row=[p.address,p.city,p.zip].concat(cols.map(c=>fieldVal(p,c.k))); lines.push(row.map(csvEsc).join(',')); });
  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([lines.join('\n')],{type:'text/csv'})); a.download='cre-explorer-'+rows.length+'.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000);
}
function copyShareLink(){ const t=$('#linkbtn'); try{ navigator.clipboard.writeText(location.href); if(t){ const o=t.textContent; t.textContent='✓ Copied'; setTimeout(()=>t.textContent=o,1400); } }catch(e){ if(t) t.textContent='⌘C to copy'; } }
// ---- side-by-side compare of shortlisted listings ----
const CMP=[
  {l:'Type', f:p=>esc(p.type||'—')},
  {l:'Price', f:p=>fmt(p.price)},
  {l:'$/unit', f:p=>{const v=ppu(p);return v?'$'+v.toLocaleString():'—';}},
  {l:'$/SF', f:p=>{const v=ppsf(p);return v?'$'+v.toLocaleString():'—';}},
  {l:'Units', f:p=>p.units??'—'},
  {l:'Cap rate', f:p=>p.cap_rate!=null?p.cap_rate+'%':'—'},
  {l:'DSCR', f:p=>p.finance&&p.finance.dscr!=null?p.finance.dscr:'—'},
  {l:'Cash-on-cash', f:p=>p.finance&&p.finance.coc!=null?p.finance.coc+'%':'—'},
  {l:'Cash needed', f:p=>p.finance?fmt(p.finance.cashNeeded):'—'},
  {l:'Cash flow/yr', f:p=>p.finance?fmt(p.finance.cashFlow):'—'},
  {l:'Est. rent', f:p=>{const e=rentEstimate(p);return e?'$'+e.std.toLocaleString()+'/mo':'—';}},
  {l:'Beds / Baths', f:p=>(p.beds!=null||p.baths!=null)?((p.beds??'—')+' / '+(p.baths??'—')):'—'},
  {l:'SqFt', f:p=>p.sqft?(+p.sqft).toLocaleString():'—'},
  {l:'Year built', f:p=>p.year_built||'—'},
  {l:'Warrantability', f:p=>esc(p.warrantable_status||'—')},
  {l:'Deal score', f:p=>p.deal&&p.deal.score!=null?p.deal.score:'—'},
  {l:'Invest score', f:p=>p.composite??'—'},
];
function openCompare(){
  const ov=$('#cmpov'), body=$('#cmpbody');
  const items=DATA.ranked.filter(p=>SHORT.has(p.id));
  if(items.length<2){ body.innerHTML='<div class="cmphint">Star at least 2 listings (the ★ on any card) to compare them side by side.</div>'; ov.classList.add('on'); return; }
  const head='<tr><th class="cmpk" scope="col"></th>'+items.map(p=>`<th scope="col">${esc(p.address)}<div class="cmpsub">${esc(p.city)}</div></th>`).join('')+'</tr>';
  const rows=CMP.map(m=>`<tr><td class="cmpk">${m.l}</td>`+items.map(p=>`<td>${m.f(p)}</td>`).join('')+'</tr>').join('');
  body.innerHTML=`<div class="cmptools no-print"><button id="cmpcsv">⬇ CSV</button><button id="cmpprint">🖨 Print</button></div><table class="cmptbl"><thead>${head}</thead><tbody>${rows}</tbody></table>`;
  ov.classList.add('on');
}
function decodeEnt(s){ return String(s).replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&#39;/g,"'"); }
function exportCompareCSV(){
  const items=DATA.ranked.filter(p=>SHORT.has(p.id)); if(items.length<2) return;
  const cols=['Address','City'].concat(CMP.map(m=>m.l));
  const lines=[cols.map(csvEsc).join(',')];
  items.forEach(p=>{ const row=[p.address,p.city].concat(CMP.map(m=>decodeEnt(m.f(p)))); lines.push(row.map(csvEsc).join(',')); });
  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([lines.join('\n')],{type:'text/csv'})); a.download='cre-compare-'+items.length+'.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000);
}
function printCompare(){ document.body.classList.add('printing-compare'); window.print(); }
// ---- paginated DOM (perf): render in pages, "Show more" appends ----
let _rows=[], _shown=0;
const PAGE=()=> view==='list'?200:60;
function moreBtn(){ return _shown<_rows.length ? `<div class="showmore-wrap"><button class="showmore" id="showmore">Show ${Math.min(PAGE(),_rows.length-_shown)} more · showing ${_shown} of ${_rows.length}</button></div>` : ''; }
function paintPage(){
  const g=$('#grid'); const r=_rows; const slice=r.slice(0,_shown);
  if(!r.length){ g.className='grid'; g.innerHTML=`<div class="empty">No properties match these filters.<br><span class="small">Loosen a filter or hit “Reset all”.</span></div>`; return; }
  if(view==='list'){ g.className='listhost'; g.innerHTML=renderList(slice)+moreBtn(); }
  else { g.className='grid'; g.innerHTML=slice.map(card).join('')+moreBtn(); enrichAssessor(); }
  applyHeat(slice);
}
// ---- saved searches (name + restore the full shareable-URL state) ----
const SAVED=(function(){ try{ return JSON.parse(localStorage.getItem('cre_saved')||'[]'); }catch(e){ return []; } })();
function saveSaved(){ try{ localStorage.setItem('cre_saved', JSON.stringify(SAVED)); }catch(e){} }
function buildSavedList(){ const el=$('#savedList'); if(!el) return;
  el.innerHTML = SAVED.length
    ? SAVED.map((s,i)=>`<div class="savedrow"><button class="savedname" data-qs="${(s.qs||'').replace(/"/g,'&quot;')}" title="Restore this saved view">📁 ${esc(s.name)}</button><button class="saveddel" data-del="${i}" aria-label="Delete saved search">✕</button></div>`).join('')
    : '<div class="savedempty">None yet — filter, then “Save current view”.</div>';
}
// ---- per-listing clipboard summary (honest fields only) ----
function cardSummary(p){ const f=p.finance||{}; const L=[];
  L.push(`${p.address}, ${p.city} CA ${p.zip||''}`.trim());
  const l2=[p.type, p.units?`${p.units} unit${p.units>1?'s':''}`:null, p.price?fmt(p.price):null].filter(Boolean).join(' · '); if(l2) L.push(l2);
  const m=[]; if(p.cap_rate!=null) m.push('Cap '+p.cap_rate+'%'); if(ppu(p)&&!p.isCondo) m.push('$'+ppu(p).toLocaleString()+'/unit'); if(ppsf(p)) m.push('$'+ppsf(p).toLocaleString()+'/SF'); if(m.length) L.push(m.join(' · '));
  const fin=[]; if(f.dscr!=null) fin.push('DSCR '+f.dscr); if(f.coc!=null) fin.push('CoC '+f.coc+'%'); if(f.cashNeeded!=null) fin.push('Cash needed '+fmt(f.cashNeeded)); if(f.cashFlow!=null) fin.push('Cash flow/yr '+fmt(f.cashFlow)); if(fin.length) L.push(fin.join(' · '));
  const r=rentEstimate(p); const ex=[]; if(r) ex.push('Est. rent $'+r.std.toLocaleString()+'/mo'); if(p.deal&&p.deal.score!=null) ex.push('Deal score '+p.deal.score+'/100'); if(p.warrantable_status) ex.push('Warrantability: '+p.warrantable_status); if(ex.length) L.push(ex.join(' · '));
  if(p.source) L.push(p.source);
  return L.join('\n');
}
function copySummary(id, btn){ const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
  const done=()=>{ if(btn){ btn.textContent='✓'; setTimeout(()=>btn.textContent='📋',1200); } };
  try{ navigator.clipboard.writeText(cardSummary(p)).then(done).catch(done); }catch(e){ done(); }
}
function applyQuick(k){
  if(k==='condo'){ F.types=new Set(['Condo']); }
  else if(k==='mf'){ F.types=new Set(['Multifamily']); }
  else if(k==='nonqm'){ F.nonqm=true; }
  else if(k==='unw'){ F.unwarrantable=true; F.types=new Set(['Condo']); }
  else if(k==='afford'){ F.afford=true; if($('#fAfford')) $('#fAfford').checked=true; }
  else if(k==='short'){ F.shortlist=true; }
  syncChips(); render();
}
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(r){
  const sb=$('#statsbar'); if(!sb) return;
  if(!r||!r.length){ sb.innerHTML=''; return; }
  const prices=r.map(p=>p.price).filter(v=>v>0);
  const ppus=r.map(p=>ppu(p)).filter(v=>v>0);
  const ppsfs=r.map(p=>ppsf(p)).filter(v=>v>0);
  const caps=r.map(p=>p.cap_rate).filter(v=>v!=null);
  const aff=r.filter(p=>p.finance&&p.finance.affordable).length;
  const rents=r.map(p=>{ const e=rentEstimate(p); return e?e.std:null; }).filter(v=>v);
  const pill=(l,v)=>v==null?'':`<span class="statpill"><span class="sl">${l}</span> <b>${v}</b></span>`;
  sb.innerHTML =
    pill('Median price', prices.length?'$'+Math.round(median(prices)).toLocaleString():null)
    +pill('Median $/unit', ppus.length?'$'+Math.round(median(ppus)).toLocaleString():null)
    +pill('Median $/SF', ppsfs.length?'$'+Math.round(median(ppsfs)).toLocaleString():null)
    +pill('Avg cap', caps.length?(caps.reduce((a,b)=>a+(+b),0)/caps.length).toFixed(2)+'% ('+caps.length+' disc.)':null)
    +pill('Median est. rent', rents.length?'$'+Math.round(median(rents)).toLocaleString()+'/mo':null)
    +pill('In budget', aff+' of '+r.length);
}
function render(){
  const g=$('#grid'); const fc=activeFilterCount();
  $('#fcount').textContent=fc; $('#fcount').classList.toggle('on',fc>0);
  { const sb=$('#shortbtn'); if(sb){ sb.textContent='⭐ Shortlist ('+SHORT.size+')'; sb.classList.toggle('shorton',F.shortlist); } }
  renderActiveBar(); stateToURL(); updateFacetCounts();
  // Wait-for-selection: on a clean load (no filter chosen) don't dump 2,134 cards — prompt for a pick.
  if(fc===0 && !F.cityQuery){
    g.className='grid';
    g.innerHTML=`<div class="empty">👈 Expand a filter tab on the left — or start with one of these:
      <div class="quickstart">
        <button class="qchip" data-quick="condo">🏢 Condos</button>
        <button class="qchip" data-quick="mf">🏠 Multifamily</button>
        <button class="qchip" data-quick="nonqm">◆ Non-QM</button>
        <button class="qchip" data-quick="unw">⚑ Unwarrantable</button>
        <button class="qchip" data-quick="afford">✓ In budget</button>
        ${SHORT.size?`<button class="qchip" data-quick="short">⭐ My shortlist (${SHORT.size})</button>`:''}
      </div>
      <span class="small">${(DATA.ranked.length).toLocaleString()} listings loaded · ${DATA.meta.market} — results appear instantly; your active filters show as removable chips up top.</span></div>`;
    $('#count').innerHTML=`<b>${(DATA.ranked.length).toLocaleString()}</b> loaded · choose a filter to view`;
    $('#statsbar').innerHTML='';
    saveState(); return;
  }
  let r = applyFilters(DATA.ranked.slice());
  sortRows(r);
  renderStats(r);
  _rows=r; _shown=Math.min(PAGE(), r.length);   // paginate the DOM; stats/count/facets still use the full set
  paintPage();
  $('#count').innerHTML = `<b>${r.length}</b> of ${DATA.ranked.length} properties${fc?` · ${fc} filter${fc>1?'s':''} active`:''}`;
  $('#foot').textContent = `${scenario==='cash'?'all-cash':SCEN[scenario].downPct+'% down @ '+SCEN[scenario].ratePct+'%'} · * →X% = projected cap after value-add (not in-place). Cap rates broker-stated — verify NOI in DD.`;
  saveState();
}

// ---- build the filter UI from the data ----
function buildFacets(){
  const tCount={}, fCount={}, cCount={};
  DATA.ranked.forEach(p=>{ tCount[p.type]=(tCount[p.type]||0)+1; const fl=firmLabel(p); fCount[fl]=(fCount[fl]||0)+1; if(p.city) cCount[p.city]=(cCount[p.city]||0)+1; });
  const chipHTML=(obj,key)=>Object.entries(obj).sort((a,b)=>b[1]-a[1])
    .map(([k,c])=>`<span class="chip" data-${key}="${k.replace(/"/g,'&quot;')}">${k}<span class="ct">${c}</span></span>`).join('');
  // Condos are merged into the grid as a real 'Condo' asset type (chip comes from the data below).
  // Prepend one special in-grid filter: ⚑ Unwarrantable = Condo AND not FHA-approved (non-QM target).
  const nonqmChip='<span class="chip" data-warr="nonqm" style="border-color:rgba(188,140,255,.55);color:var(--purp,#bc8cff)" title="Non-QM target set: unwarrantable condos + small multifamily (1–9 units) + mixed-use">◆ Non-QM</span>';
  const unwChip='<span class="chip" data-warr="unwarrantable" style="border-color:rgba(248,81,73,.55);color:var(--bad)" title="Non-FHA-approved condos — filters the grid to non-QM targets">⚑ Unwarrantable</span>';
  $('#fType').innerHTML=nonqmChip+unwChip+chipHTML(tCount,'t');
  $('#fFirm').innerHTML=chipHTML(fCount,'fm');
  // top 14 cities as quick chips; the search box covers the rest
  const topCities=Object.entries(cCount).sort((a,b)=>b[1]-a[1]).slice(0,14);
  $('#fCityChips').innerHTML=topCities.map(([k,c])=>`<span class="chip" data-cy="${k.replace(/"/g,'&quot;')}">${k}<span class="ct">${c}</span></span>`).join('');
  // sort options mirrored into the top bar
  $('#sortTop').innerHTML=$('#sort').innerHTML;
}

// ---- chat context ----
function getContext(){
  const lines=[];
  lines.push(`MARKET: ${DATA.meta.market}. Budget: $400,000 down. Active scenario: ${scenario==='cash'?'ALL CASH':SCEN[scenario].downPct+'% down, '+SCEN[scenario].ratePct+'%/'+SCEN[scenario].amortYears+'yr'}.`);
  lines.push(`Active filters: types=[${[...F.types]}], firms=[${[...F.firms]}], city~"${F.cityQuery}", price ${F.priceMin||'any'}-${F.priceMax||'any'}, capMin=${F.capMin}, unitsMin=${F.unitsMin||'any'}, builtAfter=${F.yearMin||'any'}, status=${F.status}, inBudgetOnly=${F.afford}, sort=${$('#sort').value}.`);
  lines.push('RANKED PROPERTIES (current scenario):');
  applyFilters(DATA.ranked).slice(0,120).forEach(p=>{ const f=p.finance;
    lines.push(`#${p.rank} id=${p.id} | ${p.address}, ${p.city} | ${p.type} | $${p.price.toLocaleString()} | ${p.units}u | cap ${p.cap_rate!=null?p.cap_rate+'%':'n/d'}${p.verified===true?' VERIFIED':''} | firm ${firmLabel(p)} | CoC ${f.coc!=null?f.coc+'%':'n/a'} | DSCR ${f.dscr??'n/a'} | cash needed $${f.cashNeeded.toLocaleString()} ${f.affordable?'(in budget)':'(OVER)'} | composite ${p.composite} | status ${p.status}`);
  });
  if(DATA.comps && DATA.comps.submarket_cap_benchmarks){
    lines.push('\nSUBMARKET CAP BENCHMARKS:');
    Object.entries(DATA.comps.submarket_cap_benchmarks).forEach(([k,v])=>lines.push(`- ${k}: ${v}`));
  }
  const lc = Object.entries(window.liveComps||{}).filter(([,v])=>v&&v.length);
  if(lc.length){ lines.push('\nLIVE COMPS THIS SESSION:'); lc.forEach(([id,arr])=>{ const p=DATA.ranked.find(x=>x.id===id); lines.push(`for ${p?p.address:id}:`); arr.forEach(c=>lines.push(`  - ${c.title} | ${[c.price,c.cap,c.units,c.sf].filter(Boolean).join(' · ')}`)); }); }
  return lines.join('\n');
}

// ---- chat-issued actions (extended for the rich filters) ----
const TYPE_ALIASES={multifamily:'Multifamily',retail:'Retail',office:'Office',industrial:'Industrial','mixed':'Mixed-use','mixed-use':'Mixed-use',land:'Land',hospitality:'Hospitality'};
function applyAction(a){
  const done=[];
  if(a.scenario && SCEN[String(a.scenario)]){ setScenario(String(a.scenario)); done.push('scenario→'+a.scenario); }
  if(a.sort && SORTS[a.sort]){ $('#sort').value=a.sort; $('#sortTop').value=a.sort; done.push('sort→'+a.sort); }
  const setType=v=>{ const key=TYPE_ALIASES[String(v).toLowerCase()]; if(key){ F.types=new Set([key]); done.push('type→'+key); } };
  if(a.type) setType(a.type);
  if(a.filter && a.filter!=='all' && a.filter!=='active' && a.filter!=='affordable') setType(a.filter);
  if(a.filter==='affordable'){ F.afford=true; done.push('in-budget only'); }
  if(a.filter==='active'){ F.status='active'; done.push('active only'); }
  if(a.filter==='all'){ F.types.clear(); F.afford=false; F.status='all'; done.push('cleared'); }
  if(a.firm){ F.firms=new Set([a.firm]); done.push('firm→'+a.firm); }
  if(a.city){ F.cityQuery=String(a.city).toLowerCase(); $('#fCity').value=a.city; done.push('city→'+a.city); }
  if(a.maxPrice!=null && !isNaN(+a.maxPrice)){ F.priceMax=+a.maxPrice; $('#fPriceMax').value=+a.maxPrice; done.push('max $'+(+a.maxPrice).toLocaleString()); }
  if(a.minPrice!=null && !isNaN(+a.minPrice)){ F.priceMin=+a.minPrice; $('#fPriceMin').value=+a.minPrice; done.push('min $'+(+a.minPrice).toLocaleString()); }
  if(a.capMin!=null && !isNaN(+a.capMin)){ F.capMin=+a.capMin; $('#fCap').value=+a.capMin; $('#capVal').textContent=F.capMin?F.capMin+'%+':'any'; done.push('cap ≥'+a.capMin+'%'); }
  if(a.unitsMin!=null && !isNaN(+a.unitsMin)){ F.unitsMin=+a.unitsMin; $('#fUnits').value=+a.unitsMin; done.push('units ≥'+a.unitsMin); }
  syncChips(); render();
  if(a.highlightId){ const el=$('#card-'+a.highlightId); if(el){ el.scrollIntoView({behavior:'smooth',block:'center'}); el.classList.add('flash'); setTimeout(()=>el.classList.remove('flash'),2200); done.push('→'+a.highlightId); } }
  return done;
}

function setScenario(s){ scenario=s; localStorage.setItem('cre_scenario',s);
  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===s)); recompute(); }

function syncChips(){
  $$('#fType .chip').forEach(c=>{ if(c.dataset.warr==='unwarrantable') c.classList.toggle('active',F.unwarrantable); else if(c.dataset.warr==='nonqm') c.classList.toggle('active',F.nonqm); else c.classList.toggle('active',F.types.has(c.dataset.t)); });
  $$('#fFirm .chip').forEach(c=>c.classList.toggle('active',F.firms.has(c.dataset.fm)));
  $$('#fCityChips .chip').forEach(c=>c.classList.toggle('active',F.cityQuery===c.dataset.cy.toLowerCase()));
  $$('#fStatus .chip').forEach(c=>c.classList.toggle('active',F.status===c.dataset.st));
}

// ---- persistence ----
function saveState(){ try{ localStorage.setItem('cre_filters', JSON.stringify({
  types:[...F.types],firms:[...F.firms],cityQuery:F.cityQuery,priceMin:F.priceMin,priceMax:F.priceMax,
  capMin:F.capMin,unitsMin:F.unitsMin,yearMin:F.yearMin,status:F.status,afford:F.afford,sort:$('#sort').value })); }catch(e){} }
function loadState(){ try{ const s=JSON.parse(localStorage.getItem('cre_filters')||'{}');
  if(s.types) F.types=new Set(s.types); if(s.firms) F.firms=new Set(s.firms);
  F.cityQuery=s.cityQuery||''; F.priceMin=s.priceMin??null; F.priceMax=s.priceMax??null;
  F.capMin=s.capMin||0; F.unitsMin=s.unitsMin??null; F.yearMin=s.yearMin??null; F.status=s.status||'all'; F.afford=!!s.afford;
  if(s.cityQuery) $('#fCity').value=s.cityQuery; if(s.priceMin) $('#fPriceMin').value=s.priceMin; if(s.priceMax) $('#fPriceMax').value=s.priceMax;
  if(s.capMin){ $('#fCap').value=s.capMin; $('#capVal').textContent=s.capMin+'%+'; } if(s.unitsMin) $('#fUnits').value=s.unitsMin; if(s.yearMin) $('#fYear').value=s.yearMin;
  $('#fAfford').checked=F.afford; if(s.sort){ $('#sort').value=s.sort; }
}catch(e){} }
function resetAll(){ F.types.clear(); F.firms.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.yearMin=null; F.capMin=0; F.status='all'; F.afford=false; F.unwarrantable=false; F.nonqm=false;
  ['fCity','fPriceMin','fPriceMax','fUnits','fYear'].forEach(id=>$('#'+id).value=''); $('#fCap').value=0; $('#capVal').textContent='any'; $('#fAfford').checked=false;
  syncChips(); render(); }

// ---- chat ----
function addMsg(role, text, actions){
  const d=document.createElement('div'); d.className='msg '+role;
  d.innerHTML = (text||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/```action[\s\S]*?```/g,'').trim()
    .replace(/\*\*(.+?)\*\*/g,'<b>$1</b>').replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\n/g,'<br>');
  if(actions&&actions.length) d.innerHTML += `<div class="actiontag">⚡ view updated: ${actions.join(', ')}</div>`;
  $('#log').appendChild(d); $('#log').scrollTop=$('#log').scrollHeight; return d;
}
function parseAction(reply){ const m=reply.match(/```action\s*([\s\S]*?)```/); if(!m) return null; try{ return JSON.parse(m[1].trim()); }catch{ return null; } }
async function ask(text){
  addMsg('u', text); history.push({role:'user',content:text});
  $('#send').disabled=true; const thinking=addMsg('sys','…thinking');
  try{
    const r = await fetch('/api/claude-chat',{ method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ messages: history, context: getContext() }) });
    const j = await r.json(); thinking.remove();
    if(j.error){ addMsg('sys','⚠ '+j.error); }
    else { const act=parseAction(j.reply); const applied=act?applyAction(act):null; addMsg('a', j.reply, applied); history.push({role:'assistant',content:j.reply}); }
  }catch(e){ thinking.remove(); addMsg('sys','⚠ '+e.message); }
  $('#send').disabled=false; $('#q').focus();
}

// ---- property history + live comps ----
window.liveComps = {};
async function fetchHistory(btn){
  const id=btn.dataset.id, out=$('#hist-'+id); if(!out) return;
  if(out.dataset.loaded){ out.style.display = out.style.display==='none'?'block':'none'; return; }
  out.innerHTML='<span class="small">…checking WhoLivedThere archive (free)…</span>';
  try{
    const q=new URLSearchParams({address:btn.dataset.address||'',city:btn.dataset.city||'',zip:btn.dataset.zip||''});
    const j=await (await fetch('/api/history?'+q)).json();
    out.dataset.loaded='1';
    if(j.available===false){ out.innerHTML='<span class="b warn">⚠ archive offline</span>'; return; }
    if(!j.matched){ out.innerHTML='<span class="small">📜 No record in the WhoLivedThere archive for this address yet.</span>'; return; }
    const P=j.parcel, money=v=>v?'$'+Number(v).toLocaleString():'—';
    let h='<div class="small" style="margin-top:6px;border-top:1px solid #2a2a2a;padding-top:6px">📜 <b>WhoLivedThere archive</b>';
    if(P){ h+=`<div class="badges" style="margin:4px 0">${P.year_built?`<span class="b">built ${P.year_built}</span>`:''}${P.use_type?`<span class="b type">${P.use_type}</span>`:''}${P.units?`<span class="b">${P.units} unit${P.units>1?'s':''}</span>`:''}${P.total_value?`<span class="b ok" title="LA County assessor total assessed value">assessed ${money(P.total_value)}</span>`:''}${P.land_value?`<span class="b">land ${money(P.land_value)}</span>`:''}${P.improvement_value?`<span class="b">improv ${money(P.improvement_value)}</span>`:''}</div><div class="small">${P.use_description||''} · APN ${P.apn||'—'} · ${P.situs_address||''}</div>`; }
    if(j.permits&&j.permits.length){ h+=`<div class="small" style="margin-top:4px">🔨 Permits (${j.permits.length}): `+j.permits.slice(0,5).map(pm=>`${pm.permit_type||'permit'}${pm.valuation?' $'+Number(pm.valuation).toLocaleString():''}${pm.issue_date?' ('+String(pm.issue_date).slice(0,10)+')':''}`).join(' · ')+'</div>'; }
    if(j.filming&&j.filming.length){ h+=`<div class="small" style="margin-top:4px">🎬 Filmed here: `+j.filming.slice(0,5).map(f=>`${f.title||'production'}${f.year?' ('+f.year+')':''}`).join(' · ')+'</div>'; }
    if(j.events&&j.events.length){ h+=`<div class="small" style="margin-top:4px">📌 ${j.events.slice(0,4).map(ev=>ev.summary||ev.kind).join(' · ')}</div>`; }
    out.innerHTML=h+'</div>';
  }catch(e){ out.innerHTML='<span class="b warn">⚠ '+e.message+'</span>'; }
}
async function fetchLiveComps(id){
  const p = DATA.ranked.find(x=>x.id===id); if(!p) return;
  const out = $('#lc-'+id); if(!out) return;
  out.innerHTML = '<span class="small">…pulling fresh comps (~$0.03)…</span>';
  try{
    const r = await fetch('/api/comps',{method:'POST',headers:{'Content-Type':'application/json'},
      body:JSON.stringify({address:p.address,city:p.city,type:p.type})});
    const j = await r.json();
    if(j.error && !(j.comps&&j.comps.length)){ out.innerHTML = `<span class="b warn">⚠ ${j.error}</span>`; return; }
    window.liveComps[id] = j.comps||[];
    const none = (!j.comps||!j.comps.length)?' — none scraped (source may have blocked)':'';
    out.innerHTML = `<div class="small">Live comps via ${j.source||'web'} (cost ${j.cost})${none}:</div>` +
      (j.comps||[]).map(c=>`<div class="lc-item"><span>${esc(c.title||c.url||'comp')}</span>${c.url?` <button class="copyb" data-copyval="${esc(c.url)}" title="Copy comp URL">⧉</button>`:''} — ${esc([c.price,c.cap,c.units,c.sf].filter(Boolean).join(' · '))}</div>`).join('');
  }catch(e){ out.innerHTML = `<span class="b bad">⚠ ${e.message}</span>`; }
}

// ---- drawer open/close ----
function openDrawer(){ $('#drawer').classList.add('open'); $('#scrim').classList.add('on'); }
function closeDrawer(){ $('#drawer').classList.remove('open'); $('#scrim').classList.remove('on'); }

// ── Inline list-row expansion + Agent CRM actions ──────────────────────────────────
function postJSON(url,body){ return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body||{})}).then(r=>r.json()); }
function toast(msg){ let t=document.getElementById('crmToast'); if(!t){ t=document.createElement('div'); t.id='crmToast'; t.className='crmToast'; document.body.appendChild(t); } t.textContent=msg; t.classList.add('on'); clearTimeout(t._t); t._t=setTimeout(()=>t.classList.remove('on'),1900); }
// In-place refresh after any CRM save — repaints EVERY visible surface for this listing (list
// agent cell + open list-row panel + card block) WITHOUT a full render() (which would collapse
// expanded rows). Works for every asset type in either view.
function refreshContactUI(id){
  const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
  const row=[...document.querySelectorAll('.listtbl tr.lrow')].find(r=>r.dataset.id===id);
  if(row){ const cols=listCols(); const ai=cols.findIndex(c=>c.k==='agent');
    if(ai>=0 && row.children[ai+1]) row.children[ai+1].innerHTML=colCell('agent',p);
    const exp=row.nextElementSibling;
    if(exp&&exp.classList.contains('lexp')&&exp.classList.contains('open')){
      const body=exp.querySelector('.lexpbody'); if(body){ body.innerHTML=expandBody(p); body.dataset.loaded='1'; }
    }
  }
  const cb=document.getElementById('cardagent-'+id); if(cb) cb.outerHTML=cardAgentBlock(p);
}
async function markContacted(id){
  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/contacted',{}); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('✓ marked contacted '+fmtWhen(j.contact.contacted_at)); } else toast('⚠ '+(j.error||'failed')); }
  catch(e){ toast('⚠ '+e.message); }
}
async function saveAgentInfo(id){
  const scope=document.getElementById('composeM')&&document.getElementById('composeM').classList.contains('on')?document.getElementById('composeM'):document;
  const inputs=[...scope.querySelectorAll('.crmf')].filter(i=>i.dataset.id===id);
  const body={}; inputs.forEach(i=>body[i.dataset.info]=i.value.trim());
  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/info',body); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); toast('💾 contact info saved'); } else toast('⚠ '+(j.error||'failed')); }
  catch(e){ toast('⚠ '+e.message); }
}
async function deleteLetter(id,idx){
  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/letter/'+encodeURIComponent(idx)+'/delete',{}); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('letter deleted'); } }
  catch(e){ toast('⚠ '+e.message); }
}
function agentDraft(p,a){ return `Hi ${a.name||'there'},\n\nI'm interested in ${p.address||'your listing'}${p.city?' in '+p.city:''}${p.price?' (listed '+fmt(p.price)+')':''}. Could you share the setup — current rents / in-place NOI, any financing in place, and whether an offer near list would be entertained?\n\nThanks`; }
// The full per-listing contact modal body (editable info + mark-contacted + compose/save + letter log).
// The single per-listing "Agent window" — ONE modal with tabs: Agent · Letters · Find · Comps · History.
function contactModalBody(p){
  const a=agentInfo(p), id=p.id, firm=a.firm;
  const tel=a.phone?`<a class="ain" href="tel:${esc(a.phone).replace(/[^0-9+]/g,'')}">📞 ${esc(a.phone)}</a>`:'';
  const eml=a.email?`<a class="ain" href="mailto:${esc(a.email)}">✉ ${esc(a.email)}</a>`:'';
  const letters=a.letters.length?`<div class="crm-letters"><div class="deeplbl">Saved letters (${a.letters.length}) — saved, not sent</div>`
    +a.letters.map((l,i)=>`<div class="lett"><div class="letth"><b>${esc(l.subject||'(no subject)')}</b>${l.draft_id?`<span class="achip done" title="Gmail draft created${l.to?' to '+esc(l.to):''} — review & send in Gmail">📤 drafted</span>`:''}<span class="letts">${fmtWhen(l.ts)}</span><button class="letdel" data-letdel="${id}" data-idx="${l.id||i}" title="Delete this saved letter">✕</button></div><div class="lettb">${esc(l.body||'')}</div></div>`).join('')+`</div>`:'<div class="tabmut">No saved letters yet.</div>';
  const find=[];
  if(firm&&firm!=='Unknown'){
    find.push(`<a class="findlink" href="${gsearch('"'+firm+'" commercial real estate Los Angeles')}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm)} — Google</a>`);
    find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/companies/?keywords=${encodeURIComponent(firm)}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm)} — LinkedIn</a>`);
    find.push(`<button class="findlink" data-firmfilter="${esc(firm).replace(/"/g,'&quot;')}" title="Show only ${esc(firm)} listings in the grid">▦ More from ${esc(firm)} (in-app)</button>`);
  }
  if(a.name){
    find.push(`<a class="findlink" href="${gsearch('"'+a.name+'"'+(firm&&firm!=='Unknown'?' "'+firm+'"':'')+' real estate agent')}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — Google</a>`);
    find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/people/?keywords=${encodeURIComponent(a.name+(firm&&firm!=='Unknown'?' '+firm:''))}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — LinkedIn</a>`);
    find.push(`<a class="findlink" href="${gsearch('"'+a.name+'" DRE license California real estate')}" target="_blank" rel="noopener noreferrer">🪪 ${esc(a.name)} — DRE license</a>`);
  }
  if(p.source) find.push(`<a class="findlink" href="${safeUrl(p.source)}" target="_blank" rel="noopener noreferrer">↗ View source listing</a>`);
  find.push(`<span class="findlink findloop">${loopnetLink(p)}</span>`);
  const findHtml=find.length?`<div class="findgrid">${find.join('')}</div>`:'<div class="tabmut">Add an agent/firm in the Agent tab to enable lookups.</div>';
  return `<div class="composeCard tabwin">
    <div class="composeH">🧑‍💼 <b>${esc(p.address||'listing')}</b>${p.city?', '+esc(p.city):''}<span class="winmeta">${a.name?esc(a.name):'no agent yet'}${a.contacted_at?' · ✓ '+fmtWhen(a.contacted_at):''}</span><button class="composeX" title="Close">✕</button></div>
    <div class="tabbar">
      <button class="tabt active" data-tab="agent">Agent</button>
      <button class="tabt" data-tab="letters">Letters${a.letters.length?' ('+a.letters.length+')':''}</button>
      <button class="tabt" data-tab="find">Find</button>
      <button class="tabt" data-tab="comps">Comps</button>
      <button class="tabt" data-tab="history">History</button>
    </div>
    <div class="tabpane active" data-pane="agent">
      <div class="cm-info">
        <input class="crmf" data-info="name" data-id="${id}" placeholder="Agent name" value="${esc(a.name||'')}">
        <input class="crmf" data-info="phone" data-id="${id}" placeholder="Phone" value="${esc(a.phone||'')}">
        <input class="crmf" data-info="email" data-id="${id}" placeholder="Email" value="${esc(a.email||'')}">
      </div>
      ${(tel||eml)?`<div class="ainrow">${tel} ${eml}</div>`:''}
      <div class="cm-notes"><div class="deeplbl">Notes</div><textarea class="crmf" data-info="notes" data-id="${id}" rows="4" placeholder="Notes on this agent / listing">${esc(a.notes||'')}</textarea></div>
      <div class="cm-row">
        <button class="abtn saveinfobtn" data-saveinfo="${id}">💾 Save info + notes</button>
        <button class="abtn markbtn" data-mark="${id}">${a.contacted_at?'↻ Update contacted date':'✓ Mark contacted'}</button>
        ${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}
      </div>
    </div>
    <div class="tabpane" data-pane="letters">
      <div class="composeGate">⚑ <b>Save letter</b> logs it. <b>Send via George</b> creates a Gmail <b>DRAFT</b> — NOT sent; you review &amp; hit send in Gmail.</div>
      <input id="composeSubj" placeholder="Subject" value="${esc('Interest in '+(p.address||'your listing')+(p.city?', '+p.city:''))}">
      <textarea id="composeBody" rows="7">${esc(agentDraft(p,a))}</textarea>
      <div class="composeActs">
        <button id="composeSave" class="primary" data-id="${id}">💾 Save letter</button>
        <button id="composeSend" class="gated" data-id="${id}" title="Create a Gmail DRAFT to the agent (fill Email in the Agent tab) — not sent">📤 Send via George</button>
        <button id="composeCopy">📋 Copy</button>
      </div>
      ${letters}
    </div>
    <div class="tabpane" data-pane="find">${findHtml}</div>
    <div class="tabpane" data-pane="comps">
      <button class="lcbtn" data-id="${id}" title="Pull fresh comps via a cloud browser (~$0.03)">🔄 Pull live comps (~$0.03)</button>
      <div class="lcout" id="lc-${id}"></div>
    </div>
    <div class="tabpane" data-pane="history">
      <button class="histbtn" data-id="${id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (free)">📜 Load property history</button>
      <div class="histout" id="hist-${id}"></div>
    </div>
  </div>`;
}
// One modal, opens for EVERY listing from any surface. Its own click delegation (it lives on
// document.body, outside #grid) handles save-info / mark / save-letter / delete-letter.
function openContact(id){
  const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
  let m=document.getElementById('composeM');
  if(!m){ m=document.createElement('div'); m.id='composeM'; m.className='composeM'; document.body.appendChild(m); }
  m.dataset.id=id; m.innerHTML=contactModalBody(p); m.classList.add('on');
  m.onclick=async(e)=>{
    if(e.target===m || e.target.closest('.composeX,.composeX2')){ closeCompose(); return; }
    const tb=e.target.closest('.tabt'); if(tb){ activateTab(m,tb.dataset.tab); return; }
    const cv=e.target.closest('[data-copyval]'); if(cv){ navigator.clipboard.writeText(cv.dataset.copyval).then(()=>toast('📋 copied')).catch(()=>{}); return; }
    const ff=e.target.closest('[data-firmfilter]'); if(ff){ closeCompose(); F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
    const lc=e.target.closest('.lcbtn'); if(lc){ fetchLiveComps(lc.dataset.id); return; }
    const hi=e.target.closest('.histbtn'); if(hi){ fetchHistory(hi); return; }
    if(e.target.closest('#composeCopy')){ navigator.clipboard.writeText(m.querySelector('#composeBody').value); toast('📋 copied'); return; }
    if(e.target.closest('#composeSave')){
      const subject=m.querySelector('#composeSubj').value, body=m.querySelector('#composeBody').value;
      try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/letter',{subject,body,channel:'letter'});
        if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('💾 letter saved to contact log'); }
        else toast('⚠ '+(j.error||'save failed')); }catch(err){ toast('⚠ '+err.message); }
      return;
    }
    if(e.target.closest('#composeSend')){
      const to=((m.querySelector('.crmf[data-info=email]')||{}).value||'').trim();
      if(!to){ toast('add the agent email first'); return; }
      const subject=m.querySelector('#composeSubj').value, body=m.querySelector('#composeBody').value;
      if(!confirm('Create a Gmail DRAFT to '+to+' via George?\n\nIt will NOT be sent — it lands in your Gmail drafts (steve@designerwallcoverings.com) for you to review and send.')) return;
      try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/send-draft',{to,subject,body});
        if(j.ok){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('📤 Gmail draft created — review & send in Gmail'); }
        else if(j.error==='george_not_configured'){ toast('⚠ George drafting not enabled on this box'); alert('George drafting isn’t enabled on this server yet — the letter is not drafted.\n\nTo turn it on, GEORGE_AUTH must be set in the CRCP env (one-time). The letter is unaffected; use 💾 Save letter to keep it.'); }
        else toast('⚠ '+(j.detail||j.error||'draft failed')); }
      catch(err){ toast('⚠ '+err.message); }
      return;
    }
    if(e.target.closest('[data-saveinfo]')){ saveAgentInfo(id); return; }
    if(e.target.closest('[data-mark]')){ markContacted(id); return; }
    const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(id, ld.dataset.idx); return; }
  };
}
// Switch the active tab within the agent window.
function activateTab(m,tab){
  m.querySelectorAll('.tabt').forEach(t=>t.classList.toggle('active',t.dataset.tab===tab));
  m.querySelectorAll('.tabpane').forEach(pn=>pn.classList.toggle('active',pn.dataset.pane===tab));
}
// Repaint the open modal in place (after mark/letter changes) — preserves open state AND active tab.
function repaintModal(id){ const m=document.getElementById('composeM'); const p=DATA.ranked.find(x=>x.id===id);
  if(m && m.classList.contains('on') && m.dataset.id===id && p){
    const cur=(m.querySelector('.tabt.active')||{}).dataset; const tab=cur?cur.tab:'agent';
    m.innerHTML=contactModalBody(p); activateTab(m,tab||'agent');
  } }
function openCompose(id){ openContact(id); }   // back-compat alias
function closeCompose(){ const m=document.getElementById('composeM'); if(m) m.classList.remove('on'); }

// ---- Broker-history pilot index ----
// Keyed by both listing id (e.g. crx2477882) and lowercase address.
// Loaded once at boot; cards read from this map at render time.
window.BROKER_HIST = {};   // id → enrichment object
window.BROKER_HIST_ADDR = {};  // lc-address → enrichment object
fetch('/api/broker-history').then(r=>r.json()).then(j=>{
  (j.properties||[]).forEach(p=>{
    if(p.id) window.BROKER_HIST[p.id]=p;
    if(p.address) window.BROKER_HIST_ADDR[p.address.toLowerCase().trim()]=p;
  });
  // Re-render if DATA is already loaded (race: demographics loads fast too)
  if(window.DATA_READY) render();
}).catch(()=>{}); // graceful — broker pilot data is additive, not load-blocking

// ---- boot ----
fetch('data/comps.json').then(r=>r.json()).then(c=>{ if(DATA) DATA.comps=c; else window.__comps=c; }).catch(()=>{});
fetch('data/demographics.json').then(r=>r.json()).then(d=>{ window.DEMO=d.byCity||{}; if(DATA) render(); }).catch(()=>{});
// Map a condo (separate /api/condos schema) into the explorer's property shape so it flows through
// finance, scoring, filters, sort, and the card/list renderers as a first-class 'Condo' asset type.
function mapCondo(c){
  return { id:c.id, address:c.address, city:c.city, zip:c.zip, type:'Condo',
    price:+c.price||0, units:1, cap_rate:null, year_built:c.year_built||null,
    status:'Active', source:c.source, firm:c.firm_name||null, verified:false,
    isCondo:true, warrantable_status:c.warrantable_status,
    beds:c.beds, baths:c.baths, sqft:c.sqft, hoa:c.hoa, broker_name:c.broker_name,
    project_name:c.project_name, upside_note:null, qwen:null };
}
fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
  DATA=d; if(window.__comps) DATA.comps=window.__comps;
  // Merge condo inventory in-grid (best-effort; explorer still works if the API is down).
  let condoN=0; try{ const cd=await (await fetch('/api/condos')).json(); const cs=(cd.condos||[]).map(mapCondo); condoN=cs.length; DATA.ranked=DATA.ranked.concat(cs); }catch(e){}
  $('#sub').innerHTML = `${d.ranked.length} listings${condoN?` · ${condoN} condos`:''} · <b>${d.meta.market}</b>${d.generated?' · sourced '+d.generated:''}`;

  scenario = localStorage.getItem('cre_scenario')||'25';
  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===scenario));
  buildFacets();
  loadState();
  urlToState();  // a shared ?query overrides saved localStorage state and restores the exact view
  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===scenario));

  // drawer wiring
  $('#burger').onclick=openDrawer; $('#dclose').onclick=closeDrawer; $('#scrim').onclick=closeDrawer;
  $('#reset').onclick=resetAll;
  // collapsible filter tabs — click a section header to expand/collapse; all collapsed on load
  $('#drawer').addEventListener('click',e=>{ const h=e.target.closest('h4'); if(h && h.parentElement.classList.contains('fsec')) h.parentElement.classList.toggle('collapsed'); });
  $$('#drawer .fsec').forEach(s=>s.classList.add('collapsed'));
  // Fields section starts EXPANDED so the field-toggle cards are visible; the rest stay collapsed.
  const fieldsSec=$('#fFields')&&$('#fFields').closest('.fsec'); if(fieldsSec) fieldsSec.classList.remove('collapsed');
  // active-filter summary bar: remove one filter or reset all
  $('#activebar').addEventListener('click',e=>{ if(e.target.closest('#afreset')){ resetAll(); return; } const c=e.target.closest('.afchip'); if(c) clearFilter(c.dataset.clear); });
  // saved searches
  buildSavedList();
  $('#savedList').addEventListener('click',e=>{ const del=e.target.closest('[data-del]'); if(del){ SAVED.splice(+del.dataset.del,1); saveSaved(); buildSavedList(); return; } const n=e.target.closest('.savedname'); if(n){ location.href=location.pathname+(n.dataset.qs||''); } });
  $('#savebtn').onclick=()=>{ const name=(prompt('Name this saved search:')||'').trim(); if(!name) return; SAVED.push({name, qs:location.search||''}); saveSaved(); buildSavedList(); };
  $('#linkbtn').onclick=copyShareLink;
  $('#csvbtn').onclick=exportCSV;
  // facet chip handlers (multi-select toggles)
  $('#fType').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return;
    if(c.dataset.warr==='nonqm'){ F.nonqm=!F.nonqm; syncChips(); render(); return; }
    if(c.dataset.warr==='unwarrantable'){ F.unwarrantable=!F.unwarrantable; if(F.unwarrantable) F.types=new Set(['Condo']); syncChips(); render(); return; }
    F.types.has(c.dataset.t)?F.types.delete(c.dataset.t):F.types.add(c.dataset.t);
    if(!F.types.has('Condo')) F.unwarrantable=false;
    syncChips(); render(); };
  $('#fFirm').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; F.firms.has(c.dataset.fm)?F.firms.delete(c.dataset.fm):F.firms.add(c.dataset.fm); syncChips(); render(); };
  $('#fCityChips').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; const cy=c.dataset.cy.toLowerCase(); F.cityQuery=(F.cityQuery===cy)?'':cy; $('#fCity').value=(F.cityQuery?c.dataset.cy:''); syncChips(); render(); };
  $('#fStatus').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; F.status=c.dataset.st; syncChips(); render(); };
  $('#fCity').oninput=()=>{ F.cityQuery=$('#fCity').value.trim().toLowerCase(); syncChips(); render(); };
  $('#fPriceMin').oninput=()=>{ F.priceMin=$('#fPriceMin').value?+$('#fPriceMin').value:null; render(); };
  $('#fPriceMax').oninput=()=>{ F.priceMax=$('#fPriceMax').value?+$('#fPriceMax').value:null; render(); };
  $('#fCap').oninput=()=>{ F.capMin=+$('#fCap').value; $('#capVal').textContent=F.capMin?F.capMin+'%+':'any'; render(); };
  $('#fUnits').oninput=()=>{ F.unitsMin=$('#fUnits').value?+$('#fUnits').value:null; render(); };
  $('#fYear').oninput=()=>{ F.yearMin=$('#fYear').value?+$('#fYear').value:null; render(); };
  $('#fAfford').onchange=()=>{ F.afford=$('#fAfford').checked; render(); };
  $('#scenario').onclick=e=>{ if(!e.target.dataset.s) return; setScenario(e.target.dataset.s); render(); };

  // sort (drawer + top bar mirror each other)
  $('#sort').onchange=()=>{ colSort=null; $('#sortTop').value=$('#sort').value; render(); };
  $('#sortTop').onchange=()=>{ colSort=null; $('#sort').value=$('#sortTop').value; render(); };
  $('#sortTop').value=$('#sort').value;
  // density
  const dv=String(Math.min(10,Math.max(1,+(localStorage.getItem('cre_density')||'3')||3))); $('#density').value=dv; $('#densVal').textContent=dv; document.documentElement.style.setProperty('--cols',dv); localStorage.setItem('cre_density',dv);
  $('#density').oninput=()=>{ document.documentElement.style.setProperty('--cols',$('#density').value); $('#densVal').textContent=$('#density').value; localStorage.setItem('cre_density',$('#density').value); };

  // field show/hide toggles + list/grid view
  buildFieldToggles();
  $('#fFields').onchange=e=>{ const cb=e.target.closest('input[data-fk]'); if(!cb) return; VIS[cb.dataset.fk]=cb.checked; saveVis(); render(); };
  $('#fAll').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=true); saveVis(); buildFieldToggles(); render(); };
  $('#fNone').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=false); saveVis(); buildFieldToggles(); render(); };
  $('#fDef').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=!!f.def); saveVis(); buildFieldToggles(); render(); };
  $$('#viewseg button').forEach(b=>b.classList.toggle('active',b.dataset.view===view));
  $('#viewseg').onclick=e=>{ const b=e.target.closest('button[data-view]'); if(!b) return; view=b.dataset.view; localStorage.setItem('cre_view',view); $$('#viewseg button').forEach(x=>x.classList.toggle('active',x.dataset.view===view)); render(); };

  // chat
  const EX=['Show only multifamily under $1M, sorted by cash-on-cash','Which deals actually cash-flow at 30% down?','Office and industrial in Pasadena','Take me to the Reseda mixed-use','Best in-budget deals'];
  $('#examples').innerHTML = EX.map(x=>`<button class="ex">${x}</button>`).join('');
  $('#examples').onclick=e=>{ if(e.target.classList.contains('ex')){ $('#q').value=e.target.textContent; $('#q').focus(); } };
  $('#chatform').onsubmit=e=>{ e.preventDefault(); const t=$('#q').value.trim(); if(!t) return; $('#q').value=''; ask(t); };
  $('#grid').onclick=e=>{ const cp=e.target.closest('[data-copy]'); if(cp){ copySummary(cp.dataset.copy, cp); return; } const qk=e.target.closest('[data-quick]'); if(qk){ applyQuick(qk.dataset.quick); return; } const more=e.target.closest('#showmore'); if(more){ _shown=Math.min(_shown+PAGE(), _rows.length); paintPage(); return; } const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); const sb=$('#shortbtn'); if(sb) sb.textContent='⭐ Shortlist ('+SHORT.size+')'; if(F.shortlist) render(); return; } const th=e.target.closest('.listtbl th[data-sortk]'); if(th){ const k=th.dataset.sortk; if(colSort&&colSort.key===k) colSort.dir*=-1; else colSort={key:k,dir:['type','firm','warr','broker','status','zip','addr','rec'].includes(k)?1:-1}; render(); return; } const b=e.target.closest('.lcbtn'); if(b){ fetchLiveComps(b.dataset.id); return; } const h=e.target.closest('.histbtn'); if(h){ fetchHistory(h); return; }
    const cv=e.target.closest('[data-copyval]'); if(cv){ navigator.clipboard.writeText(cv.dataset.copyval).then(()=>toast('📋 copied')).catch(()=>toast('⚠ copy failed')); return; }
    const ff=e.target.closest('[data-firmfilter]'); if(ff){ F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
    const ct=e.target.closest('[data-contact]'); if(ct){ openCompose(ct.dataset.contact); return; }
    const mk=e.target.closest('[data-mark]'); if(mk){ markContacted(mk.dataset.mark); return; }
    const si=e.target.closest('[data-saveinfo]'); if(si){ saveAgentInfo(si.dataset.saveinfo); return; }
    const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(ld.dataset.letdel, ld.dataset.idx); return; }
    const row=e.target.closest('.listtbl tr.lrow'); if(row && !e.target.closest('a,button,input,textarea,select,[data-firmfilter],[data-copyval]')){ openContact(row.dataset.id); return; } };
  $('#shortbtn').onclick=()=>{ F.shortlist=!F.shortlist; render(); };
  $('#cmpbtn').onclick=openCompare;
  $('#heat').value=heat; $('#heat').onchange=()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat',heat); render(); };
  $('#cmpov').addEventListener('click',e=>{ if(e.target.id==='cmpov') e.currentTarget.classList.remove('on'); });
  $('#cmpmodal').addEventListener('click',e=>{ if(e.target.closest('#cmpcsv')) exportCompareCSV(); else if(e.target.closest('#cmpprint')) printCompare(); });
  window.addEventListener('afterprint',()=>document.body.classList.remove('printing-compare'));
  // a11y: Escape closes any open overlay (compare / broker / property-history modals)
  document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ ['cmpov','bkov'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('on'); }); } });

  window.DATA_READY = true;
  recompute(); syncChips(); render();
}).catch(e=>{ $('#sub').textContent='Failed to load data/ranked.json — run: npm run analyze'; });
</script>
  <script src="/col-resize.js" defer></script>
</body>
</html>