← back to Commercialrealestate

public/fha-loans.html

585 lines

<!doctype html>
<html lang="en">
<head>
<script>/* credential-safe fetch (2026-08-18): resolve EVERY relative fetch (with or without a leading slash) against a credential-free base so creds embedded in the page URL (http://user:pass@host/...) don't poison document.baseURI and throw "Request cannot be constructed from a URL that includes credentials". */(function(){var of=window.fetch,O=location.origin;window.fetch=function(u,o){try{if(typeof u==='string'){if(/^[a-z][a-z0-9+.-]*:/i.test(u)){var x=new URL(u);if(x.username||x.password){x.username='';x.password='';u=x.href;}}else{u=new URL(u,O+location.pathname+location.search).href;}}else if(u&&typeof u.url==='string'){var y=new URL(u.url,O+location.pathname+location.search);if(y.username||y.password){y.username='';y.password='';u=new Request(y.href,u);}}}catch(e){}return of.call(this,u,o);};try{if(location.href.indexOf('@')>=0){history.replaceState(null,'',location.pathname+location.search+location.hash);}}catch(e){}})();</script>
<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='dark'?'dark':'light');}catch(e){document.documentElement.setAttribute('data-theme','light');}})();</script>
<!-- Self-heal: nav-agent grid controls can persist a "removed"/"collapsed" state that hides the
     whole grid with no obvious way back. Clear it before nav-agent runs so a grid can never stay hidden. -->
<script>(function(){try{for(var i=localStorage.length-1;i>=0;i--){var k=localStorage.key(i);if(k&&k.indexOf('navagent:')===0){var v=localStorage.getItem(k);if(v&&v.indexOf('"removed":true')>=0){localStorage.removeItem(k);}}}}catch(e){}})();</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230a0d13'/%3E%3Ctext x='8' y='12' font-size='11' text-anchor='middle' fill='%23c8a24b'%3E%24%3C/text%3E%3C/svg%3E">
<title>FHA / Low-Rate Loans — LA County CRE</title>
<link rel="stylesheet" href="/crcp-grid.css">
<link rel="stylesheet" href="/three-view.css">
<script src="/three-view.js"></script>
<script src="/infinite-scroll.js"></script><!-- windowed infinite scroll -->
<script src="/crcp-grid.js"></script>
<style>
  :root{
    --gold:#e9c46a; --g2:#c8a24b; --violet:#8b7cf6; --teal:#2dd4bf; --blue:#60a5fa; --pink:#f472b6; --orange:#fb923c;
    --red:#f85149;
    --ink:#eef1f7; --muted:#9aa3b4; --glass:rgba(255,255,255,.055); --glassb:rgba(255,255,255,.12);
    /* crcp-grid engine theme mapping (liquid-glass) */
    --cg-line:rgba(255,255,255,.12); --cg-mut:#9aa3b4; --cg-ink:#eef1f7; --cg-card:rgba(255,255,255,.06);
    --cg-acc:#e9c46a; --cg-accbg:rgba(233,196,106,.14); --cg-railbg:rgba(255,255,255,.035);
    --cg-topbg:rgba(7,10,16,.72); --cg-red:#f85149;
  }
  *{box-sizing:border-box;margin:0;padding:0;}
  html,body{min-height:100%;background:var(--bg);color:var(--ink);font:15px/1.5 -apple-system,'SF Pro Display',Segoe UI,Roboto,sans-serif;-webkit-font-smoothing:antialiased;}
  body::before{content:'';position:fixed;inset:-25%;z-index:-2;
    background:
      radial-gradient(38vw 38vw at 18% 22%, rgba(139,124,246,.40), transparent 60%),
      radial-gradient(34vw 34vw at 82% 18%, rgba(45,212,191,.34), transparent 60%),
      radial-gradient(40vw 40vw at 72% 86%, rgba(233,196,106,.30), transparent 62%),
      radial-gradient(36vw 36vw at 24% 88%, rgba(96,165,250,.30), transparent 60%);
    filter:saturate(1.25);animation:drift 26s ease-in-out infinite alternate;}
  body::after{content:'';position:fixed;inset:0;z-index:-1;backdrop-filter:blur(70px);background:color-mix(in srgb,var(--bg) 34%,transparent);}
  @keyframes drift{0%{transform:translate3d(0,0,0) scale(1) rotate(0deg);}
    50%{transform:translate3d(2%,-2%,0) scale(1.08) rotate(4deg);}100%{transform:translate3d(-2%,2%,0) scale(1.04) rotate(-3deg);}}
  .wrap{max-width:1320px;margin:0 auto;padding:32px 32px 60px;}
  /* header */
  header{display:flex;align-items:center;gap:16px;margin-bottom:18px;flex-wrap:wrap;}
  .title h1{font-size:26px;font-weight:800;letter-spacing:-.5px;background:linear-gradient(92deg,#fff,var(--gold) 60%,var(--teal));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
  .title p{font-size:12.5px;color:var(--muted);margin-top:2px;}
  nav{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap;}
  nav a{font-size:12.5px;color:var(--muted);text-decoration:none;padding:7px 13px;border-radius:13px;
    background:var(--glass);border:1px solid var(--glassb);backdrop-filter:blur(14px);transition:.2s;}
  nav a:hover{color:#fff;border-color:rgba(233,196,106,.5);transform:translateY(-1px);}
  nav a.on{color:#fff;border-color:rgba(233,196,106,.6);background:linear-gradient(90deg,rgba(233,196,106,.22),rgba(45,212,191,.16));}
  /* glass */
  .glass{position:relative;background:var(--glass);border:1px solid var(--glassb);border-radius:22px;
    backdrop-filter:blur(22px) saturate(1.5);-webkit-backdrop-filter:blur(22px) saturate(1.5);
    box-shadow:0 12px 40px rgba(0,0,0,.40), inset 0 1px 0 rgba(255,255,255,.18), inset 0 -1px 0 rgba(0,0,0,.25);
    overflow:hidden;opacity:0;transform:translateY(14px);animation:rise .7s cubic-bezier(.2,.7,.2,1) forwards;}
  @keyframes rise{to{opacity:1;transform:none;}}
  /* explainer */
  .explain{padding:16px 19px;margin-bottom:16px;font-size:12.8px;color:#cfd6e4;line-height:1.65;}
  .explain h3{font-size:13px;font-weight:700;margin-bottom:7px;display:flex;align-items:center;gap:8px;color:#fff;}
  .explain .dot{width:9px;height:9px;border-radius:50%;display:inline-block;box-shadow:0 0 10px currentColor;}
  .explain b{color:#fff;} .explain code{background:rgba(255,255,255,.07);padding:1px 6px;border-radius:6px;font-size:11.5px;}
  .explain .caveat{margin-top:10px;padding:10px 12px;border-radius:11px;background:rgba(248,81,73,.08);border:1px solid rgba(248,81,73,.28);color:#ffd5d2;font-size:12px;}
  .explain a{color:var(--gold);text-decoration:none;border-bottom:1px dotted rgba(233,196,106,.5);}
  .explain a:hover{color:#fff;}
  .redkey{display:inline-block;width:11px;height:11px;border-radius:3px;background:var(--red);vertical-align:-1px;margin-right:4px;}
  /* hero tiles */
  .tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px;margin-bottom:22px;}
  .tile{padding:22px 24px;border:2px solid rgba(255,255,255,.4);}
  .tile .k{font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);}
  .tile .v{font-size:30px;font-weight:800;letter-spacing:-.5px;margin-top:6px;font-variant-numeric:tabular-nums;line-height:1;}
  .tile .s{font-size:11.5px;color:var(--muted);margin-top:6px;}
  .v.gold{background:linear-gradient(90deg,#fff,var(--gold));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
  .v.teal{background:linear-gradient(90deg,#fff,var(--teal));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
  .v.red{background:linear-gradient(90deg,#fff,var(--red));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
  .v.violet{background:linear-gradient(90deg,#fff,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;}
  /* controls */
  .controls{display:flex;gap:12px;align-items:center;padding:13px 19px;margin-bottom:14px;flex-wrap:wrap;}
  .controls input[type=text],.controls select{background:rgba(255,255,255,.05);color:var(--ink);border:1px solid var(--glassb);border-radius:11px;padding:8px 11px;font-size:13px;}
  .controls input[type=text]{min-width:240px;}
  .controls input[type=range]{accent-color:var(--gold);}
  .controls label{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:7px;}
  .controls .count{margin-left:auto;font-size:12px;color:var(--muted);} .controls .count b{color:var(--ink);}
  /* table */
  .tablewrap{padding:6px;overflow-x:auto;}
  table{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--tsz,13px);font-variant-numeric:tabular-nums;}
  thead th{position:sticky;top:0;background:var(--headbg);backdrop-filter:blur(8px);text-align:left;
    font-size:11px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;color:var(--muted);
    padding:11px 12px;border-bottom:1px solid var(--glassb);cursor:pointer;user-select:none;white-space:nowrap;}
  thead th:hover{color:#fff;}
  thead th .ar{opacity:.5;font-size:10px;margin-left:3px;}
  thead th.sorted{color:var(--gold);} thead th.sorted .ar{opacity:1;}
  tbody td{padding:9px 12px;border-bottom:1px solid rgba(255,255,255,.05);vertical-align:top;}
  tbody tr:hover td{background:rgba(255,255,255,.03);}
  tbody tr.red td{background:rgba(248,81,73,.07);}
  tbody tr.red:hover td{background:rgba(248,81,73,.12);}
  tbody tr.red td:first-child{border-left:3px solid var(--red);}
  td.prop{font-weight:600;color:#fff;}
  td.prop .loc{display:block;font-weight:400;color:var(--muted);font-size:.86em;margin-top:1px;}
  td.red-txt{color:var(--red);font-weight:700;}
  td.num{text-align:right;}
  .rate{font-weight:700;}
  .whybadge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10.5px;font-weight:700;letter-spacing:.2px;cursor:help;margin:1px 2px 1px 0;}
  .whybadge.below{background:rgba(96,165,250,.18);color:#9cc4ff;border:1px solid rgba(96,165,250,.35);}
  .whybadge.era{background:rgba(248,81,73,.18);color:#ff9b94;border:1px solid rgba(248,81,73,.40);}
  .empty{padding:50px;text-align:center;color:var(--muted);}
  /* sortable + draggable + resizable headers (Steve list-build rule 2026-07-31) */
  thead th.dragcol{cursor:grab;}
  thead th.dragging{opacity:.45;}
  thead th.dropL{box-shadow:inset 3px 0 0 var(--gold);}
  thead th.dropR{box-shadow:inset -3px 0 0 var(--gold);}
  thead th.asc .ar::after{content:'▲';} thead th.desc .ar::after{content:'▼';}
  tbody tr.frow{cursor:pointer;}
  /* left rail — facets + column toggles */
  .fha-shell{display:flex;align-items:flex-start;gap:0;}
  .fha-rail{flex:0 0 250px;align-self:stretch;border-right:1px solid var(--glassb);background:rgba(255,255,255,.035);
    position:sticky;top:0;max-height:100vh;overflow-y:auto;padding-bottom:30px;border-radius:0;}
  .fha-main{flex:1 1 auto;min-width:0;}
  .fha-sec{padding:12px 15px;border-bottom:1px solid var(--glassb);}
  .fha-sec h4{margin:0 0 9px;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:space-between;user-select:none;}
  .fha-sec h4::after{content:'▾';font-size:11px;margin-left:8px;transition:transform .15s;}
  .fha-sec.collapsed h4::after{transform:rotate(-90deg);}
  .fha-sec.collapsed > *:not(h4){display:none !important;}
  .fchips{display:flex;flex-wrap:wrap;gap:6px;}
  .fchip{background:var(--glass);color:var(--muted);border:1px solid var(--glassb);border-radius:16px;padding:4px 10px;font-size:12px;cursor:pointer;user-select:none;}
  .fchip:hover{color:#fff;} .fchip.active{border-color:var(--gold);color:var(--gold);background:rgba(233,196,106,.12);}
  .fct{color:var(--muted);font-size:10px;margin-left:4px;}
  .frow2{display:flex;gap:8px;} .ffld{flex:1;} .ffld label{display:block;font-size:10px;color:var(--muted);margin-bottom:3px;}
  .ffld input{width:100%;background:var(--glass);color:var(--ink);border:1px solid var(--glassb);border-radius:8px;padding:7px 9px;font-size:13px;}
  .ftogs{display:flex;flex-direction:column;gap:1px;max-height:300px;overflow-y:auto;}
  .ftog{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink);cursor:pointer;padding:3px 4px;border-radius:6px;} .ftog:hover{background:rgba(233,196,106,.08);} .ftog input{accent-color:var(--gold);}
  .fgrp{font-size:9.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);margin:9px 0 3px;font-weight:700;opacity:.85;} .fgrp:first-child{margin-top:0;}
  .fmini{margin-top:8px;display:flex;gap:5px;flex-wrap:wrap;}
  .fmini button{background:var(--glass);color:var(--muted);border:1px solid var(--glassb);border-radius:7px;padding:4px 9px;font-size:11px;cursor:pointer;} .fmini button:hover{border-color:var(--gold);color:var(--ink);}
  /* expandable-row detail modal */
  #fov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.62);z-index:80;align-items:flex-start;justify-content:center;padding:40px 14px;overflow-y:auto;}
  #fov.on{display:flex;}
  #fmodal{width:min(640px,96vw);background:var(--card);border:1px solid var(--line);border-radius:16px;padding:20px 22px;position:relative;backdrop-filter:blur(22px);}
  #fmodal h3{margin:0 0 3px;font-size:19px;color:#fff;} #fmodal .msub{color:var(--gold);font-size:12.5px;margin-bottom:6px;}
  #fmodal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--muted);font-size:22px;cursor:pointer;}
  #fmodal .litem{display:flex;justify-content:space-between;gap:12px;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.06);font-size:13px;}
  #fmodal .litem .m{color:var(--muted);}
  @media(max-width:820px){.fha-rail{display:none;}}
  .foot{margin-top:20px;font-size:11px;color:var(--muted);text-align:center;line-height:1.7;}
  .pill{display:inline-block;padding:2px 9px;border-radius:11px;background:var(--glass);border:1px solid var(--glassb);margin:0 3px;}
  @media(prefers-reduced-motion:reduce){body::before{animation:none;}.glass{animation:none;opacity:1;transform:none;}}
</style>
<link rel="stylesheet" href="/crcp-theme.css">
  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<div class="wrap">
  <header>
    <div class="title">
      <h1>◆ FHA / Low-Rate Loans</h1>
      <p id="asof">HUD/FHA multifamily insured mortgages originally at ≤3.5% · loading…</p>
    </div>
    <nav>
      <a href="/">Properties</a><a href="/map.html">Map</a><a href="/deals.html">Deals</a>
      <a href="/crcp.html">CRCP</a><a href="/condos.html">Condos</a>
      <a href="/fha-loans.html" class="on">FHA Loans</a>
    </nav>
  </header>

  <div class="glass explain" style="animation-delay:.02s">
    <h3><span class="dot" style="color:var(--gold)"></span>What this shows</h3>
    <div>
      Every <b>HUD/FHA-insured multifamily (commercial) mortgage</b> that was originated at an
      <b>original interest rate of ≤ 3.5%</b>, pulled live from HUD's public Active Insured Mortgages
      file. Each loan is benchmarked against the <b>Freddie Mac PMMS</b> 30-year fixed market average
      for the month it closed.
      <span class="redkey"></span><b>Rows highlighted in red</b> fired at least one of two signals:
      <span class="whybadge below" style="cursor:default">BELOW MARKET</span> the rate is ≥ 0.75 pts under
      the month's market average · <span class="whybadge era" style="cursor:default">HISTORIC LOW ERA</span>
      it closed inside a derived rock-bottom-rate window. <b>RED = either signal</b>; hover a badge to see why.
      <div class="caveat">
        ⚠️ <b>Single-family caveat:</b> FHA <i>single-family</i> per-loan rate + address data is <b>not public</b>
        (borrower-privacy law), so individual home FHA loans cannot be listed here. The realistic public dataset
        carrying a property + an original interest rate is HUD/FHA <b>multifamily</b> (apartments, senior/healthcare
        housing, etc.) — that is exactly what this page reports. HUD's multifamily mortgage file lists property name +
        city/state/zip, not a street address line.
      </div>
      <div style="margin-top:9px;font-size:11.5px;">
        Sources:
        <a id="srcHud" href="#" target="_blank" rel="noopener noreferrer">HUD Active Insured Multifamily Mortgages</a> ·
        <a id="srcPmms" href="#" target="_blank" rel="noopener noreferrer">Freddie Mac PMMS 30-yr history</a> ·
        <a id="srcFred" href="#" target="_blank" rel="noopener noreferrer">FRED MORTGAGE30US</a>
        <span id="windows" style="color:var(--muted)"></span>
        · <span style="color:var(--teal)">$0 — all public data</span>
      </div>
    </div>
  </div>

  <div class="tiles" id="tiles"></div>

  <div class="glass controls" style="animation-delay:.08s">
    <input type="text" id="q" placeholder="search all fields — property, city, lender, rate, ZIP… (space = AND)">
    <span class="tv-seg" id="fviewseg"></span>
    <select id="fsort" title="Sort loans"></select>
    <label>density <input type="range" id="density" min="4" max="24" step="1"></label>
    <span class="count" id="count">…</span>
  </div>

  <div class="glass" style="animation-delay:.1s;padding:0;overflow:hidden">
    <div class="fha-shell">
      <aside class="fha-rail" id="fRail"></aside>
      <div class="fha-main">
        <div class="tablewrap" id="ftableView">
          <table id="ftable" data-no-sort><thead id="fthead"></thead><tbody id="ftbody"></tbody></table>
        </div>
        <div id="fgridView" class="hide" style="padding:16px;display:grid;gap:16px;grid-template-columns:repeat(var(--fcols,3),minmax(0,1fr))"></div>
        <div id="flistView" class="hide"></div>
        <div class="empty" id="femptymsg" style="display:none">loading real HUD data…</div>
      </div>
    </div>
  </div>

  <div id="fov"><div id="fmodal"><button class="x" aria-label="Close" onclick="document.getElementById('fov').classList.remove('on')">✕</button><div id="fmbody"></div></div></div>

  <div class="foot">
    <span class="pill">Liquid-glass UI · 2026</span><span class="pill">HUD Open Data</span>
    <span class="pill">Freddie Mac PMMS</span><span class="pill">real data · $0</span><br>
    Scope: HUD/FHA multifamily insured mortgages (commercial). Single-family FHA per-loan data is not public. Not financial advice.
  </div>
</div>

<script>
const $=s=>document.querySelector(s);
const F={
  money:n=>(n==null||!isFinite(n))?'—':'$'+(n>=1e9?(n/1e9).toFixed(2)+'B':n>=1e6?(n/1e6).toFixed(2)+'M':n>=1e3?Math.round(n/1e3)+'k':n),
  num:n=>{n=Number(n);return isFinite(n)?n.toLocaleString():'—';},
  pct:n=>(n==null||!isFinite(n))?'—':(+n).toFixed(2)+'%',
};
let DATA=null, ROWS=[];

// ── esc + formatters ──────────────────────────────────────────────────────────
const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
const flagOf=r=> r.red ? (r.belowMarket&&r.historicLowEra?'Below + Era':r.belowMarket?'Below market':'Historic-low era') : '—';
// Every data point carries a group `g` for organized toggles + sort menu.
// COLS order below IS the tight default column order; def:1 shows on first load.
const COLS=[
  {k:'property',       l:'Property',    g:'Identity', t:'s',     def:1},
  {k:'rate',           l:'Orig. Rate',  g:'Loan',     t:'pct',   def:1},
  {k:'pmmsBenchmark',  l:'Market avg',  g:'Loan',     t:'pct',   def:1},
  {k:'originalAmount', l:'Loan amount', g:'Loan',     t:'money', def:1},
  {k:'originationDate',l:'Originated',  g:'Loan',     t:'s',     def:1},
  {k:'units',          l:'Units',       g:'Loan',     t:'num',   def:1},
  {k:'flag',           l:'Flag',        g:'Flag',     t:'flag',  def:0, calc:flagOf},
  {k:'city',           l:'City',        g:'Location', t:'s',     def:0},
  {k:'state',          l:'State',       g:'Location', t:'s',     def:0},
  {k:'zip',            l:'ZIP',         g:'Location', t:'s',     def:0},
  {k:'holder',         l:'Lender',      g:'Details',  t:'s',     def:0},
];
const FACETS=[
  {k:'flag',  l:'Flag',          type:'chip', calc:r=> r.red ? (r.belowMarket&&r.historicLowEra?'Below + Era':r.belowMarket?'Below market':'Historic-low era') : 'Not flagged'},
  {k:'state', l:'State',         type:'chip'},
  {k:'city',  l:'City',          type:'chip', max:20},
  {k:'year',  l:'Year closed',   type:'chip', calc:r=>String(r.originationDate||'').slice(0,4), max:24},
  {k:'rate',  l:'Orig. rate %',  type:'range'},
  {k:'originalAmount', l:'Loan amount $', type:'range', money:1},
  {k:'units', l:'Units',         type:'range'},
];

function badge(r){
  if(!r.red) return '<span style="color:var(--muted)">—</span>';
  const reasons=r.redReasons||[];
  let html='';
  if(r.belowMarket){
    const t=reasons.find(x=>/below the/.test(x))||'';
    html+=`<span class="whybadge below" title="${t.replace(/"/g,'&quot;')}">BELOW MARKET</span>`;
  }
  if(r.historicLowEra){
    const t=reasons.find(x=>/historic/.test(x))||'';
    html+=`<span class="whybadge era" title="${t.replace(/"/g,'&quot;')}">HISTORIC LOW ERA</span>`;
  }
  return html;
}

// ── cell formatting ───────────────────────────────────────────────────────────
const fmtDate=v=>{ if(!v) return '—'; try{ return new Date(v).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }catch(e){ return '—'; } };
function fval(r,c){ return c.calc?c.calc(r):r[c.k]; }
function cell(r,c){
  const v=fval(r,c);
  if(c.k==='flag') return r.red?badge(r):'<span style="color:var(--muted)">—</span>';
  if(v==null||v==='') return '<span style="color:var(--muted)">—</span>';
  if(c.t==='pct')   return '<span class="rate">'+(+v).toFixed(2)+'%</span>';
  if(c.t==='money') return F.money(+v);
  if(c.t==='num')   return (+v).toLocaleString();
  if(c.k==='originationDate') return fmtDate(v);
  return esc(v);
}

// ── column visibility (START TIGHT — only def:1 on first load; saved pref wins) ─
let VISCOL=(function(){let s={};try{s=JSON.parse(localStorage.getItem('fhaCols')||'{}');}catch(e){}return s;})();
function colVis(k){ const c=COLS.find(x=>x.k===k); if(k in VISCOL) return !!VISCOL[k]; return c?!!c.def:false; }
function saveCols(){ try{localStorage.setItem('fhaCols',JSON.stringify(VISCOL));}catch(e){} }
// ── draggable column ORDER (persisted per page) ───────────────────────────────
let COLORDER=(function(){let a=[];try{a=JSON.parse(localStorage.getItem('fhaColOrder')||'[]');}catch(e){}return Array.isArray(a)?a:[];})();
function syncColOrder(){
  const keys=COLS.map(c=>c.k);
  COLORDER=COLORDER.filter(k=>keys.includes(k));
  keys.forEach(k=>{ if(!COLORDER.includes(k)) COLORDER.push(k); });
}
function orderedCols(){ syncColOrder(); return COLORDER.map(k=>COLS.find(c=>c.k===k)).filter(Boolean); }
function visCols(){ return orderedCols().filter(c=>colVis(c.k)); }
function saveColOrder(){ try{localStorage.setItem('fhaColOrder',JSON.stringify(COLORDER));}catch(e){} }
// Hand the table back to col-resize.js when the visible column set/order changes.
let lastColSig='';
function reinitResize(){
  const tbl=document.querySelector('#ftable'); if(!tbl) return;
  const cg=tbl.querySelector('colgroup[data-cr]'); if(cg) cg.remove();
  delete tbl.dataset.crDone; tbl.style.tableLayout=''; tbl.style.width='';
  if(window.ColResize && window.ColResize.refresh) window.ColResize.refresh();
}

// ── search + sort state ───────────────────────────────────────────────────────
let q='', sortKey='rate', sortDir=1;
// 3-view state (grid cards / compact list / dense table). Persisted. TK-10526.
let VIEW=(function(){ let v=null; try{v=localStorage.getItem('fhaView');}catch(e){} return ['grid','list','table'].includes(v)?v:'table'; })();
const FF={ chips:{}, ranges:{} };
const gv=(r,fc)=>fc.calc?fc.calc(r):r[fc.k];
FACETS.forEach(fc=>{ if(fc.type==='chip') FF.chips[fc.k]=new Set(); else FF.ranges[fc.k]={min:null,max:null}; });
function passFacets(r){
  for(const fc of FACETS){
    if(fc.type==='chip'){ const set=FF.chips[fc.k]; if(set.size && !set.has(String(gv(r,fc)))) return false; }
    else { const rg=FF.ranges[fc.k], v=gv(r,fc);
      if(rg.min!=null && !(+v>=rg.min)) return false;
      if(rg.max!=null && !(+v<=rg.max)) return false; }
  }
  return true;
}
function filtered(){
  let rows=ROWS.filter(passFacets);
  if(q){
    // Search ALL data — every field on the row, incl. nested (JSON.stringify). Space = AND.
    const terms=q.toLowerCase().split(/\s+/).filter(Boolean);
    rows=rows.filter(r=>{
      const hay=Object.keys(r).map(k=>{const v=r[k];return v==null?'':(typeof v==='object'?JSON.stringify(v):String(v));}).join('  ').toLowerCase();
      return terms.every(t=>hay.includes(t));
    });
  }
  const c=COLS.find(x=>x.k===sortKey)||COLS[0];
  rows=rows.slice().sort((a,b)=>{
    let x=fval(a,c), y=fval(b,c);
    const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
    if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
    if(x<y) return -sortDir; if(x>y) return sortDir; return 0;
  });
  return rows;
}

// ── render: grid (cards) / list (compact) / table (dense) ─────────────────────
let _flInf=null,_flRows=[],_flCols=[];
// Render one window (slice) of loans into the active view (infinite scroll windows the ~7,400 rows).
function _flPaint(win){ const cols=_flCols;
  const propCell=r=>esc(r.property||'(name n/a)')+(r.city||r.state?`<span class="loc">${esc([r.city,r.state].filter(Boolean).join(', ')+' '+(r.zip||''))}${r.holder?' · '+esc(r.holder):''}</span>`:'');
  if(VIEW==='table'){
    $('#fthead').innerHTML='<tr>'+cols.map(c=>`<th data-k="${c.k}" draggable="true" title="Drag to reorder · click to sort" class="dragcol${sortKey===c.k?(sortDir>0?' asc sorted':' desc sorted'):''}">${esc(c.l)}<span class="ar"></span></th>`).join('')+'</tr>';
    $('#ftbody').innerHTML=win.map(r=>`<tr class="frow${r.red?' red':''}" data-id="${esc(r.__i)}">`
      +cols.map(c=>{const numT=(c.t==='pct'||c.t==='money'||c.t==='num');return `<td class="${c.k==='property'?'prop':''}${numT?' num':''}">`
        +(c.k==='property'?propCell(r):cell(r,c))
        +'</td>';}).join('')+'</tr>').join('');
    const csig=cols.map(c=>c.k).join(',');
    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
    else if(window.ColResize && window.ColResize.reattach){ window.ColResize.reattach(document.querySelector('#ftable')); } // resize handles survive sort/search re-render (TK-10089)
  } else if(VIEW==='list'){
    const metaCols=cols.filter(c=>c.k!=='property');
    $('#flistView').innerHTML='<div class="tv-rows">'+win.map(r=>`<div class="tv-lrow frow${r.red?' red':''}" data-id="${esc(r.__i)}">`
      +`<div class="tv-lmain"><span class="tv-la">${esc(r.property||'(name n/a)')}</span><span class="tv-lsub">${esc([r.city,r.state].filter(Boolean).join(', ')+' '+(r.zip||''))}${r.holder?' · '+esc(r.holder):''}</span>${r.red?badge(r):''}</div>`
      +`<div class="tv-lmeta">`+metaCols.map(c=>`<span class="tv-lm"><span class="tv-lmk">${esc(c.l)}</span>${cell(r,c)}</span>`).join('')+`</div></div>`).join('')+'</div>';
  } else {
    const bodyCols=cols.filter(c=>c.k!=='property');
    $('#fgridView').innerHTML=win.map(r=>`<div class="glass tile frow${r.red?' red':''}" data-id="${esc(r.__i)}" style="padding:16px 18px;cursor:pointer;border:1px solid var(--glassb)">`
      +`<div style="font-weight:700;color:#fff;margin-bottom:2px">${esc(r.property||'(name n/a)')}</div>`
      +`<div style="font-size:11.5px;color:var(--muted);margin-bottom:8px">${esc([r.city,r.state].filter(Boolean).join(', ')+' '+(r.zip||''))}${r.holder?' · '+esc(r.holder):''}</div>`
      +(r.red?`<div style="margin-bottom:8px">${badge(r)}</div>`:'')
      +bodyCols.map(c=>`<div style="display:flex;justify-content:space-between;gap:10px;font-size:12.5px;padding:2px 0"><span style="color:var(--muted)">${esc(c.l)}</span><span>${cell(r,c)}</span></div>`).join('')
      +`</div>`).join('');
  }
}
function render(){
  const rows=filtered(), cols=visCols();
  $('#count').innerHTML=`<b>${rows.length.toLocaleString()}</b> of ${ROWS.length.toLocaleString()} loans`;
  ['#ftableView','#fgridView','#flistView'].forEach(s=>$(s).classList.toggle('hide',s!==(VIEW==='table'?'#ftableView':VIEW==='list'?'#flistView':'#fgridView')));
  const ss=$('#fsort'); if(ss) ss.value = sortKey ? sortKey+':'+(sortDir<0?'desc':'asc') : '';
  if(!rows.length){ $('#femptymsg').style.display='block'; $('#femptymsg').textContent='no matches for these filters.'; }
  else $('#femptymsg').style.display='none';
  // Windowed infinite scroll (was rendering ALL ~7,400 loans at once).
  _flRows=rows; _flCols=cols;
  if(!_flInf&&window.InfScroll) _flInf=InfScroll.attach({batch:120,getRows:()=>_flRows,getMount:()=>$(VIEW==='table'?'#ftableView':VIEW==='list'?'#flistView':'#fgridView'),render:_flPaint});
  if(_flInf) _flInf.reset(); else _flPaint(rows);
}

// ── expandable row → detail modal (all fields) ───────────────────────────────
function openDetail(i){
  const r=ROWS[+i]; if(!r) return;
  const ov=$('#fov'), mb=$('#fmbody'); ov.classList.add('on');
  const rowsHtml=COLS.map(c=>`<div class="litem"><span class="m">${esc(c.l)}</span><span>${cell(r,c)}</span></div>`).join('');
  // Any extra fields the API returned that aren't defined columns:
  const known=new Set(COLS.map(c=>c.k).concat(['red','belowMarket','historicLowEra','redReasons','__i']));
  const extra=Object.keys(r).filter(k=>!known.has(k)&&r[k]!=null&&typeof r[k]!=='object')
    .map(k=>`<div class="litem"><span class="m">${esc(k)}</span><span>${esc(r[k])}</span></div>`).join('');
  mb.innerHTML=`<h3>${esc(r.property||'(name n/a)')}</h3>`
    +`<div class="msub">${esc([r.city,r.state].filter(Boolean).join(', ')+' '+(r.zip||''))}${r.holder?' · '+esc(r.holder):''}</div>`
    +(r.red?`<div style="margin:6px 0 10px">${badge(r)}</div>`:'')
    +rowsHtml+extra
    +((r.redReasons&&r.redReasons.length)?`<div style="margin-top:12px;font-size:12px;color:var(--muted);line-height:1.6">${r.redReasons.map(esc).join('<br>')}</div>`:'');
}

// ── left rail: facets + column toggles ────────────────────────────────────────
function buildRail(){
  let html='';
  FACETS.forEach(fc=>{
    if(fc.type==='chip'){
      const counts={};
      ROWS.forEach(r=>{ const v=gv(r,fc); if(v!=null&&v!=='') counts[v]=(counts[v]||0)+1; });
      const ent=Object.entries(counts).sort((a,b)=>b[1]-a[1]).slice(0,fc.max||20);
      if(!ent.length) return;
      html+=`<div class="fha-sec collapsed"><h4>${esc(fc.l)}</h4><div class="fchips" data-facet="${esc(fc.k)}">`
        +ent.map(([k,c])=>`<span class="fchip" data-v="${esc(k)}">${esc(k)}<span class="fct">${c}</span></span>`).join('')+`</div></div>`;
    } else {
      html+=`<div class="fha-sec collapsed"><h4>${esc(fc.l)}</h4><div class="frow2" data-facet="${esc(fc.k)}">`
        +`<div class="ffld"><label>Min</label><input type="number" class="fmin" placeholder="any"></div>`
        +`<div class="ffld"><label>Max</label><input type="number" class="fmax" placeholder="any"></div></div></div>`;
    }
  });
  // column toggles — every column toggleable, grouped
  const groups={}; COLS.forEach(c=>{ (groups[c.g||'Other']=groups[c.g||'Other']||[]).push(c); });
  html+=`<div class="fha-sec"><h4>Fields / columns — show / hide</h4><div class="ftogs">`
    +Object.entries(groups).map(([g,cs])=>`<div class="fgrp">${esc(g)}</div>`
      +cs.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join('')).join('')
    +`</div><div class="fmini"><button data-all="1">All</button><button data-none="1">None</button>`
    +`<button id="fReset" title="Reset columns to the tight default — order, widths &amp; visibility">↺ Reset</button></div>`
    +`<div style="font-size:10.5px;color:var(--muted);margin-top:7px;line-height:1.35">Drag a header to reorder · drag its right edge to resize · click to sort.</div></div>`;
  $('#fRail').innerHTML=html;

  // wire facets
  $('#fRail').querySelectorAll('.fchips').forEach(box=>box.addEventListener('click',e=>{
    const c=e.target.closest('.fchip'); if(!c) return;
    const set=FF.chips[box.dataset.facet], v=c.dataset.v;
    set.has(v)?set.delete(v):set.add(v); c.classList.toggle('active'); render();
  }));
  $('#fRail').querySelectorAll('.frow2').forEach(box=>{
    const k=box.dataset.facet;
    box.querySelector('.fmin').addEventListener('input',e=>{FF.ranges[k].min=e.target.value?+e.target.value:null;render();});
    box.querySelector('.fmax').addEventListener('input',e=>{FF.ranges[k].max=e.target.value?+e.target.value:null;render();});
  });
  // column toggles
  $('#fRail').querySelector('.ftogs').addEventListener('change',e=>{const cb=e.target.closest('input[data-ck]');if(!cb)return;VISCOL[cb.dataset.ck]=cb.checked;saveCols();render();});
  const mini=$('#fRail').querySelector('.fmini');
  mini.addEventListener('click',e=>{
    const b=e.target.closest('button'); if(!b) return;
    if(b.id==='fReset'){ VISCOL={}; saveCols(); COLORDER=[]; saveColOrder();
      try{Object.keys(localStorage).forEach(k=>{ if(k.indexOf('cr:'+location.pathname+':')===0) localStorage.removeItem(k); });}catch(e){}
      lastColSig=''; buildRail(); render(); return; }
    if(b.dataset.all) COLS.forEach(c=>VISCOL[c.k]=true);
    else if(b.dataset.none) COLS.forEach(c=>VISCOL[c.k]=false);
    saveCols(); buildRail(); render();
  });
  // collapsible headers
  $('#fRail').querySelectorAll('.fha-sec > h4').forEach(h=>h.addEventListener('click',()=>h.parentElement.classList.toggle('collapsed')));
}

// ── wire header sort + drag-reorder + row-click + search + density ─────────────
function wireGrid(){
  const thead=$('#fthead');
  thead.addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;
    if(sortKey===k)sortDir*=-1;else{const c=COLS.find(x=>x.k===k);sortKey=k;sortDir=(c&&(c.t==='money'||c.t==='num'||c.t==='pct'||c.k==='originationDate'))?-1:1;}
    localStorage.setItem('fhaSortKey',sortKey);localStorage.setItem('fhaSortDir',sortDir);render();});
  // drag-to-reorder columns
  let dragKey=null;
  thead.addEventListener('dragstart',e=>{ const th=e.target.closest('th'); if(!th) return;
    if(e.target.closest('.cr-handle')){ e.preventDefault(); return; }
    dragKey=th.dataset.k; e.dataTransfer.effectAllowed='move'; try{e.dataTransfer.setData('text/plain',dragKey);}catch(_){}
    th.classList.add('dragging'); });
  thead.addEventListener('dragover',e=>{ if(!dragKey) return; const th=e.target.closest('th'); if(!th||th.dataset.k===dragKey) return;
    e.preventDefault(); e.dataTransfer.dropEffect='move';
    const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
    th.classList.toggle('dropR',after); th.classList.toggle('dropL',!after); });
  thead.addEventListener('dragleave',e=>{ const th=e.target.closest('th'); if(th) th.classList.remove('dropL','dropR'); });
  thead.addEventListener('drop',e=>{ if(!dragKey) return; const th=e.target.closest('th'); if(!th) return; e.preventDefault();
    const tgt=th.dataset.k; if(tgt!==dragKey){
      const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
      syncColOrder(); const from=COLORDER.indexOf(dragKey); if(from>=0){
        COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,dragKey);
        saveColOrder(); lastColSig=''; render();
      }
    }
    dragKey=null; });
  thead.addEventListener('dragend',()=>{ document.querySelectorAll('#fthead th.dragging,#fthead th.dropL,#fthead th.dropR').forEach(x=>x.classList.remove('dragging','dropL','dropR')); dragKey=null; });
  // row/card click → detail modal (table rows, grid cards, list rows all carry .frow + data-id)
  $('#ftbody').addEventListener('click',e=>{ const tr=e.target.closest('tr.frow'); if(tr) openDetail(tr.dataset.id); });
  $('#fgridView').addEventListener('click',e=>{ if(e.target.closest('a'))return; const el=e.target.closest('.frow'); if(el) openDetail(el.dataset.id); });
  $('#flistView').addEventListener('click',e=>{ if(e.target.closest('a'))return; const el=e.target.closest('.frow'); if(el) openDetail(el.dataset.id); });
  $('#fov').addEventListener('click',e=>{ if(e.target.id==='fov') $('#fov').classList.remove('on'); });
  // ── 3-view segmented toggle (grid cards / compact list / dense table) ──
  const seg=$('#fviewseg');
  const LABELS={grid:'▦ Grid',list:'☰ List',table:'▤ Table'};
  seg.innerHTML=['grid','list','table'].map(v=>`<button type="button" data-v="${v}" class="${VIEW===v?'active':''}">${LABELS[v]}</button>`).join('');
  seg.addEventListener('click',e=>{ const b=e.target.closest('button[data-v]'); if(!b) return; VIEW=b.dataset.v; try{localStorage.setItem('fhaView',VIEW);}catch(_){}
    seg.querySelectorAll('button').forEach(x=>x.classList.toggle('active',x.dataset.v===VIEW)); render(); });
  // ── all-field Sort <select> (mirrors the header-click sort; every column) ──
  const ss=$('#fsort');
  ss.innerHTML='<option value="">Sort…</option>'+COLS.map(c=>`<option value="${c.k}:desc">${esc(c.l)} ↓</option><option value="${c.k}:asc">${esc(c.l)} ↑</option>`).join('');
  ss.value = sortKey ? sortKey+':'+(sortDir<0?'desc':'asc') : '';
  ss.addEventListener('change',e=>{ const v=e.target.value; if(!v){ return; } const [k,d]=v.split(':'); sortKey=k; sortDir=d==='desc'?-1:1; localStorage.setItem('fhaSortKey',sortKey); localStorage.setItem('fhaSortDir',sortDir); render(); });
  // search
  const qi=$('#q'); qi.addEventListener('input',()=>{ clearTimeout(qi._t); qi._t=setTimeout(()=>{q=qi.value.trim();render();},140); });
  // density → column count / card min (drives table font scale here via --tsz stays; use --cg? keep simple: font size + col width n/a for table → adjust --tsz)
  const dz=$('#density');
  const applyDensity=v=>document.documentElement.style.setProperty('--tsz',(10+(+v-4)/20*6).toFixed(1)+'px');
  const dv=parseInt(localStorage.getItem('fhaDensity'),10), dval=(dv>=4&&dv<=24)?dv:13;
  dz.value=dval; applyDensity(dval);
  dz.addEventListener('input',()=>{const v=+dz.value;applyDensity(v);localStorage.setItem('fhaDensity',v);});
  // restore saved sort
  const sk=localStorage.getItem('fhaSortKey'), sd=localStorage.getItem('fhaSortDir');
  if(sk&&COLS.some(c=>c.k===sk))sortKey=sk; if(sd==='1'||sd==='-1')sortDir=+sd;
}

function mountGrid(rows){
  ROWS=rows.map((r,i)=>{ r.__i=i; return r; });
  buildRail(); wireGrid(); render();
}

function renderTiles(d){
  const tiles=[
    {k:'Loans ≤3.5%',v:F.num(d.count),cls:'gold',s:'original interest rate at or under 3.5%'},
    {k:'Flagged RED',v:F.num(d.redCount),cls:'red',s:'below-market OR historic-low-era'},
    {k:'Below market',v:F.num((d.redSplit.belowMarketOnly||0)+(d.redSplit.both||0)),cls:'violet',s:'≥0.75pt under the month avg'},
    {k:'Historic-low era',v:F.num((d.redSplit.historicLowEraOnly||0)+(d.redSplit.both||0)),cls:'teal',s:'closed in a rate-trough window'},
  ];
  $('#tiles').innerHTML=tiles.map((t,i)=>
    `<div class="glass tile" style="animation-delay:${.05+i*.04}s"><div class="k">${t.k}</div>`+
    `<div class="v ${t.cls}">${t.v}</div><div class="s">${t.s}</div></div>`).join('');
}

async function load(){
  let d;
  try{ d=await (await fetch('/data/fha-loans.json')).json(); }
  catch(e){ $('#asof').textContent='data load error — run scripts/fetch-fha-loans.js to populate data/fha-loans.json'; const em=$('#femptymsg'); if(em){em.style.display='block';em.innerHTML='No data file. Run <code>node scripts/fetch-fha-loans.js</code> to fetch real HUD + PMMS data.';} return; }
  DATA=d; ROWS=d.rows||[];
  renderTiles(d);
  // source links
  const s=d.sources||[];
  const find=sub=>(s.find(x=>(x.url||'').includes(sub))||{}).url||'#';
  $('#srcHud').href=find('hud.gov'); $('#srcPmms').href=find('freddiemac'); $('#srcFred').href=find('fred');
  const w=(d.historicLowWindows||[]).map(x=>x.start+'…'+x.end).join(', ');
  $('#windows').textContent=w?(' · historic-low windows: '+w):'';
  const gen=d.generatedAt?new Date(d.generatedAt).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}):'';
  $('#asof').textContent=`${F.num(d.count)} HUD/FHA multifamily loans ≤3.5% · ${F.num(d.redCount)} red · benchmarked vs Freddie Mac PMMS · updated ${gen}`;
  mountGrid(ROWS);
}
load();
</script>
  <script src="/col-resize.js" defer></script>
  <script src="/global-search.js" defer></script>
<script src="/corner-nav.js" defer></script>
<script>
// Day/Night theme — pre-applied before first paint (see head snippet).
// apply() only PAINTS — does NOT persist (passive loads must not lock in a theme;
// only an explicit toggle click below saves a pref, so everyone gets the Day default).
(function(){
  var root=document.documentElement;
  function applyTheme(t){ root.setAttribute('data-theme',t); }
  var cur; try{ cur=localStorage.getItem('crcp-theme'); }catch(e){}
  applyTheme(cur==='dark'?'dark':'light');
  // Wire up the toggle button injected by crcp-theme.js (defer — may not exist yet;
  // observe the header for it and intercept its click to persist crcp-theme).
  function wireToggle(){
    var btn=document.getElementById('crcpThemeToggle'); if(!btn||btn.__wired2) return;
    btn.__wired2=true;
    btn.addEventListener('click',function(e){
      e.stopImmediatePropagation();   // suppress crcp-theme.js's own handler so we don't double-toggle
      var nt=root.getAttribute('data-theme')==='light'?'dark':'light';
      applyTheme(nt); try{ localStorage.setItem('crcp-theme',nt); }catch(e){}
    },true);   // capture phase — fires before crcp-theme.js's own click handler
  }
  if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',wireToggle); else wireToggle();
  // also wire after defer scripts finish (crcp-theme.js injects the button on DOMContentLoaded)
  document.addEventListener('DOMContentLoaded',wireToggle);
})();
</script>
<script src="/crcp-theme.js" defer></script>
  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>