← back to Commercialrealestate
public/condos.html
334 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='10' text-anchor='middle' fill='%233fb950'%3E%F0%9F%8F%A2%3C/text%3E%3C/svg%3E">
<title>LA Condos — Warrantability (FHA-approval proxy)</title>
<style>
:root{ --bg:#0e1116; --card:#161b22; --line:#2a313c; --ink:#e6edf3; --mut:#8b949e; --acc:#3fb950; --blue:#58a6ff; --gold:#d29922; --red:#f85149; --purp:#bc8cff; }
*{box-sizing:border-box;} html,body{margin:0;}
body{background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;}
header{display:flex;align-items:center;gap:16px;padding:14px 22px;border-bottom:1px solid var(--line);position:sticky;top:0;background:rgba(14,17,22,.98);backdrop-filter:blur(8px);z-index:30;}
header h1{font-size:18px;margin:0;} header a{color:var(--blue);text-decoration:none;font-size:13px;}
header .nav{margin-left:auto;display:flex;gap:18px;}
.banner{font-size:12px;color:var(--gold);padding:9px 22px;background:rgba(210,153,34,.08);border-bottom:1px solid rgba(210,153,34,.25);}
input[type=text],input[type=number]{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 11px;font-size:13px;}
input[type=range]{accent-color:var(--blue);}
.count{font-size:12px;color:var(--mut);margin-left:auto;} .count b{color:var(--ink);}
/* ---- Amazon-style left rail ---- */
.shell{display:flex;align-items:flex-start;}
.rail{flex:0 0 262px;background:#0b0e13;border-right:1px solid var(--line);align-self:stretch;position:sticky;top:53px;height:calc(100vh - 53px);overflow-y:auto;padding:0 0 40px;}
.rail .rsec{padding:13px 16px;border-bottom:1px solid var(--line);}
.rail h4{margin:0 0 9px;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--mut);}
.chips{display:flex;flex-wrap:wrap;gap:6px;}
.chip{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:16px;padding:4px 10px;font-size:12px;cursor:pointer;user-select:none;}
.chip:hover{color:var(--ink);} .chip.active{border-color:var(--blue);color:var(--blue);background:rgba(88,166,255,.08);}
.chip.warr-unwarrantable.active{border-color:var(--red);color:var(--red);background:rgba(248,81,73,.10);}
.chip .ct{color:var(--mut);font-size:10px;margin-left:4px;}
.row2{display:flex;gap:8px;} .fld{flex:1;} .fld label{display:block;font-size:10px;color:var(--mut);margin-bottom:3px;}
.rail input[type=number]{width:100%;}
.fieldtoggles{display:flex;flex-direction:column;gap:1px;max-height:320px;overflow-y:auto;}
.ftog{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink);cursor:pointer;padding:3px 4px;border-radius:6px;} .ftog:hover{background:rgba(88,166,255,.06);} .ftog input{accent-color:var(--blue);}
.minibtn{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:7px;padding:4px 9px;font-size:11px;cursor:pointer;margin-right:5px;} .minibtn:hover{border-color:var(--blue);color:var(--ink);}
main{flex:1 1 auto;min-width:0;padding:0 0 40px;}
.topbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:14px 24px;border-bottom:1px solid var(--line);position:sticky;top:53px;background:rgba(14,17,22,.95);backdrop-filter:blur(6px);z-index:20;}
.topbar input[type=text]{min-width:260px;}
.topbar .dens{font-size:12px;color:var(--mut);display:flex;align-items:center;gap:7px;}
.viewseg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;}
.viewseg button{background:var(--card);color:var(--mut);border:0;border-right:1px solid var(--line);padding:7px 12px;font-size:12px;cursor:pointer;}
.viewseg button:last-child{border-right:0;} .viewseg button.active{background:#1f6feb;color:#fff;}
.gridwrap{padding:24px;}
.grid{display:grid;gap:22px;grid-template-columns:repeat(var(--cols,3),minmax(0,1fr));}
.grid.listhost{display:block;}
.condo{background:var(--card);border:2px solid #fff;border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:6px;}
.condo .name{font-weight:600;font-size:14px;}
.condo .addr{font-size:12px;color:var(--mut);}
.row{display:flex;justify-content:space-between;font-size:12px;gap:10px;}
.row .k{color:var(--mut);}
.badge{display:inline-block;padding:2px 9px;border-radius:11px;font-size:11px;font-weight:700;width:fit-content;}
.badge.fha_approved{background:rgba(63,185,80,.16);color:var(--acc);}
.badge.fha_expired{background:rgba(210,153,34,.16);color:var(--gold);}
.badge.heuristic_flag{background:rgba(248,81,73,.16);color:var(--red);}
.badge.not_listed{background:rgba(139,148,158,.16);color:var(--mut);}
.src{font-size:11px;color:var(--mut);border-top:1px solid var(--line);padding-top:7px;margin-top:2px;}
.broker{font-size:11px;color:var(--purp);}
.empty{padding:50px;text-align:center;color:var(--mut);}
.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;}
.listtbl td.stick,.listtbl th.stick{text-align:left;position:sticky;left:0;background:var(--card);}
.listtbl th.stick{background:#0b0e13;}
.listtbl tr:hover td{background:rgba(88,166,255,.06);}
.listtbl .la{font-weight:600;color:var(--ink);} .listtbl .lc{color:var(--mut);font-size:11px;}
@media(max-width:820px){ .rail{display:none;} }
:focus-visible { outline:2px solid var(--blue); outline-offset:2px; }
@media (prefers-reduced-motion: reduce){ *,*::before,*::after { transition-duration:.001ms !important; animation-duration:.001ms !important; } }
.rail .rsec > h4 { cursor:pointer; display:flex; align-items:center; justify-content:space-between; user-select:none; }
.rail .rsec > h4::after { content:'▾'; color:var(--mut); font-size:11px; margin-left:8px; transition:transform .15s; }
.rail .rsec.collapsed > h4::after { transform:rotate(-90deg); }
.rail .rsec.collapsed > *:not(h4) { display:none !important; }
.starbtn { background:none; border:0; color:var(--mut); font-size:16px; line-height:1; cursor:pointer; padding:0; }
.starbtn:hover, .starbtn.on { color:var(--gold); }
.condo .chead { display:flex; justify-content:space-between; gap:8px; align-items:flex-start; }
#shortbtn { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:8px; padding:7px 12px; font-size:12px; cursor:pointer; }
#shortbtn.shorton { border-color:var(--gold); color:var(--gold); }
.statsbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:9px 24px; border-bottom:1px solid var(--line); }
.statsbar:empty { display:none; }
.statpill { font-size:12px; color:var(--ink); background:var(--card); border:1px solid var(--line); border-radius:8px; padding:4px 10px; }
.statpill .sl { color:var(--mut); }
</style>
</head>
<body>
<header>
<h1>🏢 LA Condos — Warrantability</h1>
<div class="nav"><a href="/">← properties</a><a href="/mls.html">MLS</a><a href="/graphics.html">graphics</a><a href="/fha-loans.html">FHA loans</a></div>
</header>
<div class="banner" id="banner">⚖️ Loading warrantability label…</div>
<div class="shell">
<aside class="rail">
<div class="rsec"><h4>Warrantability</h4><div class="chips" id="fWarr"></div></div>
<div class="rsec"><h4>City</h4><div class="chips" id="fCity"></div></div>
<div class="rsec" id="firmSec"><h4>Listing firm</h4><div class="chips" id="fFirm"></div></div>
<div class="rsec" id="bedSec"><h4>Beds (min)</h4><div class="chips" id="fBeds"></div></div>
<div class="rsec" id="bathSec"><h4>Baths (min)</h4><div class="chips" id="fBaths"></div></div>
<div class="rsec"><h4>Price</h4><div class="row2"><div class="fld"><label>Min $</label><input type="number" id="pMin" step="50000" placeholder="any"></div><div class="fld"><label>Max $</label><input type="number" id="pMax" step="50000" placeholder="any"></div></div></div>
<div class="rsec"><h4>HOA / mo</h4><div class="row2"><div class="fld"><label>Min $</label><input type="number" id="hoaMin" step="50" placeholder="any"></div><div class="fld"><label>Max $</label><input type="number" id="hoaMax" step="50" placeholder="any"></div></div></div>
<div class="rsec"><h4>Sq Ft</h4><div class="row2"><div class="fld"><label>Min</label><input type="number" id="sqMin" step="100" placeholder="any"></div><div class="fld"><label>Max</label><input type="number" id="sqMax" step="100" placeholder="any"></div></div></div>
<div class="rsec"><h4>$ / Sq Ft</h4><div class="row2"><div class="fld"><label>Min</label><input type="number" id="psfMin" step="25" placeholder="any"></div><div class="fld"><label>Max</label><input type="number" id="psfMax" step="25" placeholder="any"></div></div></div>
<div class="rsec"><h4>Year built</h4><div class="row2"><div class="fld"><label>After</label><input type="number" id="yrMin" step="1" placeholder="any"></div><div class="fld"><label>Before</label><input type="number" id="yrMax" step="1" placeholder="any"></div></div></div>
<div class="rsec"><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>
</aside>
<main>
<div class="topbar">
<input type="text" id="q" placeholder="search project / city / zip / address / broker / firm…">
<span class="viewseg" id="viewseg"><button data-view="grid" class="active">▦ Grid</button><button data-view="list">☰ List</button></span>
<button id="shortbtn" title="Show only your shortlisted (starred) listings — shared with the Explorer & MLS">⭐ Shortlist (0)</button>
<select id="csort" title="Sort condos" style="background:var(--card,#141925);color:var(--ink,#E6E8EC);border:1px solid var(--line,#232a38);border-radius:7px;padding:5px 8px;font-size:12px">
<option value="">Sort: Newest</option>
<option value="price_desc">Sort: Price ↓</option>
<option value="price_asc">Sort: Price ↑</option>
<option value="pps_desc">Sort: $/SF ↓</option>
<option value="year_desc">Sort: Year (newest)</option>
<option value="hoa_asc">Sort: HOA ↑</option>
<option value="proj_az">Sort: Project A→Z</option>
<option value="city_az">Sort: City A→Z</option>
</select>
<label class="dens">Density <input type="range" id="cols" min="1" max="20" step="1" value="3"></label>
<span class="count" id="count">…</span>
</div>
<div class="statsbar" id="statsbar"></div>
<div class="gridwrap"><div class="grid" id="grid"><div class="empty">loading…</div></div></div>
</main>
</div>
<script>
const $=s=>document.querySelector(s);
const $$=s=>Array.from(document.querySelectorAll(s));
const esc=s=>String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');
const PROXY='FHA/VA-approval-based proxy, NOT lender-verified Fannie/Freddie warrantability.';
const NOW_YEAR=new Date().getFullYear();
let DATA=[], MODE='db', VIEW=localStorage.getItem('condoView')||'grid', bannerDefault='';
let SORTKEY=localStorage.getItem('condoSort')||''; // hoisted so early apply() during boot never hits TDZ
function statusBadge(s){
const txt={ fha_approved:'FHA-APPROVED · proxy warrantable', fha_expired:'FHA-EXPIRED · re-cert', heuristic_flag:'FLAGGED · review', not_listed:'NOT LISTED · verify' }[s]||s;
return `<span class="badge ${s}">${txt}</span>`;
}
// ---- every data point (incl. computed) — each toggleable, drives cards + list columns ----
const FIELDS=[
{k:'warrantable_status', l:'Warrantability', badge:1, col:1, def:1},
{k:'price', l:'Price', money:1, col:1, def:1},
{k:'hoa', l:'HOA/mo', money:1, col:1, def:1, suf:'/mo'},
{k:'beds', l:'Beds', col:1, def:1},
{k:'baths', l:'Baths', col:1, def:1},
{k:'sqft', l:'Sq Ft', num:1, col:1, def:1},
{k:'ppsf', l:'$/Sq Ft', money:1, col:1, def:1, calc:c=>c.sqft?Math.round(c.price/c.sqft):null},
{k:'ppbed', l:'$/Bed', money:1, col:1, def:0, calc:c=>c.beds?Math.round(c.price/c.beds):null},
{k:'year_built', l:'Year built', col:1, def:1},
{k:'age', l:'Age (yrs)', col:1, def:0, calc:c=>c.year_built?(NOW_YEAR-c.year_built):null},
{k:'city', l:'City', col:1, def:1},
{k:'zip', l:'ZIP', col:1, def:0},
{k:'project_name',l:'Project', col:1, def:0},
{k:'broker_name', l:'Broker', col:1, def:1},
{k:'firm_name', l:'Firm', col:1, def:0},
{k:'warrant_source',l:'Warrant note', col:0, def:0},
{k:'created_at', l:'Added', date:1, col:1, def:0},
];
const VIS=(function(){ let s={}; try{s=JSON.parse(localStorage.getItem('condoFields')||'{}');}catch(e){} const o={}; FIELDS.forEach(f=>o[f.k]=(f.k in s)?!!s[f.k]:!!f.def); return o; })();
function fvis(k){ return !!VIS[k]; }
function saveVis(){ try{localStorage.setItem('condoFields',JSON.stringify(VIS));}catch(e){} }
function fval(c,f){ return f.calc?f.calc(c):c[f.k]; }
function fcell(c,f){ const v=fval(c,f);
if(f.badge) return statusBadge(c.warrantable_status||c.warrant_signal||'—');
if(v==null||v==='') return '—';
if(f.money) return '$'+(+v).toLocaleString()+(f.suf||'');
if(f.num) return (+v).toLocaleString();
if(f.date){ try{ return new Date(v).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }catch(e){ return '—'; } }
return esc(v)+(f.suf||'');
}
// ---- facet state ----
// unified shortlist — same localStorage key + listing ids as the Explorer & MLS
const SHORT=new Set((function(){ try{ return JSON.parse(localStorage.getItem('cre_shortlist')||'[]'); }catch(e){ return []; } })());
function saveShort(){ try{ localStorage.setItem('cre_shortlist', JSON.stringify([...SHORT])); }catch(e){} }
function toggleStar(id){ SHORT.has(id)?SHORT.delete(id):SHORT.add(id); saveShort(); }
function updateShortBtn(){ const sb=$('#shortbtn'); if(sb){ sb.textContent='⭐ Shortlist ('+SHORT.size+')'; sb.classList.toggle('shorton',F.shortlist); } }
function median(a){ if(!a.length) return null; const s=a.slice().sort((x,y)=>x-y); const m=Math.floor(s.length/2); return s.length%2?s[m]:(s[m-1]+s[m])/2; }
function renderStats(rows){ const sb=$('#statsbar'); if(!sb) return; if(!rows.length){ sb.innerHTML=''; return; }
const prices=rows.map(c=>+c.price).filter(v=>v>0);
const pps=rows.map(c=>c.sqft?+c.price/c.sqft:null).filter(v=>v>0);
const hoas=rows.map(c=>+c.hoa).filter(v=>v>0);
const yrs=rows.map(c=>+c.year_built).filter(v=>v>1800);
const pill=(l,v)=>v==null?'':`<span class="statpill"><span class="sl">${l}</span> <b>${v}</b></span>`;
sb.innerHTML=pill('Count',rows.length)+pill('Median price',prices.length?'$'+Math.round(median(prices)).toLocaleString():null)+pill('Median $/SF',pps.length?'$'+Math.round(median(pps)).toLocaleString():null)+pill('Median HOA',hoas.length?'$'+Math.round(median(hoas)).toLocaleString()+'/mo':null)+pill('Median year',yrs.length?Math.round(median(yrs)):null);
}
const F={ warr:'all', cities:new Set(), firms:new Set(), bedsMin:0, bathsMin:0, shortlist:false,
pmin:null,pmax:null, hoamin:null,hoamax:null, sqftMin:null,sqftMax:null, yearMin:null,yearMax:null, ppsfMin:null,ppsfMax:null };
function rng(v,mn,mx){ if(mn!=null){ if(v==null||!(+v>=mn)) return false; } if(mx!=null){ if(v==null||!(+v<=mx)) return false; } return true; }
function passFacets(c){
const ws=c.warrantable_status||c.warrant_signal;
if(!(F.warr==='all'?true:F.warr==='unwarrantable'?ws!=='fha_approved':ws===F.warr)) return false;
if(F.cities.size&&!F.cities.has(c.city)) return false;
if(F.firms.size&&!F.firms.has(c.firm_name)) return false;
if(F.bedsMin&&!(+c.beds>=F.bedsMin)) return false;
if(F.bathsMin&&!(+c.baths>=F.bathsMin)) return false;
if(!rng(c.price,F.pmin,F.pmax)) return false;
if(!rng(c.hoa,F.hoamin,F.hoamax)) return false;
if(!rng(c.sqft,F.sqftMin,F.sqftMax)) return false;
if(!rng(c.year_built,F.yearMin,F.yearMax)) return false;
if(!rng(c.sqft?c.price/c.sqft:null,F.ppsfMin,F.ppsfMax)) return false;
if(F.shortlist && !SHORT.has(c.id)) return false;
return true;
}
// ---- rail builder ----
function buildRail(){
const wc={},cc={},fc={};
DATA.forEach(c=>{ const s=c.warrantable_status||c.warrant_signal||'?'; wc[s]=(wc[s]||0)+1; if(c.city)cc[c.city]=(cc[c.city]||0)+1; if(c.firm_name)fc[c.firm_name]=(fc[c.firm_name]||0)+1; });
const order=[['all','All'],['fha_approved','FHA-approved'],['fha_expired','FHA-expired'],['not_listed','Not-listed'],['heuristic_flag','Flagged'],['unwarrantable','⚑ Unwarrantable · non-FHA']];
$('#fWarr').innerHTML=order.filter(([v])=>v==='all'||v==='unwarrantable'||wc[v]).map(([v,l])=>{
const n=v==='all'?DATA.length:v==='unwarrantable'?(DATA.length-(wc.fha_approved||0)):wc[v];
return `<span class="chip warr-${v} ${F.warr===v?'active':''}" data-warr="${v}">${l}<span class="ct">${n}</span></span>`;
}).join('');
$('#fCity').innerHTML=Object.entries(cc).sort((a,b)=>b[1]-a[1]).slice(0,20).map(([k,c])=>`<span class="chip ${F.cities.has(k)?'active':''}" data-cy="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
const firms=Object.entries(fc).sort((a,b)=>b[1]-a[1]).slice(0,14);
$('#firmSec').style.display=firms.length?'':'none';
$('#fFirm').innerHTML=firms.map(([k,c])=>`<span class="chip ${F.firms.has(k)?'active':''}" data-fm="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
const hasBeds=DATA.some(c=>c.beds!=null), hasBaths=DATA.some(c=>c.baths!=null);
$('#bedSec').style.display=hasBeds?'':'none'; $('#bathSec').style.display=hasBaths?'':'none';
$('#fBeds').innerHTML=[0,1,2,3,4].map(n=>`<span class="chip ${F.bedsMin===n?'active':''}" data-beds="${n}">${n===0?'Any':n+'+'}</span>`).join('');
$('#fBaths').innerHTML=[0,1,2,3].map(n=>`<span class="chip ${F.bathsMin===n?'active':''}" data-baths="${n}">${n===0?'Any':n+'+'}</span>`).join('');
buildFieldToggles();
}
function buildFieldToggles(){ $('#fFields').innerHTML=FIELDS.map(f=>`<label class="ftog"><input type="checkbox" data-fk="${f.k}" ${fvis(f.k)?'checked':''}><span>${esc(f.l)}</span>${f.col?'<span class="ct" title="Also a List column">▤</span>':''}</label>`).join(''); }
// ---- renderers ----
const CARD_SKIP=new Set(['warrantable_status','city','zip','project_name','warrant_source']);
function dbCards(rows){
const vis=FIELDS.filter(f=>fvis(f.k)&&!CARD_SKIP.has(f.k));
return rows.map(c=>`<div class="condo">
<div class="chead"><div class="name">${esc(c.project_name||c.address||'Condo')}</div><button class="starbtn ${SHORT.has(c.id)?'on':''}" data-star="${c.id}" aria-label="${SHORT.has(c.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(c.id)}" title="Shortlist">★</button></div>
<div class="addr">${esc(c.address||'')}${c.city?', '+esc(c.city):''} ${esc(c.zip||'')}</div>
${fvis('warrantable_status')?statusBadge(c.warrantable_status||c.warrant_signal):''}
${vis.map(f=>`<div class="row"><span class="k">${esc(f.l)}</span><span>${fcell(c,f)}</span></div>`).join('')}
${(fvis('firm_name')||fvis('broker_name'))&&(c.broker_name||c.firm_name)?`<div class="broker">🧑💼 ${esc(c.broker_name||'')}${c.firm_name?(c.broker_name?' · ':'')+esc(c.firm_name):''}</div>`:''}
${fvis('warrant_source')&&c.warrant_source?`<div class="src">${esc(c.warrant_source)}</div>`:''}
</div>`).join('');
}
function dbList(rows){
const cols=FIELDS.filter(f=>f.col&&fvis(f.k));
const head='<th class="stick">Project</th>'+cols.map(f=>`<th>${esc(f.l)}</th>`).join('');
const body=rows.map(c=>`<tr><td class="stick"><button class="starbtn ${SHORT.has(c.id)?'on':''}" data-star="${c.id}" aria-label="${SHORT.has(c.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(c.id)}" title="Shortlist">★</button> <span class="la">${esc(c.project_name||c.address||'Condo')}</span><div class="lc">${esc(c.address||'')}${c.city?', '+esc(c.city):''}</div></td>`
+cols.map(f=>`<td>${fcell(c,f)}</td>`).join('')+'</tr>').join('');
return `<div class="listwrap"><table class="listtbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
}
function paint(rows){ const g=$('#grid');
if(!rows.length){ g.className='grid'; g.innerHTML='<div class="empty">no condos match these filters.</div>'; return; }
if(VIEW==='list'){ g.className='grid listhost'; g.innerHTML=dbList(rows); }
else { g.className='grid'; g.innerHTML=dbCards(rows); }
}
// ---- apply filters + render ----
function apply(){
let rows=DATA.filter(passFacets);
const ql=($('#q').value||'').trim().toLowerCase();
if(ql) rows=rows.filter(c=>((c.project_name||'')+' '+(c.city||'')+' '+(c.zip||'')+' '+(c.address||'')+' '+(c.broker_name||'')+' '+(c.firm_name||'')).toLowerCase().includes(ql));
rows=sortRows(rows);
renderStats(rows); updateShortBtn();
const unw=F.warr==='unwarrantable';
$('#count').innerHTML=`<b>${rows.length}</b> of ${DATA.length} ${unw?'unwarrantable (non-FHA) condos':(MODE==='fha'?'FHA-list projects':'condos')}`;
$('#banner').textContent = unw
? '⚑ Non-warrantable condos — NOT on the FHA-approved list (expired or never listed). Conventional/FHA lenders decline these — non-QM / portfolio financing territory.'
: bannerDefault;
paint(rows);
}
// ---- boot ----
async function boot(){
try{ const r=await (await fetch('/api/condos')).json(); DATA=r.condos||[]; if(r.label) bannerDefault='⚖️ '+r.label; }catch(_){}
if(!DATA.length){
MODE='fha';
try{ const r=await (await fetch('/api/fha-condos?status=all')).json(); DATA=(r.condos||[]).map(c=>({...c,warrantable_status:c.warrant_signal})); bannerDefault='⚖️ '+(( DATA[0]&&DATA[0].label)||PROXY)+' — HUD FHA-approved project directory (no live condo listings loaded; full residential scrape is gated).'; }catch(_){}
}
if(!bannerDefault) bannerDefault='⚖️ '+PROXY;
// deep-link: /condos?status=unwarrantable (or any warrantability value)
const qs=new URLSearchParams(location.search).get('status');
if(qs && ['all','fha_approved','fha_expired','not_listed','heuristic_flag','unwarrantable'].includes(qs)) F.warr=qs;
buildRail(); apply();
}
// ---- wiring ----
function tog(set,v){ set.has(v)?set.delete(v):set.add(v); }
$('#fWarr').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.warr=c.dataset.warr;$$('#fWarr .chip').forEach(x=>x.classList.toggle('active',x.dataset.warr===F.warr));apply();});
$('#fCity').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.cities,c.dataset.cy);c.classList.toggle('active');apply();});
$('#fFirm').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.firms,c.dataset.fm);c.classList.toggle('active');apply();});
$('#fBeds').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.bedsMin=+c.dataset.beds;$$('#fBeds .chip').forEach(x=>x.classList.toggle('active',+x.dataset.beds===F.bedsMin));apply();});
$('#fBaths').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.bathsMin=+c.dataset.baths;$$('#fBaths .chip').forEach(x=>x.classList.toggle('active',+x.dataset.baths===F.bathsMin));apply();});
const rangeWire=(id,key)=>$('#'+id).addEventListener('input',()=>{F[key]=$('#'+id).value?+$('#'+id).value:null;apply();});
rangeWire('pMin','pmin');rangeWire('pMax','pmax');rangeWire('hoaMin','hoamin');rangeWire('hoaMax','hoamax');
rangeWire('sqMin','sqftMin');rangeWire('sqMax','sqftMax');rangeWire('psfMin','ppsfMin');rangeWire('psfMax','ppsfMax');
rangeWire('yrMin','yearMin');rangeWire('yrMax','yearMax');
$('#fFields').addEventListener('change',e=>{const cb=e.target.closest('input[data-fk]');if(!cb)return;VIS[cb.dataset.fk]=cb.checked;saveVis();apply();});
$('#fAll').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=true);saveVis();buildFieldToggles();apply();});
$('#fNone').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=false);saveVis();buildFieldToggles();apply();});
$('#fDef').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=!!f.def);saveVis();buildFieldToggles();apply();});
$('#q').oninput=()=>{ clearTimeout(window._t); window._t=setTimeout(apply,180); };
// ── sort (Steve rule: every grid gets sort + density; condos already had density) ──
function sortRows(rows){
const num=(v)=>{const n=+v;return isFinite(n)?n:null;};
const cmp={
price_desc:(a,b)=>(num(b.price)??-1)-(num(a.price)??-1),
price_asc:(a,b)=>(num(a.price)??Infinity)-(num(b.price)??Infinity),
pps_desc:(a,b)=>((num(b.price)&&num(b.sqft)?b.price/b.sqft:-1))-((num(a.price)&&num(a.sqft)?a.price/a.sqft:-1)),
year_desc:(a,b)=>(num(b.year_built)??-1)-(num(a.year_built)??-1),
hoa_asc:(a,b)=>(num(a.hoa)??Infinity)-(num(b.hoa)??Infinity),
proj_az:(a,b)=>String(a.project_name||a.address||'').localeCompare(String(b.project_name||b.address||'')),
city_az:(a,b)=>String(a.city||'').localeCompare(String(b.city||'')),
}[SORTKEY];
return cmp?rows.slice().sort(cmp):rows;
}
(function(){ const s=$('#csort'); if(s){ s.value=SORTKEY; s.addEventListener('change',()=>{ SORTKEY=s.value; localStorage.setItem('condoSort',SORTKEY); apply(); }); } })();
$('#cols').oninput=e=>{ document.documentElement.style.setProperty('--cols',e.target.value); localStorage.setItem('condoCols',e.target.value); };
const savedCols=localStorage.getItem('condoCols'); if(savedCols){ $('#cols').value=savedCols; document.documentElement.style.setProperty('--cols',savedCols); }
$$('#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('condoView',VIEW); $$('#viewseg button').forEach(x=>x.classList.toggle('active',x.dataset.view===VIEW)); apply(); };
// shortlist stars (unified) + toggle
$('#grid').addEventListener('click',e=>{ const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); updateShortBtn(); if(F.shortlist) apply(); } });
$('#shortbtn').addEventListener('click',()=>{ F.shortlist=!F.shortlist; apply(); });
// collapsible rail tabs — all collapsed on load
const railEl=document.querySelector('.rail');
if(railEl){ railEl.addEventListener('click',e=>{ const h=e.target.closest('h4'); if(h && h.parentElement.classList.contains('rsec')) h.parentElement.classList.toggle('collapsed'); });
document.querySelectorAll('.rail .rsec').forEach(s=>s.classList.add('collapsed')); }
boot();
</script>
<script src="/col-resize.js" defer></script>
</body>
</html>