← 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,'&').replace(/</g,'<').replace(/"/g,'"');
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,'"')}">BELOW MARKET</span>`;
}
if(r.historicLowEra){
const t=reasons.find(x=>/historic/.test(x))||'';
html+=`<span class="whybadge era" title="${t.replace(/"/g,'"')}">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 & 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>