← back to Commercialrealestate
nav-agent: add universal grid-controls drop-in (field-toggle panel + sort/group/density + movable/removable/expandable panels) to all internal grid pages
680d4422000488d50f835c275a62e3944f9f548e · 2026-08-13 08:01:41 -0700 · Steve Abrams
Files touched
M data/residential-report.htmlM public/admin.htmlM public/broker-grid.htmlM public/condos.htmlM public/contractors.htmlM public/crcp-grid.cssA public/crcp-grid.css.pre-3view.bakM public/crcp-grid.jsA public/crcp-grid.js.pre-3view.bakM public/crcp.htmlM public/credit-unions.htmlM public/deals-flow.htmlM public/deals.htmlM public/fha-leads.htmlM public/fha-loans.htmlM public/gov-agents.htmlM public/graphics.htmlM public/index.htmlM public/just-listed.htmlM public/lenders.htmlM public/licensed-agents.htmlM public/licensed.htmlM public/loan-officers.htmlM public/mls.htmlM public/mortgage-market.htmlA public/nav-agent/nav-agent.cssA public/nav-agent/nav-agent.jsM public/parcel.htmlM public/rent-rolls.htmlM public/sales.htmlM public/sba-lenders.html
Diff
commit 680d4422000488d50f835c275a62e3944f9f548e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 13 08:01:41 2026 -0700
nav-agent: add universal grid-controls drop-in (field-toggle panel + sort/group/density + movable/removable/expandable panels) to all internal grid pages
---
data/residential-report.html | 24 +-
public/admin.html | 2 +
public/broker-grid.html | 2 +
public/condos.html | 2 +
public/contractors.html | 4 +-
public/crcp-grid.css | 58 ++++-
public/crcp-grid.css.pre-3view.bak | 56 +++++
public/crcp-grid.js | 270 +++++++++++++++++++++--
public/crcp-grid.js.pre-3view.bak | 191 ++++++++++++++++
public/crcp.html | 2 +
public/credit-unions.html | 2 +
public/deals-flow.html | 4 +-
public/deals.html | 2 +
public/fha-leads.html | 4 +-
public/fha-loans.html | 2 +
public/gov-agents.html | 2 +
public/graphics.html | 2 +
public/index.html | 2 +
public/just-listed.html | 4 +-
public/lenders.html | 2 +
public/licensed-agents.html | 2 +
public/licensed.html | 4 +-
public/loan-officers.html | 2 +
public/mls.html | 2 +
public/mortgage-market.html | 2 +
public/nav-agent/nav-agent.css | 93 ++++++++
public/nav-agent/nav-agent.js | 433 +++++++++++++++++++++++++++++++++++++
public/parcel.html | 4 +-
public/rent-rolls.html | 2 +
public/sales.html | 2 +
public/sba-lenders.html | 2 +
31 files changed, 1144 insertions(+), 41 deletions(-)
diff --git a/data/residential-report.html b/data/residential-report.html
index b90076e..c632e39 100644
--- a/data/residential-report.html
+++ b/data/residential-report.html
@@ -1,46 +1,46 @@
<!doctype html><html><body style="margin:0;background:#0e1116;color:#e6edf3;font-family:-apple-system,Helvetica,Arial,sans-serif;padding:22px">
<div style="max-width:760px;margin:0 auto">
- <h1 style="font-size:20px;margin:0 0 3px">🏠 LA County Residential Pulse <span style="color:#3fb950">· 2026-08-12</span></h1>
+ <h1 style="font-size:20px;margin:0 0 3px">🏠 LA County Residential Pulse <span style="color:#3fb950">· 2026-08-13</span></h1>
<div style="color:#8b949e;font-size:12px;margin-bottom:16px">Redfin for-sale + in-escrow · last 7 days · <a href="https://crcp.agentabrams.com/mls.html" style="color:#58a6ff">open all listings →</a></div>
<table style="width:100%;border-spacing:8px;margin:-8px 0 18px"><tr>
<td style="padding:14px 16px;background:#161b22;border:1px solid #2a313c;border-radius:10px;text-align:center">
- <div style="font-size:26px;font-weight:700;color:#e6edf3">13,933</div>
+ <div style="font-size:26px;font-weight:700;color:#e6edf3">13,907</div>
<div style="font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;margin-top:2px">Active</div></td><td style="padding:14px 16px;background:#161b22;border:1px solid #2a313c;border-radius:10px;text-align:center">
- <div style="font-size:26px;font-weight:700;color:#d29922">7,862</div>
+ <div style="font-size:26px;font-weight:700;color:#d29922">7,953</div>
<div style="font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;margin-top:2px">In Escrow</div></td><td style="padding:14px 16px;background:#161b22;border:1px solid #2a313c;border-radius:10px;text-align:center">
- <div style="font-size:26px;font-weight:700;color:#3fb950">1,442</div>
+ <div style="font-size:26px;font-weight:700;color:#3fb950">1,355</div>
<div style="font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;margin-top:2px">Just Listed</div></td><td style="padding:14px 16px;background:#161b22;border:1px solid #2a313c;border-radius:10px;text-align:center">
- <div style="font-size:26px;font-weight:700;color:#ffa600">799</div>
+ <div style="font-size:26px;font-weight:700;color:#ffa600">774</div>
<div style="font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;margin-top:2px">Price Cuts</div></td><td style="padding:14px 16px;background:#161b22;border:1px solid #2a313c;border-radius:10px;text-align:center">
- <div style="font-size:26px;font-weight:700;color:#58a6ff">42</div>
+ <div style="font-size:26px;font-weight:700;color:#58a6ff">39</div>
<div style="font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;margin-top:2px">Just Sold</div></td><td style="padding:14px 16px;background:#161b22;border:1px solid #2a313c;border-radius:10px;text-align:center">
- <div style="font-size:26px;font-weight:700;color:#f85149">7,500</div>
+ <div style="font-size:26px;font-weight:700;color:#f85149">7,662</div>
<div style="font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;margin-top:2px">Withdrawn</div></td>
</tr></table>
<h3 style="font-size:13px;text-transform:uppercase;color:#8b949e;margin:18px 0 6px">🆕 Just Listed (newest 7d)</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tr style="color:#8b949e;text-align:left"><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Address</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">City</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Type</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Price</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Bd/Ba</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">DOM</th></tr>
- <tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/1883-Rising-Glen-Rd-90069/home/7122213" style="color:#58a6ff;text-decoration:none">1883 Rising Glen Rd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$9,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/6</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Arcadia/800-Hampton-Rd-91006/home/7229012" style="color:#58a6ff;text-decoration:none">800 Hampton</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Arcadia</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$9,880,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">6/9</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Westlake-Village/949-W-Stafford-Rd-91361/home/196682854" style="color:#58a6ff;text-decoration:none">949 W Stafford</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Westlake Village</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,900,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Manhattan-Beach/840-18th-St-90266/home/6707484" style="color:#58a6ff;text-decoration:none">840 18th</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Manhattan Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,499,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Hidden-Hills/25225-Eldorado-Meadow-Rd-91302/home/3303202" style="color:#58a6ff;text-decoration:none">25225 Eldorado Mdw</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Hidden Hills</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,449,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">6/6.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Manhattan-Beach/1658-Ruhland-Ave-90266/home/6701973" style="color:#58a6ff;text-decoration:none">1658 Ruhland Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Manhattan Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$5,799,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/4.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/4830-Los-Feliz-Blvd-90027/home/7133722" style="color:#58a6ff;text-decoration:none">4830 Los Feliz Blvd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$4,595,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/6</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Malibu/6262-Trancas-Canyon-Rd-90265/home/6858134" style="color:#58a6ff;text-decoration:none">6262 Trancas Canyon Rd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Malibu</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$4,350,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/3</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Santa-Clarita/29906-N-Camino-los-Robles-91354/home/204551567" style="color:#58a6ff;text-decoration:none">29906 N Camino Los Robles</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Santa Clarita</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,999,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Sherman-Oaks/15656-Crownridge-Pl-91403/home/4901661" style="color:#58a6ff;text-decoration:none">15656 Crownridge Pl</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Sherman Oaks</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/6</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Hermosa-Beach/567-24th-Pl-90254/home/6714103" style="color:#58a6ff;text-decoration:none">567 24th Pl</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Hermosa Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,950,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/3</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Bradbury/677-Deodar-Ln-91008/home/7974325" style="color:#58a6ff;text-decoration:none">677 N Deodar</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Bradbury</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,900,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">7/5.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Pacific-Palisades/1545-Umeo-Rd-90272/home/6846840" style="color:#58a6ff;text-decoration:none">1545 Umeo Rd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Pacific Palisades</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,799,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">3/2</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/San-Marino/1164-Adair-St-91108/home/7016726" style="color:#58a6ff;text-decoration:none">1164 Adair St</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">San Marino</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,700,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/3.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/San-Dimas/2005-Trail-Ridge-Ln-91773/home/173285979" style="color:#58a6ff;text-decoration:none">2005 Trail Ridge Ln</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">San Dimas</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,695,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Glendale/1663-Vista-Dr-91201/home/7145855" style="color:#58a6ff;text-decoration:none">1663 Vista</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Glendale</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,375,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">7/7</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Rowland-Heights/2251-Cascade-Way-91748/home/8113188" style="color:#58a6ff;text-decoration:none">2251 Cascade</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Rowland Heights</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,350,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/11420-Biona-Dr-90066/home/6738962" style="color:#58a6ff;text-decoration:none">11420 Biona Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,249,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/4</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/San-Marino/1206-Winston-Ave-91108/home/7014928" style="color:#58a6ff;text-decoration:none">1206 Winston Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">San Marino</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,088,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/3.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/247-S-Carmelina-Ave-90049/home/6838700" style="color:#58a6ff;text-decoration:none">247 S Carmelina</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,999,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/4</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr>
+ <tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Westlake-Village/949-W-Stafford-Rd-91361/home/196682854" style="color:#58a6ff;text-decoration:none">949 W Stafford</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Westlake Village</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,900,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Manhattan-Beach/840-18th-St-90266/home/6707484" style="color:#58a6ff;text-decoration:none">840 18th</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Manhattan Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,499,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Hidden-Hills/25225-Eldorado-Meadow-Rd-91302/home/3303202" style="color:#58a6ff;text-decoration:none">25225 Eldorado Mdw</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Hidden Hills</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,449,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">6/6.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/3640-S-Barrington-Ave-90066/home/6739159" style="color:#58a6ff;text-decoration:none">3640 S Barrington Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$5,595,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">6/7</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Studio-City/11210-Briarcliff-Ln-91604/home/8113707" style="color:#58a6ff;text-decoration:none">11210 Briarcliff Ln</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Studio City</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$4,795,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Santa-Clarita/29906-N-Camino-los-Robles-91354/home/204551567" style="color:#58a6ff;text-decoration:none">29906 N Camino Los Robles</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Santa Clarita</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,999,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Pacific-Palisades/1545-Umeo-Rd-90272/home/6846840" style="color:#58a6ff;text-decoration:none">1545 Umeo Rd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Pacific Palisades</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,799,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">3/2</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Diamond-Bar/24106-Lodge-Pole-Rd-91765/home/8064243" style="color:#58a6ff;text-decoration:none">24106 Lodge Pole Rd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Diamond Bar</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,790,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">6/6.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/San-Marino/1164-Adair-St-91108/home/7016726" style="color:#58a6ff;text-decoration:none">1164 Adair St</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">San Marino</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,700,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/3.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/San-Dimas/2005-Trail-Ridge-Ln-91773/home/173285979" style="color:#58a6ff;text-decoration:none">2005 Trail Ridge Ln</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">San Dimas</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,695,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Tarzana/4949-Bilmoor-Ave-91356/home/4239257" style="color:#58a6ff;text-decoration:none">4949 Bilmoor Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Tarzana</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,595,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Calabasas/24142-Park-Riviera-91302/home/3518697" style="color:#58a6ff;text-decoration:none">24142 Park Riviera</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Calabasas</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,450,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Rowland-Heights/2251-Cascade-Way-91748/home/8113188" style="color:#58a6ff;text-decoration:none">2251 Cascade</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Rowland Heights</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,350,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/11420-Biona-Dr-90066/home/6738962" style="color:#58a6ff;text-decoration:none">11420 Biona Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,249,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/4</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/247-S-Carmelina-Ave-90049/home/6838700" style="color:#58a6ff;text-decoration:none">247 S Carmelina</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,999,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/4</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Calabasas/3469-Cordova-Dr-91302/home/3512153" style="color:#58a6ff;text-decoration:none">3469 Cordova Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Calabasas</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,900,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/4</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Calabasas/4433-Tedregal-Ct-91302/home/3511173" style="color:#58a6ff;text-decoration:none">4433 Tedregal Ct</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Calabasas</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,899,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/3.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Santa-Monica/907-23rd-St-90403/home/6769195" style="color:#58a6ff;text-decoration:none">907 23rd St</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Santa Monica</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,800,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">3/2</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Glendale/1357-Doverwood-Dr-91207/home/7165475" style="color:#58a6ff;text-decoration:none">1357 Doverwood</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Glendale</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,799,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/3</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Sherman-Oaks/14800-McCormick-St-91411/home/4744562" style="color:#58a6ff;text-decoration:none">14800 Mccormick</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Sherman Oaks</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,650,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">5/4.5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1d</td></tr>
</table>
<h3 style="font-size:13px;text-transform:uppercase;color:#8b949e;margin:20px 0 6px">✅ Just Sold (closed escrow, 7d)</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tr style="color:#8b949e;text-align:left"><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Address</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">City</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Type</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Sold</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Date</th></tr>
- <tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">859 S Gretna Green Way</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,635,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri Jun 12</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">2005 Mathews Ave Unit B</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Redondo Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,845,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri May 22</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">9620 Sepulveda #39</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">North Hills</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$459,950</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed May 20</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">121 E 223rd St #3</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Carson</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$760,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Thu May 14</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">15111 Freeman</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Lawndale</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$665,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Thu May 07</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">727 Meridian Ave Unit D</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">South Pasadena</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,420,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed May 06</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">1011 N Ogden Dr #103</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">West Hollywood</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,400,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed May 06</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">1149 Magnolia #16</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Gardena</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$420,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed Apr 01</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">3711 Baldwin St #1304</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$687,500</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Tue Mar 31</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">13360 Burbank Blvd #10</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Sherman Oaks</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$775,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Mon Jan 12</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">1826 Garvey #9</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Alhambra</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$699,888</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed Dec 31</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">7650 Topanga Cyn</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Canoga Park</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$425,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Thu Nov 13</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">301 W Loraine St #5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Glendale</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,060,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed Oct 01</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">330 Cordova #307</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Pasadena</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$925,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Thu Sep 25</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">12750 Centralia #91</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Lakewood</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$550,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri Aug 08</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">422 W Route 66 #47</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Glendora</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$610,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Tue Jun 24</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">18347 Saticoy #24</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Reseda</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$625,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri Jun 20</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">18347 Saticoy St #39</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Reseda</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$625,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri Jun 20</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">4040 Grand Vw #65</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$750,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed Jun 18</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">15539 Woodruff #7</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Bellflower</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$660,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri Apr 18</td></tr>
+ <tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">859 S Gretna Green Way</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,635,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri Jun 12</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">607 W Duarte Rd Unit D</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Monrovia</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$765,888</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed May 27</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">2005 Mathews Ave Unit B</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Redondo Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,845,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri May 22</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">121 E 223rd St #3</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Carson</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$760,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Thu May 14</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">727 Meridian Ave Unit D</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">South Pasadena</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,420,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed May 06</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">1011 N Ogden Dr #103</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">West Hollywood</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,400,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed May 06</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">1149 Magnolia #16</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Gardena</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$420,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed Apr 01</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">3711 Baldwin St #1304</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$687,500</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Tue Mar 31</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">13360 Burbank Blvd #10</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Sherman Oaks</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$775,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Mon Jan 12</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">1826 Garvey #9</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Alhambra</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$699,888</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed Dec 31</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">7650 Topanga Cyn</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Canoga Park</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$425,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Thu Nov 13</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">301 W Loraine St #5</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Glendale</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,060,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed Oct 01</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">12750 Centralia #91</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Lakewood</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$550,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri Aug 08</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">422 W Route 66 #47</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Glendora</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$610,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Tue Jun 24</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">18347 Saticoy St #39</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Reseda</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$625,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri Jun 20</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">4040 Grand Vw #65</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$750,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed Jun 18</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">228 S San Marino Ave Unit B</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">San Gabriel</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,055,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Tue May 20</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">15539 Woodruff #7</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Bellflower</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$660,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri Apr 18</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">18730 Hatteras #47</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Tarzana</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$769,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Wed Mar 26</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">10125 De Soto Ave #2</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Chatsworth</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$620,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Fri Dec 13</td></tr>
</table>
<h3 style="font-size:13px;text-transform:uppercase;color:#8b949e;margin:20px 0 6px">💸 Price Cuts (biggest, 7d)</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tr style="color:#8b949e;text-align:left"><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Address</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">City</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Type</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Was</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Now</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Cut</th></tr>
- <tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Malibu/10936-Pacific-View-Dr-90265/home/4694102" style="color:#58a6ff;text-decoration:none">10936 Pacific Vw</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Malibu</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$6,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,000,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$4,500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Manhattan-Beach/3216-The-Strand-90266/home/6710053" style="color:#58a6ff;text-decoration:none">3216 The Strand</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Manhattan Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$24,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$21,000,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$3,995,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Malibu/6625-Zumirez-Dr-90265/home/6856153" style="color:#58a6ff;text-decoration:none">6625 Zumirez Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Malibu</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$28,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$26,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$2,000,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/201-S-Bristol-Ave-90049/home/6839030" style="color:#58a6ff;text-decoration:none">201 S Bristol Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$16,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$15,250,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$1,745,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Pacific-Palisades/1580-Sorrento-Dr-90272/home/6847205" style="color:#58a6ff;text-decoration:none">1580 Sorrento Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Pacific Palisades</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$21,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$19,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$1,505,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/11730-Stonehenge-Ln-90077/home/6831124" style="color:#58a6ff;text-decoration:none">11730 Stonehenge Ln</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$11,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$10,495,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$1,500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/13241-Riviera-Ranch-Rd-90049/home/58301772" style="color:#58a6ff;text-decoration:none">13241 Riviera Ranch Rd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$10,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$9,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$1,000,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Hidden-Hills/6021-John-Muir-Rd-91302/home/3291587" style="color:#58a6ff;text-decoration:none">6021 John Muir Rd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Hidden Hills</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$11,750,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$10,750,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$1,000,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/2666-Aberdeen-Ave-90027/home/7133151" style="color:#58a6ff;text-decoration:none">2666 Aberdeen Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$8,195,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,495,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$700,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Santa-Clarita/10262-Sierra-Hwy-91390/home/6364448" style="color:#58a6ff;text-decoration:none">10262 Sierra Hwy</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Santa Clarita</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,700,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,099,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$601,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Rancho-Palos-Verdes/64-Laurel-Dr-90275/home/177421053" style="color:#58a6ff;text-decoration:none">64 Laurel Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Rancho Palos Verdes</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$8,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,999,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$501,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/521-S-Bentley-Ave-90049/home/6828982" style="color:#58a6ff;text-decoration:none">521 S Bentley</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,649,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,149,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/2495-S-Barrington-Ave-90064/home/6754500" style="color:#58a6ff;text-decoration:none">2495 S Barrington</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,495,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Venice/29-27th-Ave-90291/home/6734171" style="color:#58a6ff;text-decoration:none">29 27th Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Venice</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$10,000,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$9,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Pasadena/954-Hillside-Ter-91105/home/7187442" style="color:#58a6ff;text-decoration:none">954 Hillside Ter</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Pasadena</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$5,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$5,495,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Calabasas/736-Crater-Oak-Dr-91302/home/6854101" style="color:#58a6ff;text-decoration:none">736 Crater Oak Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Calabasas</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,600,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,100,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/3036-Castle-St-90039/home/7062115" style="color:#58a6ff;text-decoration:none">3036 Castle St</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,998,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$498,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Santa-Monica/527-7th-St-90402/home/6782029" style="color:#58a6ff;text-decoration:none">527 7th St</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Santa Monica</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,950,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,495,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$455,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Palos-Verdes-Estates/702-Via-La-Cuesta-90274/home/7723640" style="color:#58a6ff;text-decoration:none">702 via La Cuesta</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Palos Verdes Estates</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$8,398,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,998,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$400,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/801-Devon-Ave-90024/home/6824744" style="color:#58a6ff;text-decoration:none">801 Devon Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,750,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,395,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$355,000</span></td></tr>
+ <tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/10939-Chalon-Rd-90077/home/6829574" style="color:#58a6ff;text-decoration:none">10939 Chalon Rd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$65,000,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$49,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$15,005,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Malibu/10936-Pacific-View-Dr-90265/home/4694102" style="color:#58a6ff;text-decoration:none">10936 Pacific Vw</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Malibu</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$6,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,000,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$4,500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Manhattan-Beach/3216-The-Strand-90266/home/6710053" style="color:#58a6ff;text-decoration:none">3216 The Strand</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Manhattan Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$24,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$21,000,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$3,995,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Malibu/6625-Zumirez-Dr-90265/home/6856153" style="color:#58a6ff;text-decoration:none">6625 Zumirez Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Malibu</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$28,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$26,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$2,000,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/201-S-Bristol-Ave-90049/home/6839030" style="color:#58a6ff;text-decoration:none">201 S Bristol Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$16,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$15,250,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$1,745,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Pacific-Palisades/1580-Sorrento-Dr-90272/home/6847205" style="color:#58a6ff;text-decoration:none">1580 Sorrento Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Pacific Palisades</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$21,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$19,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$1,505,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/11730-Stonehenge-Ln-90077/home/6831124" style="color:#58a6ff;text-decoration:none">11730 Stonehenge Ln</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$11,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$10,495,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$1,500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/13241-Riviera-Ranch-Rd-90049/home/58301772" style="color:#58a6ff;text-decoration:none">13241 Riviera Ranch Rd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$10,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$9,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$1,000,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Hidden-Hills/6021-John-Muir-Rd-91302/home/3291587" style="color:#58a6ff;text-decoration:none">6021 John Muir Rd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Hidden Hills</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$11,750,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$10,750,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$1,000,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/2666-Aberdeen-Ave-90027/home/7133151" style="color:#58a6ff;text-decoration:none">2666 Aberdeen Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$8,195,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,495,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$700,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/121-S-Rockingham-Ave-90049/home/6838979" style="color:#58a6ff;text-decoration:none">121 S Rockingham Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$11,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$10,850,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$650,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Santa-Clarita/10262-Sierra-Hwy-91390/home/6364448" style="color:#58a6ff;text-decoration:none">10262 Sierra Hwy</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Santa Clarita</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,700,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,099,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$601,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Venice/3011-Ocean-Front-Walk-90291/home/6734260" style="color:#58a6ff;text-decoration:none">3011 Ocean Front Walk</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Venice</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$10,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$9,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$505,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Rancho-Palos-Verdes/64-Laurel-Dr-90275/home/177421053" style="color:#58a6ff;text-decoration:none">64 Laurel Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Rancho Palos Verdes</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$8,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$7,999,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$501,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Calabasas/736-Crater-Oak-Dr-91302/home/6854101" style="color:#58a6ff;text-decoration:none">736 Crater Oak Dr</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Calabasas</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,600,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$2,100,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/521-S-Bentley-Ave-90049/home/6828982" style="color:#58a6ff;text-decoration:none">521 S Bentley</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,649,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,149,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/2495-S-Barrington-Ave-90064/home/6754500" style="color:#58a6ff;text-decoration:none">2495 S Barrington</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,495,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Pasadena/954-Hillside-Ter-91105/home/7187442" style="color:#58a6ff;text-decoration:none">954 Hillside Ter</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Pasadena</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$5,995,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$5,495,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Venice/29-27th-Ave-90291/home/6734171" style="color:#58a6ff;text-decoration:none">29 27th Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Venice</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$10,000,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$9,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$500,000</span></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/3036-Castle-St-90039/home/7062115" style="color:#58a6ff;text-decoration:none">3036 Castle St</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,998,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,500,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><span style="color:#ffa600">-$498,000</span></td></tr>
</table>
<h3 style="font-size:13px;text-transform:uppercase;color:#8b949e;margin:20px 0 6px">↩️ Fell Out of Escrow (back on market)</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tr style="color:#8b949e;text-align:left"><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Address</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">City</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Type</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Price</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Bd/Ba</th></tr>
- <tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Sherman-Oaks/3499-Beverly-Glen-Blvd-91423/home/4872436" style="color:#58a6ff;text-decoration:none">3499 Beverly Glen Blvd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Sherman Oaks</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$3,950,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/5</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Woodland-Hills/23036-Victory-Blvd-91367/home/3218174" style="color:#58a6ff;text-decoration:none">23036 Victory Blvd</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Woodland Hills</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,189,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">6/4</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Pasadena/257-Bella-Vista-Ave-91107/home/7218645" style="color:#58a6ff;text-decoration:none">257 Bella Vis</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Pasadena</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$1,000,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">2/1</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Burbank/1440-N-Evergreen-St-91505/home/5317253" style="color:#58a6ff;text-decoration:none">1440 N Evergreen</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Burbank</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$949,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">3/1</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Los-Angeles/441-N-Commonwealth-Ave-90004/home/7110987" style="color:#58a6ff;text-decoration:none">441 N Commonwealth Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$850,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">1/1</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Panorama-City/9591-N-Corbina-Ln-91402/home/182337402" style="color:#58a6ff;text-decoration:none">9591 N Corbina Ln</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Panorama City</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$819,900</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/3</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Lancaster/42301-Woodstone-Ln-93536/home/6358910" style="color:#58a6ff;text-decoration:none">42301 Woodstone</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Lancaster</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$750,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/3</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Lancaster/48038-93rd-St-W-93536/home/6389871" style="color:#58a6ff;text-decoration:none">48038 W 93rd St</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Lancaster</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$519,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/2</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Lancaster/43600-Santa-Rosa-Cir-93535/home/6339162" style="color:#58a6ff;text-decoration:none">43600 Santa Rosa Cir</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Lancaster</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$425,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/2</td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Lancaster/45339-Raysack-Ave-93535/home/6353294" style="color:#58a6ff;text-decoration:none">45339 Raysack Ave</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Lancaster</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">SFR</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$280,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">4/2</td></tr>
+ <tr><td style="padding:6px 8px;border-bottom:1px solid #20262f"><a href="https://www.redfin.com/CA/Canyon-Country/17955-River-Cir-91387/unit-4/home/6033143" style="color:#58a6ff;text-decoration:none">17955 River Cir #4</a></td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Canyon Country</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">Condo</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">$515,000</td><td style="padding:6px 8px;border-bottom:1px solid #20262f">3/3</td></tr>
</table>
<h3 style="font-size:13px;text-transform:uppercase;color:#8b949e;margin:20px 0 6px">🤝 In Escrow by City</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tr style="color:#8b949e;text-align:left"><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">City</th><th style="padding:6px 8px;border-bottom:1px solid #2a313c;font-size:11px;text-transform:uppercase">Pending / Contingent</th></tr>
- <tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>1087</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Lancaster</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>365</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Long Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>301</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Palmdale</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>301</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Torrance</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>192</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Santa Clarita</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>183</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Brentwood</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>157</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Whittier</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>156</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Pasadena</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>145</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Woodland Hills</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>127</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Van Nuys</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>109</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Redondo Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>108</b></td></tr>
+ <tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Los Angeles</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>1103</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Lancaster</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>366</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Palmdale</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>307</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Long Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>306</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Torrance</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>192</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Santa Clarita</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>189</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Whittier</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>160</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Brentwood</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>158</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Pasadena</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>145</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Woodland Hills</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>127</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Van Nuys</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>110</b></td></tr><tr><td style="padding:6px 8px;border-bottom:1px solid #20262f">Redondo Beach</td><td style="padding:6px 8px;border-bottom:1px solid #20262f"><b>109</b></td></tr>
</table>
<div style="color:#8b949e;font-size:11px;margin-top:22px;border-top:1px solid #2a313c;padding-top:10px">
Fell-Out accrues as listings transition Pending→Active across sweeps. Just-Sold cross-references recorded sales; escrow lag means more sales surface over the following weeks.</div>
diff --git a/public/admin.html b/public/admin.html
index a43d7c0..efae571 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -49,6 +49,7 @@ tr:hover td{background:#12171f}
.nlist .n .id{color:var(--blue);font-size:11px}
.nlist .n .tx{white-space:pre-wrap;margin-top:4px;font-size:12px}
</style>
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -220,5 +221,6 @@ $('#gp').addEventListener('keydown',e=>{if(e.key==='Enter')$('#gBtn').click();})
boot();
</script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/broker-grid.html b/public/broker-grid.html
index 9afc7a1..320a79d 100644
--- a/public/broker-grid.html
+++ b/public/broker-grid.html
@@ -92,6 +92,7 @@
.rail .rsec.collapsed > h4::after { transform:rotate(-90deg); }
.rail .rsec.collapsed > *:not(h4) { display:none !important; }
</style>
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -482,5 +483,6 @@ fetch('/api/brokers/all').then(r=>r.json()).then(d=>{DATA=d.brokers||[];autoCols
<script src="/global-search.js" defer></script>
<script src="/sort-bar.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/condos.html b/public/condos.html
index 800b837..beb8fa5 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -132,6 +132,7 @@
#modal .litem a.dl,#modal .maddr a.dl{color:var(--blue);text-decoration:none;} #modal .litem a.dl:hover,#modal .maddr a.dl:hover{text-decoration:underline;}
#modal .src{font-size:12px;color:var(--mut);margin-top:12px;border-top:1px solid var(--line);padding-top:10px;}
</style>
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -816,5 +817,6 @@ boot();
<script src="/global-search.js" defer></script>
<script src="/sort-bar.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/contractors.html b/public/contractors.html
index 7bdf20f..828d137 100644
--- a/public/contractors.html
+++ b/public/contractors.html
@@ -69,7 +69,8 @@ input.dealf{background:var(--card);border:1px solid var(--line);color:var(--ink)
.finance{background:rgba(88,166,255,.08);border:1px solid rgba(88,166,255,.25);border-radius:8px;padding:2px 7px;font-size:10.5px;color:var(--blue);cursor:pointer;text-decoration:none}
.finance:hover{border-color:var(--blue)}
@media(max-width:760px){.rail{display:none}}
-</style></head><body>
+</style> <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+</head><body>
<header>
<h1><b>Contractors for this deal</b> · CSLB licensed</h1>
<span class="badge">🔨 vendor & borrower graph</span>
@@ -337,4 +338,5 @@ $('#body').addEventListener('click',e=>{
})();
</script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body></html>
diff --git a/public/crcp-grid.css b/public/crcp-grid.css
index 9642cd2..9071c95 100644
--- a/public/crcp-grid.css
+++ b/public/crcp-grid.css
@@ -25,6 +25,22 @@
.cg-mini button{background:var(--cg-card,#161b22);color:var(--cg-mut,#8b949e);border:1px solid var(--cg-line,#2a313c);border-radius:7px;padding:4px 9px;font-size:11px;cursor:pointer;}
.cg-mini button:hover{border-color:var(--cg-acc,#58a6ff);color:var(--cg-ink,#e6edf3);}
.cg-top{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:13px 18px;border-bottom:1px solid var(--cg-line,#2a313c);position:sticky;top:0;background:var(--cg-topbg,rgba(14,17,22,.94));backdrop-filter:blur(6px);z-index:6;}
+/* active-filter summary bar (removable chips + clear all) */
+.cg-active{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:12px 18px 0;}
+.cg-active:empty{display:none;}
+.cg-af-lbl{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--cg-mut,#8b949e);}
+.cg-af{display:inline-flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;
+ background:var(--cg-accbg,rgba(200,162,75,.12));color:var(--cg-ink,#e6edf3);
+ border:1px solid var(--cg-acc,#58a6ff);border-radius:14px;padding:3px 10px;white-space:nowrap;}
+.cg-af b{color:var(--cg-acc,#58a6ff);font-weight:700;}
+.cg-af:hover{background:var(--cg-acc,#58a6ff);color:var(--cg-railbg,#0b0e13);}
+.cg-af:hover b{color:var(--cg-railbg,#0b0e13);}
+.cg-af-clear{font-size:12px;background:none;border:0;cursor:pointer;padding:3px 6px;
+ color:var(--cg-mut,#8b949e);text-decoration:underline;}
+.cg-af-clear:hover{color:var(--cg-acc,#58a6ff);}
+.cg-af-copy{font-size:12px;background:none;border:1px solid var(--cg-line,#2a313c);cursor:pointer;padding:3px 10px;
+ border-radius:8px;color:var(--cg-mut,#8b949e);}
+.cg-af-copy:hover{color:var(--cg-acc,#58a6ff);border-color:var(--cg-acc,#58a6ff);}
.cg-q{min-width:240px;background:var(--cg-card,#161b22);color:var(--cg-ink,#e6edf3);border:1px solid var(--cg-line,#2a313c);border-radius:8px;padding:8px 11px;font-size:13px;}
.cg-seg{display:inline-flex;border:1px solid var(--cg-line,#2a313c);border-radius:8px;overflow:hidden;}
.cg-seg button{background:var(--cg-card,#161b22);color:var(--cg-mut,#8b949e);border:0;border-right:1px solid var(--cg-line,#2a313c);padding:7px 12px;font-size:12px;cursor:pointer;}
@@ -42,7 +58,13 @@
.cg-sub{font-size:12px;color:var(--cg-mut,#8b949e);}
.cg-r{display:flex;justify-content:space-between;font-size:12px;gap:10px;}
.cg-r .cg-k{color:var(--cg-mut,#8b949e);}
-.cg-empty{padding:50px;text-align:center;color:var(--cg-mut,#8b949e);}
+.cg-empty{padding:64px 24px;text-align:center;color:var(--cg-mut,#8b949e);display:flex;flex-direction:column;align-items:center;gap:8px;}
+.cg-empty-ic{font-size:40px;line-height:1;opacity:.5;}
+.cg-empty-t{font-size:18px;font-weight:700;color:var(--cg-ink,#e6edf3);}
+.cg-empty-s{font-size:13px;}
+.cg-empty-btn{margin-top:10px;text-decoration:none;background:var(--cg-acc,#58a6ff);color:var(--cg-railbg,#0b0e13);
+ border:0;border-radius:8px;padding:9px 18px;font-size:13px;font-weight:700;cursor:pointer;}
+.cg-empty-btn:hover{filter:brightness(1.08);}
.cg-listwrap{overflow-x:auto;margin:18px;border:1px solid var(--cg-line,#2a313c);border-radius:12px;}
.cg-tbl{width:100%;border-collapse:collapse;font-size:12.5px;white-space:nowrap;}
.cg-tbl th,.cg-tbl td{text-align:right;padding:9px 13px;border-top:1px solid var(--cg-line,#2a313c);}
@@ -53,4 +75,36 @@
.cg-tbl tr.red td{background:rgba(248,81,73,.07);}
.cg-la{font-weight:600;color:var(--cg-ink,#e6edf3);}
.cg-lc{color:var(--cg-mut,#8b949e);font-size:11px;}
-@media(max-width:820px){.cg-rail{display:none;}}
+/* mobile filter access: rail becomes an off-canvas drawer opened by the .cg-filt-btn */
+.cg-filt-btn{display:none;align-items:center;gap:6px;background:var(--cg-card,#161b22);color:var(--cg-ink,#e6edf3);
+ border:1px solid var(--cg-acc,#58a6ff);border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer;white-space:nowrap;}
+.cg-rail-backdrop{position:fixed;inset:0;z-index:1250;background:rgba(0,0,0,.5);opacity:0;visibility:hidden;transition:opacity .2s;}
+.cg-rail-backdrop.open{opacity:1;visibility:visible;}
+@media(max-width:820px){
+ .cg-filt-btn{display:inline-flex;}
+ .cg-top{padding-left:66px;} /* clear the fixed upper-left hamburger */
+ .cg-rail{position:fixed;top:0;left:0;height:100%;width:290px;max-width:86vw;z-index:1300;
+ transform:translateX(-100%);transition:transform .22s ease;box-shadow:2px 0 22px rgba(0,0,0,.55);}
+ .cg-rail.open{transform:translateX(0);}
+}
+/* --- USRE adaptations (nationalrealestate) --- */
+.cg-sort{background:var(--cg-card,#161b22);color:var(--cg-ink,#e6edf3);border:1px solid var(--cg-line,#2a313c);border-radius:8px;padding:8px 10px;font-size:13px;}
+.cg-sortth{cursor:pointer;user-select:none;}
+.cg-sortth:hover{color:var(--cg-ink,#e6edf3);}
+/* density slider drives list-view row padding + font size via --cg-dens (1..12) */
+.cg-tbl{font-size:calc(15.5px - var(--cg-dens,3)*0.45px);font-variant-numeric:tabular-nums;}
+.cg-tbl th,.cg-tbl td{padding:calc(14px - var(--cg-dens,3)*1px) calc(16px - var(--cg-dens,3)*0.8px);}
+.cg-tbl tr[data-ri]{cursor:pointer;}
+.cg-tbl tr.dim td,.cg-card.dim{opacity:.45;}
+/* --- compact LIST view (one scannable row per record; density-aware) --- */
+.cg-rows{display:flex;flex-direction:column;gap:calc(10px - var(--cg-dens,3)*0.9px);padding:16px;}
+.cg-lrow{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;background:var(--cg-card,#161b22);border:1px solid var(--cg-line,#2a313c);border-radius:10px;padding:calc(13px - var(--cg-dens,3)*0.8px) 14px;cursor:pointer;}
+.cg-lrow:hover{border-color:var(--cg-acc,#58a6ff);}
+.cg-lrow.red{border-color:var(--cg-red,#f85149);}
+.cg-lrow.dim{opacity:.45;}
+.cg-lmain{flex:0 0 clamp(180px,22vw,280px);min-width:180px;display:flex;flex-direction:column;gap:2px;}
+.cg-lmain .cg-la{font-weight:600;color:var(--cg-ink,#e6edf3);font-size:calc(15px - var(--cg-dens,3)*0.25px);}
+.cg-lsub{color:var(--cg-mut,#8b949e);font-size:11.5px;}
+.cg-lmeta{flex:1 1 auto;min-width:0;display:flex;flex-wrap:wrap;gap:5px 18px;align-items:baseline;font-size:calc(13.5px - var(--cg-dens,3)*0.4px);font-variant-numeric:tabular-nums;color:var(--cg-ink,#e6edf3);}
+.cg-lm{white-space:nowrap;}
+.cg-lmk{color:var(--cg-mut,#8b949e);font-size:10px;text-transform:uppercase;letter-spacing:.4px;margin-right:4px;}
diff --git a/public/crcp-grid.css.pre-3view.bak b/public/crcp-grid.css.pre-3view.bak
new file mode 100644
index 0000000..9642cd2
--- /dev/null
+++ b/public/crcp-grid.css.pre-3view.bak
@@ -0,0 +1,56 @@
+/* crcp-grid.css — styles for the reusable CrcpGrid engine. Namespaced .cg-* so it never
+ * collides with a host page. Colors read the host's CSS vars with safe fallbacks, so the
+ * engine inherits each page's theme (condos dark, fha-loans glass, sales gold) automatically. */
+.cg-shell{display:flex;align-items:flex-start;}
+.cg-rail{flex:0 0 250px;align-self:stretch;border-right:1px solid var(--cg-line,#2a313c);background:var(--cg-railbg,rgba(0,0,0,.18));position:sticky;top:0;max-height:100vh;overflow-y:auto;padding-bottom:30px;}
+.cg-main{flex:1 1 auto;min-width:0;--cg-cols:3;}
+.cg-sec{padding:12px 15px;border-bottom:1px solid var(--cg-line,#2a313c);}
+.cg-sec h4{margin:0 0 9px;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--cg-mut,#8b949e);cursor:pointer;display:flex;align-items:center;justify-content:space-between;user-select:none;}
+.cg-sec h4::after{content:'▾';font-size:11px;margin-left:8px;transition:transform .15s;}
+.cg-sec.collapsed h4::after{transform:rotate(-90deg);}
+.cg-sec.collapsed > *:not(h4){display:none !important;}
+.cg-chips{display:flex;flex-wrap:wrap;gap:6px;}
+.cg-chip{background:var(--cg-card,#161b22);color:var(--cg-mut,#8b949e);border:1px solid var(--cg-line,#2a313c);border-radius:16px;padding:4px 10px;font-size:12px;cursor:pointer;user-select:none;}
+.cg-chip:hover{color:var(--cg-ink,#e6edf3);}
+.cg-chip.active{border-color:var(--cg-acc,#58a6ff);color:var(--cg-acc,#58a6ff);background:var(--cg-accbg,rgba(88,166,255,.10));}
+.cg-ct{color:var(--cg-mut,#8b949e);font-size:10px;margin-left:4px;}
+.cg-row2{display:flex;gap:8px;}
+.cg-fld{flex:1;}
+.cg-fld label{display:block;font-size:10px;color:var(--cg-mut,#8b949e);margin-bottom:3px;}
+.cg-fld input{width:100%;background:var(--cg-card,#161b22);color:var(--cg-ink,#e6edf3);border:1px solid var(--cg-line,#2a313c);border-radius:8px;padding:7px 9px;font-size:13px;}
+.cg-ftogs{display:flex;flex-direction:column;gap:1px;max-height:300px;overflow-y:auto;}
+.cg-ftog{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--cg-ink,#e6edf3);cursor:pointer;padding:3px 4px;border-radius:6px;}
+.cg-ftog:hover{background:var(--cg-accbg,rgba(88,166,255,.06));}
+.cg-mini{margin-top:8px;display:flex;gap:5px;}
+.cg-mini button{background:var(--cg-card,#161b22);color:var(--cg-mut,#8b949e);border:1px solid var(--cg-line,#2a313c);border-radius:7px;padding:4px 9px;font-size:11px;cursor:pointer;}
+.cg-mini button:hover{border-color:var(--cg-acc,#58a6ff);color:var(--cg-ink,#e6edf3);}
+.cg-top{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:13px 18px;border-bottom:1px solid var(--cg-line,#2a313c);position:sticky;top:0;background:var(--cg-topbg,rgba(14,17,22,.94));backdrop-filter:blur(6px);z-index:6;}
+.cg-q{min-width:240px;background:var(--cg-card,#161b22);color:var(--cg-ink,#e6edf3);border:1px solid var(--cg-line,#2a313c);border-radius:8px;padding:8px 11px;font-size:13px;}
+.cg-seg{display:inline-flex;border:1px solid var(--cg-line,#2a313c);border-radius:8px;overflow:hidden;}
+.cg-seg button{background:var(--cg-card,#161b22);color:var(--cg-mut,#8b949e);border:0;border-right:1px solid var(--cg-line,#2a313c);padding:7px 12px;font-size:12px;cursor:pointer;}
+.cg-seg button:last-child{border-right:0;}
+.cg-seg button.active{background:var(--cg-acc,#1f6feb);color:#fff;}
+.cg-dens{font-size:12px;color:var(--cg-mut,#8b949e);display:flex;align-items:center;gap:7px;}
+.cg-dens input{accent-color:var(--cg-acc,#58a6ff);}
+.cg-count{margin-left:auto;font-size:12px;color:var(--cg-mut,#8b949e);}
+.cg-count b{color:var(--cg-ink,#e6edf3);}
+.cg-grid{display:grid;gap:16px;padding:18px;grid-template-columns:repeat(var(--cg-cols,3),minmax(0,1fr));}
+.cg-grid.cg-listhost{display:block;}
+.cg-card{background:var(--cg-card,#161b22);border:1px solid var(--cg-line,#2a313c);border-radius:12px;padding:15px;display:flex;flex-direction:column;gap:5px;}
+.cg-card.red{border-color:var(--cg-red,#f85149);}
+.cg-name{font-weight:600;font-size:14px;color:var(--cg-ink,#e6edf3);}
+.cg-sub{font-size:12px;color:var(--cg-mut,#8b949e);}
+.cg-r{display:flex;justify-content:space-between;font-size:12px;gap:10px;}
+.cg-r .cg-k{color:var(--cg-mut,#8b949e);}
+.cg-empty{padding:50px;text-align:center;color:var(--cg-mut,#8b949e);}
+.cg-listwrap{overflow-x:auto;margin:18px;border:1px solid var(--cg-line,#2a313c);border-radius:12px;}
+.cg-tbl{width:100%;border-collapse:collapse;font-size:12.5px;white-space:nowrap;}
+.cg-tbl th,.cg-tbl td{text-align:right;padding:9px 13px;border-top:1px solid var(--cg-line,#2a313c);}
+.cg-tbl th{position:sticky;top:0;background:var(--cg-railbg,#0b0e13);color:var(--cg-mut,#8b949e);font-size:10px;text-transform:uppercase;letter-spacing:.5px;border-top:0;}
+.cg-tbl td.cg-stick,.cg-tbl th.cg-stick{text-align:left;position:sticky;left:0;background:var(--cg-card,#161b22);}
+.cg-tbl th.cg-stick{background:var(--cg-railbg,#0b0e13);}
+.cg-tbl tr:hover td{background:var(--cg-accbg,rgba(88,166,255,.06));}
+.cg-tbl tr.red td{background:rgba(248,81,73,.07);}
+.cg-la{font-weight:600;color:var(--cg-ink,#e6edf3);}
+.cg-lc{color:var(--cg-mut,#8b949e);font-size:11px;}
+@media(max-width:820px){.cg-rail{display:none;}}
diff --git a/public/crcp-grid.js b/public/crcp-grid.js
index 1bfc75d..574bd9a 100644
--- a/public/crcp-grid.js
+++ b/public/crcp-grid.js
@@ -2,7 +2,10 @@
* Config-driven, theme-agnostic (inherits the host page's CSS vars via cg-grid.css).
* One page = one CrcpGrid.mount(opts) call. Mirrors the proven condos.html engine so
* every list page gets the SAME behavior: left-rail facets, per-field show/hide toggles,
- * grid/list view toggle, density slider, live search, localStorage persistence.
+ * grid / list / table view toggle, density slider, live search, localStorage persistence.
+ * Three view modes: 'grid' (image/data cards), 'list' (one compact scannable row per
+ * record), 'table' (dense all-columns spreadsheet with click-to-sort headers). EVERY
+ * non-badge field is sortable via the topbar Sort select (both directions).
*
* opts = {
* data: [row,...], // all rows (client-side faceting)
@@ -16,7 +19,12 @@
* storageKey:'fha', // localStorage namespace
* cols: 3, // default density
* onRow: row => ({cls}), // optional per-row class hook (e.g. red flag)
+ * onRowClick:row => {}, // optional whole-row click handler (list + cards)
+ * sort: {k:'score',dir:-1}, // default sort (USRE adaptation; persisted, select + th-click)
* }
+ * USRE adaptations (nationalrealestate): sort <select> in the topbar + clickable list-view
+ * column headers (both persisted), density slider also drives list row padding/font via
+ * --cg-dens, onRowClick hook. Engine otherwise identical to the CRCP original.
*/
(function (global) {
const esc = s => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
@@ -60,6 +68,25 @@
});
}
+ // ---- sort state (persisted) ----
+ let SORT = O.sort || null; // {k, dir:1|-1}; k='' = source order
+ try { const s = JSON.parse(localStorage.getItem(KEY + 'Sort') || 'null'); if (s && typeof s.k === 'string') SORT = s; } catch (e) {}
+ const saveSort = () => { try { localStorage.setItem(KEY + 'Sort', JSON.stringify(SORT)); } catch (e) {} };
+ function sortRows(rows) {
+ if (!SORT || !SORT.k) return rows;
+ const f = fields.find(x => x.k === SORT.k);
+ if (!f) return rows;
+ const dir = SORT.dir || 1;
+ return [...rows].sort((a, b) => {
+ const va = fval(a, f), vb = fval(b, f);
+ const na = va == null || va === '', nb = vb == null || vb === '';
+ if (na && nb) return 0; if (na) return 1; if (nb) return -1; // nulls last either dir
+ if (typeof va === 'number' || typeof vb === 'number' || f.money || f.num || f.pct)
+ return ((+va) - (+vb)) * dir;
+ return String(va).localeCompare(String(vb)) * dir;
+ });
+ }
+
// ---- filter state ----
const F = { q: '', chips: {}, ranges: {} };
facets.forEach(fc => { if (fc.type === 'chip') F.chips[fc.k] = new Set(); else F.ranges[fc.k] = { min: null, max: null }; });
@@ -76,6 +103,35 @@
return true;
}
+ // ---- URL sync: filters/search/sort <-> query string (shareable views) ----
+ function readFromURL() {
+ if (O.urlSync === false) return;
+ const sp = new URLSearchParams(location.search);
+ if (![...sp.keys()].length) return;
+ facets.forEach(fc => {
+ if (fc.type === 'chip') { const v = sp.get(fc.k); if (v) v.split(',').forEach(x => F.chips[fc.k].add(x)); }
+ else {
+ const mn = sp.get(fc.k + '_min'), mx = sp.get(fc.k + '_max');
+ if (mn != null && mn !== '') F.ranges[fc.k].min = +mn;
+ if (mx != null && mx !== '') F.ranges[fc.k].max = +mx;
+ }
+ });
+ const q = sp.get('q'); if (q) F.q = q.toLowerCase();
+ const so = sp.get('sort'); if (so) { const [k, d] = so.split(':'); if (k) SORT = { k, dir: d === 'asc' ? 1 : -1 }; }
+ }
+ function syncURL() {
+ if (O.urlSync === false) return;
+ const sp = new URLSearchParams();
+ facets.forEach(fc => {
+ if (fc.type === 'chip') { const set = F.chips[fc.k]; if (set && set.size) sp.set(fc.k, [...set].join(',')); }
+ else { const r = F.ranges[fc.k]; if (r) { if (r.min != null) sp.set(fc.k + '_min', r.min); if (r.max != null) sp.set(fc.k + '_max', r.max); } }
+ });
+ if (F.q) sp.set('q', F.q);
+ if (SORT && SORT.k) sp.set('sort', SORT.k + ':' + (SORT.dir < 0 ? 'desc' : 'asc'));
+ const qs = sp.toString();
+ try { history.replaceState(null, '', location.pathname + (qs ? '?' + qs : '') + location.hash); } catch (e) {}
+ }
+
// ---- rail ----
function buildRail() {
const rail = $(O.rail);
@@ -86,12 +142,20 @@
data.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="cg-sec collapsed"><h4>${esc(fc.l)}</h4><div class="cg-chips" data-facet="${esc(fc.k)}">` +
- ent.map(([k, c]) => `<span class="cg-chip" data-v="${esc(k)}">${esc(k)}<span class="cg-ct">${c}</span></span>`).join('') + `</div></div>`;
+ // render FROM F: mark active chips + auto-expand a section that has a live filter
+ const set = F.chips[fc.k];
+ const secCls = (set && set.size) ? 'cg-sec' : 'cg-sec collapsed';
+ html += `<div class="${secCls}"><h4>${esc(fc.l)}</h4><div class="cg-chips" data-facet="${esc(fc.k)}">` +
+ ent.map(([k, c]) => `<span class="cg-chip${set && set.has(String(k)) ? ' active' : ''}" data-v="${esc(k)}">${esc(k)}<span class="cg-ct">${c}</span></span>`).join('') + `</div></div>`;
} else {
- html += `<div class="cg-sec collapsed"><h4>${esc(fc.l)}</h4><div class="cg-row2" data-facet="${esc(fc.k)}">` +
- `<div class="cg-fld"><label>Min</label><input type="number" class="cg-min" placeholder="any"></div>` +
- `<div class="cg-fld"><label>Max</label><input type="number" class="cg-max" placeholder="any"></div></div></div>`;
+ const rg = F.ranges[fc.k] || {};
+ const active = rg.min != null || rg.max != null;
+ const secCls = active ? 'cg-sec' : 'cg-sec collapsed';
+ const mn = rg.min != null ? ` value="${esc(rg.min)}"` : '';
+ const mx = rg.max != null ? ` value="${esc(rg.max)}"` : '';
+ html += `<div class="${secCls}"><h4>${esc(fc.l)}</h4><div class="cg-row2" data-facet="${esc(fc.k)}">` +
+ `<div class="cg-fld"><label>Min</label><input type="number" class="cg-min" placeholder="any"${mn}></div>` +
+ `<div class="cg-fld"><label>Max</label><input type="number" class="cg-max" placeholder="any"${mx}></div></div></div>`;
}
});
html += `<div class="cg-sec"><h4>Fields — show / hide</h4><div class="cg-ftogs">` +
@@ -127,64 +191,228 @@
// ---- renderers ----
function cards(rows) {
const vis = fields.filter(f => VIS[f.k] && !f.badge && f.k !== 'title');
- return rows.map(r => {
+ return rows.map((r, i) => {
const t = O.title ? O.title(r) : { name: r.name || '', sub: '' };
const rc = O.onRow ? (O.onRow(r).cls || '') : '';
const nm = t.href ? `<a href="${esc(t.href)}" target="_blank" rel="noopener noreferrer" style="color:inherit">${esc(t.name)}</a>` : esc(t.name);
- return `<div class="cg-card ${rc}">` +
+ return `<div class="cg-card ${rc}" data-ri="${i}">` +
`<div class="cg-name">${nm}</div>${t.sub ? `<div class="cg-sub">${esc(t.sub)}</div>` : ''}` +
(O.badgeHtml ? O.badgeHtml(r) : '') +
vis.map(f => `<div class="cg-r"><span class="cg-k">${esc(f.l)}</span><span>${fmt(fval(r, f), f)}</span></div>`).join('') +
`</div>`;
}).join('');
}
- function list(rows) {
+ function tableView(rows) {
const cols = fields.filter(f => f.col && VIS[f.k] && !f.badge);
- const head = `<th class="cg-stick">${esc((O.titleCol || 'Name'))}</th>` + cols.map(f => `<th>${esc(f.l)}</th>`).join('');
- const body = rows.map(r => {
+ const arrow = k => (SORT && SORT.k === k) ? (SORT.dir < 0 ? ' ▼' : ' ▲') : '';
+ const titleK = O.titleSortK || '';
+ const head = `<th class="cg-stick cg-sortth" data-k="${esc(titleK)}">${esc((O.titleCol || 'Name'))}${arrow(titleK)}</th>` +
+ cols.map(f => `<th class="cg-sortth" data-k="${esc(f.k)}">${esc(f.l)}${arrow(f.k)}</th>`).join('');
+ const body = rows.map((r, i) => {
const t = O.title ? O.title(r) : { name: '', sub: '' };
const rc = O.onRow ? (O.onRow(r).cls || '') : '';
const nm = t.href ? `<a href="${esc(t.href)}" target="_blank" rel="noopener noreferrer" style="color:inherit">${esc(t.name)}</a>` : esc(t.name);
- return `<tr class="${rc}"><td class="cg-stick"><span class="cg-la">${nm}</span>${t.sub ? `<div class="cg-lc">${esc(t.sub)}</div>` : ''}</td>` +
+ return `<tr class="${rc}" data-ri="${i}"><td class="cg-stick"><span class="cg-la">${nm}</span>${t.sub ? `<div class="cg-lc">${esc(t.sub)}</div>` : ''}</td>` +
cols.map(f => `<td>${fmt(fval(r, f), f)}</td>`).join('') + `</tr>`;
}).join('');
return `<div class="cg-listwrap"><table class="cg-tbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
}
+ // compact single-row-per-record list: title on the left, every visible field
+ // flowing as inline key/value pairs on the right. Denser than cards, scannable,
+ // no fixed columns. Sorted via the topbar Sort select (all fields).
+ function listCompact(rows) {
+ const vis = fields.filter(f => VIS[f.k] && !f.badge && f.k !== 'title');
+ return `<div class="cg-rows">` + rows.map((r, i) => {
+ const t = O.title ? O.title(r) : { name: r.name || '', sub: '' };
+ const rc = O.onRow ? (O.onRow(r).cls || '') : '';
+ const nm = t.href ? `<a href="${esc(t.href)}" target="_blank" rel="noopener noreferrer" style="color:inherit">${esc(t.name)}</a>` : esc(t.name);
+ return `<div class="cg-lrow ${rc}" data-ri="${i}">` +
+ `<div class="cg-lmain"><span class="cg-la">${nm}</span>${t.sub ? `<span class="cg-lsub">${esc(t.sub)}</span>` : ''}${O.badgeHtml ? O.badgeHtml(r) : ''}</div>` +
+ `<div class="cg-lmeta">` +
+ vis.map(f => `<span class="cg-lm"><span class="cg-lmk">${esc(f.l)}</span>${fmt(fval(r, f), f)}</span>`).join('') +
+ `</div></div>`;
+ }).join('') + `</div>`;
+ }
- let VIEW = localStorage.getItem(KEY + 'View') || O.view || 'grid';
+ // ---- active-filter summary bar (removable chips + clear all) ----
+ function activeFilterHTML() {
+ const parts = [];
+ facets.forEach(fc => {
+ if (fc.type === 'chip') {
+ const set = F.chips[fc.k];
+ if (set) set.forEach(v => parts.push(
+ `<span class="cg-af" data-fk="${esc(fc.k)}" data-aft="chip" data-v="${esc(v)}">${esc(fc.l)}: ${esc(v)} <b>✕</b></span>`));
+ } else {
+ const r = F.ranges[fc.k];
+ if (r && (r.min != null || r.max != null)) {
+ const txt = (r.min != null && r.max != null) ? esc(r.min) + '–' + esc(r.max)
+ : (r.min != null) ? '≥ ' + esc(r.min) : '≤ ' + esc(r.max);
+ parts.push(`<span class="cg-af" data-fk="${esc(fc.k)}" data-aft="range">${esc(fc.l)}: ${txt} <b>✕</b></span>`);
+ }
+ }
+ });
+ if (F.q) parts.push(`<span class="cg-af" data-aft="q">“${esc(F.q)}” <b>✕</b></span>`);
+ if (!parts.length) return '';
+ return `<span class="cg-af-lbl">Filters</span>` + parts.join('') +
+ `<button class="cg-af-clear" data-clearall="1">Clear all</button>` +
+ `<button class="cg-af-copy" data-copy="1" title="Copy a shareable link to this filtered view">🔗 Copy link</button>`;
+ }
+ function clearFilter(el) {
+ if (el.dataset.clearall != null) {
+ facets.forEach(fc => { if (fc.type === 'chip') F.chips[fc.k].clear(); else F.ranges[fc.k] = { min: null, max: null }; });
+ F.q = ''; const q = $(O.mount + ' .cg-q'); if (q) q.value = '';
+ } else if (el.dataset.aft === 'q') {
+ F.q = ''; const q = $(O.mount + ' .cg-q'); if (q) q.value = '';
+ } else if (el.dataset.aft === 'chip') {
+ F.chips[el.dataset.fk].delete(el.dataset.v);
+ } else if (el.dataset.aft === 'range') {
+ F.ranges[el.dataset.fk] = { min: null, max: null };
+ }
+ buildRail(); render(); // buildRail re-renders the rail from F so it stays in sync
+ }
+
+ let stored = null; try { stored = localStorage.getItem(KEY + 'View'); } catch (e) {}
+ // v2 migration: the old engine's 'list' WAS the dense table. The new engine renames
+ // that spreadsheet to 'table' and adds a distinct compact 'list'. Re-point a *stored*
+ // legacy 'list' to 'table' exactly once so returning users keep the view they had.
+ try {
+ if (stored === 'list' && !localStorage.getItem(KEY + 'ViewV2')) { stored = 'table'; localStorage.setItem(KEY + 'View', 'table'); }
+ localStorage.setItem(KEY + 'ViewV2', '1');
+ } catch (e) {}
+ let VIEW = stored || O.view || 'grid';
const CAP = O.cap || 1500; // DOM render cap for perf; count still reflects the full match set
+ let lastShown = [];
function render() {
- const rows = data.filter(pass);
+ const rows = sortRows(data.filter(pass));
const shown = rows.length > CAP ? rows.slice(0, CAP) : rows;
+ lastShown = shown;
const g = $(O.mount + ' .cg-grid');
- if (!rows.length) { g.className = 'cg-grid'; g.innerHTML = '<div class="cg-empty">no matches for these filters.</div>'; }
- else if (VIEW === 'list') { g.className = 'cg-grid cg-listhost'; g.innerHTML = list(shown); }
+ if (!rows.length) {
+ g.className = 'cg-grid';
+ g.innerHTML = '<div class="cg-empty"><div class="cg-empty-ic">⌕</div>' +
+ '<div class="cg-empty-t">No matches</div>' +
+ '<div class="cg-empty-s">No results match the current filters.</div>' +
+ '<button class="cg-af-clear cg-empty-btn" data-clearall="1">Clear all filters</button></div>';
+ }
+ else if (VIEW === 'table') { g.className = 'cg-grid cg-listhost'; g.innerHTML = tableView(shown); }
+ else if (VIEW === 'list') { g.className = 'cg-grid cg-listhost'; g.innerHTML = listCompact(shown); }
else { g.className = 'cg-grid'; g.innerHTML = cards(shown); }
const cnt = $(O.mount + ' .cg-count');
if (cnt) cnt.innerHTML = `<b>${rows.length.toLocaleString()}</b> of ${data.length.toLocaleString()}` + (rows.length > CAP ? ` <span style="opacity:.7">(first ${CAP.toLocaleString()} shown)</span>` : '');
+ const sel = $(O.mount + ' .cg-sort');
+ if (sel) sel.value = SORT && SORT.k ? SORT.k + ':' + (SORT.dir < 0 ? 'desc' : 'asc') : '';
+ const ab = $(O.mount + ' .cg-active');
+ if (ab) ab.innerHTML = activeFilterHTML();
+ // render() rebuilds the whole table via innerHTML, so the previous
+ // col-resize enhancement is gone. Re-enhance deterministically instead of
+ // waiting on the async MutationObserver (which races the first interaction).
+ if (window.ColResize && window.ColResize.refresh) window.ColResize.refresh();
+ syncURL();
}
// ---- topbar ----
function buildTop() {
const m = $(O.mount);
+ // ALL data points are sortable — not just the curated table columns.
+ const titleSK = O.titleSortK || '';
+ const sortable = fields.filter(f => !f.badge && f.k !== 'title' && f.k !== titleSK);
+ const titleOpts = titleSK
+ ? `<option value="${esc(titleSK)}:asc">${esc(O.titleCol || 'Name')} A→Z</option><option value="${esc(titleSK)}:desc">${esc(O.titleCol || 'Name')} Z→A</option>`
+ : '';
+ const sortOpts = `<option value="">Sort…</option>` + titleOpts + sortable.map(f =>
+ `<option value="${esc(f.k)}:desc">${esc(f.l)} ↓</option><option value="${esc(f.k)}:asc">${esc(f.l)} ↑</option>`).join('');
m.insertAdjacentHTML('afterbegin',
`<div class="cg-top">` +
+ `<button type="button" class="cg-filt-btn" aria-label="Filters">☰ Filters</button>` +
`<input type="text" class="cg-q" placeholder="search…">` +
- `<span class="cg-seg"><button data-view="grid" class="${VIEW === 'grid' ? 'active' : ''}">▦ Grid</button><button data-view="list" class="${VIEW === 'list' ? 'active' : ''}">☰ List</button></span>` +
+ `<select class="cg-sort" title="Sort">${sortOpts}</select>` +
+ `<span class="cg-seg"><button data-view="grid" class="${VIEW === 'grid' ? 'active' : ''}">▦ Grid</button><button data-view="list" class="${VIEW === 'list' ? 'active' : ''}">☰ List</button><button data-view="table" class="${VIEW === 'table' ? 'active' : ''}">▤ Table</button></span>` +
`<label class="cg-dens">Density <input type="range" class="cg-cols" min="1" max="12" step="1" value="${O.cols || 3}"></label>` +
- `<span class="cg-count"></span></div><div class="cg-grid"></div>`);
+ `<span class="cg-count"></span></div><div class="cg-active"></div><div class="cg-grid"></div>`);
const q = m.querySelector('.cg-q');
+ if (F.q) q.value = F.q; // reflect a search restored from the URL
q.addEventListener('input', () => { clearTimeout(q._t); q._t = setTimeout(() => { F.q = q.value.trim().toLowerCase(); render(); }, 160); });
+ m.querySelector('.cg-sort').addEventListener('change', e => {
+ const v = e.target.value;
+ if (!v) SORT = null;
+ else { const [k, d] = v.split(':'); SORT = { k, dir: d === 'desc' ? -1 : 1 }; }
+ saveSort(); render();
+ });
m.querySelector('.cg-seg').addEventListener('click', e => { const b = e.target.closest('button[data-view]'); if (!b) return; VIEW = b.dataset.view; localStorage.setItem(KEY + 'View', VIEW); m.querySelectorAll('.cg-seg button').forEach(x => x.classList.toggle('active', x.dataset.view === VIEW)); render(); });
const cols = m.querySelector('.cg-cols');
- const setCols = v => { m.style.setProperty('--cg-cols', v); localStorage.setItem(KEY + 'Cols', v); };
+ const setCols = v => { m.style.setProperty('--cg-cols', v); m.style.setProperty('--cg-dens', v); localStorage.setItem(KEY + 'Cols', v); };
cols.addEventListener('input', e => setCols(e.target.value));
const sc = localStorage.getItem(KEY + 'Cols'); if (sc) { cols.value = sc; }
setCols(cols.value);
+ // mobile: "Filters" button opens the existing rail as an off-canvas drawer
+ const filtBtn = m.querySelector('.cg-filt-btn');
+ const railEl = $(O.rail);
+ if (filtBtn && railEl) {
+ const bd = document.createElement('div'); bd.className = 'cg-rail-backdrop'; document.body.appendChild(bd);
+ const openRail = () => { railEl.classList.add('open'); bd.classList.add('open'); };
+ const closeRail = () => { railEl.classList.remove('open'); bd.classList.remove('open'); };
+ filtBtn.addEventListener('click', () => railEl.classList.contains('open') ? closeRail() : openRail());
+ bd.addEventListener('click', closeRail);
+ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeRail(); });
+ }
+ // th-click sorting + whole-row click (delegated; grid re-renders under us)
+ m.addEventListener('click', e => {
+ const cp = e.target.closest('.cg-af-copy');
+ if (cp) {
+ const done = () => { const t = cp.textContent; cp.textContent = '✓ Copied'; setTimeout(() => cp.textContent = t, 1200); };
+ if (navigator.clipboard) navigator.clipboard.writeText(location.href).then(done, done); else done();
+ return;
+ }
+ const af = e.target.closest('.cg-af, .cg-af-clear');
+ if (af) { clearFilter(af); return; }
+ // A click on the resize handle (or the tail of a resize drag) must NOT
+ // sort — otherwise grabbing the handle re-renders the whole table
+ // mid-gesture and the column never resizes. col-resize owns the handle.
+ if (e.target.closest('.cr-handle') || document.body.classList.contains('cr-dragging')) return;
+ const th = e.target.closest('.cg-sortth');
+ if (th && th.dataset.k) {
+ const k = th.dataset.k;
+ SORT = (SORT && SORT.k === k) ? { k, dir: -SORT.dir } : { k, dir: -1 };
+ saveSort(); render(); return;
+ }
+ if (O.onRowClick && !e.target.closest('a')) {
+ const tr = e.target.closest('tr[data-ri]');
+ if (tr) { O.onRowClick(lastShown[+tr.dataset.ri], tr); return; }
+ const card = e.target.closest('.cg-card[data-ri]');
+ if (card) O.onRowClick(lastShown[+card.dataset.ri], card);
+ }
+ });
+ }
+
+ // Export the CURRENTLY filtered + sorted rows (uncapped) as a CSV Blob — so a
+ // download reflects exactly what the user filtered to, not the whole dataset.
+ function exportCSV(opts) {
+ opts = opts || {};
+ const rows = sortRows(data.filter(pass));
+ const cols = fields.filter(f => !f.badge && f.k !== 'name'); // 'name' handled as the title column
+ const csvEsc = v => { if (v == null) return ''; v = String(v); return /[",\n]/.test(v) ? '"' + v.replace(/"/g, '""') + '"' : v; };
+ const header = ['Name'].concat(cols.map(f => f.l));
+ const lines = [header.map(csvEsc).join(',')];
+ rows.forEach(r => {
+ const t = O.title ? O.title(r) : { name: r.name || '' };
+ lines.push([csvEsc(t.name)].concat(cols.map(f => csvEsc(fval(r, f)))).join(','));
+ });
+ const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url; a.download = opts.filename || 'export.csv';
+ document.body.appendChild(a); a.click(); a.remove();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ return rows.length;
}
- buildTop(); buildRail(); render();
- return { render, setData(d) { O.data.length = 0; d.forEach(x => O.data.push(x)); buildRail(); render(); } };
+ readFromURL(); buildTop(); buildRail(); render();
+ return {
+ render,
+ setData(d) { O.data.length = 0; d.forEach(x => O.data.push(x)); buildRail(); render(); },
+ exportCSV,
+ filteredCount() { return data.filter(pass).length; },
+ };
}
global.CrcpGrid = { mount: opts => new CrcpGrid(opts) };
diff --git a/public/crcp-grid.js.pre-3view.bak b/public/crcp-grid.js.pre-3view.bak
new file mode 100644
index 0000000..1bfc75d
--- /dev/null
+++ b/public/crcp-grid.js.pre-3view.bak
@@ -0,0 +1,191 @@
+/* crcp-grid.js — reusable Amazon-style faceted grid engine for the CRE fleet.
+ * Config-driven, theme-agnostic (inherits the host page's CSS vars via cg-grid.css).
+ * One page = one CrcpGrid.mount(opts) call. Mirrors the proven condos.html engine so
+ * every list page gets the SAME behavior: left-rail facets, per-field show/hide toggles,
+ * grid/list view toggle, density slider, live search, localStorage persistence.
+ *
+ * opts = {
+ * data: [row,...], // all rows (client-side faceting)
+ * fields: [{k,l,def,col,badge,money,num,date,calc,suf,pct}], // every data point (card rows + list cols)
+ * facets: [{k,l,type:'chip'|'range',calc,order,max}], // rail filters (auto-derived if omitted)
+ * mount: '#cgMain', // container for topbar + grid
+ * rail: '#cgRail', // container for the left rail
+ * title: row => ({name, sub}),// card/list title + subline
+ * search: row => 'text blob', // searchable string
+ * badgeHtml: row => '<span>...'|'',// optional status badge html (card + list)
+ * storageKey:'fha', // localStorage namespace
+ * cols: 3, // default density
+ * onRow: row => ({cls}), // optional per-row class hook (e.g. red flag)
+ * }
+ */
+(function (global) {
+ const esc = s => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
+ const $ = (sel, root) => (root || document).querySelector(sel);
+
+ function fmt(v, f) {
+ if (v == null || v === '') return '—';
+ if (f.money) return '$' + (+v).toLocaleString() + (f.suf || '');
+ if (f.pct) return (+v).toFixed(2) + '%';
+ if (f.num) return (+v).toLocaleString() + (f.suf || '');
+ if (f.date) { try { return new Date(v).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); } catch (e) { return '—'; } }
+ return esc(v) + (f.suf || '');
+ }
+
+ function CrcpGrid(opts) {
+ const O = opts;
+ const KEY = O.storageKey || 'cg';
+ const fields = O.fields;
+ const data = O.data || [];
+ const fval = (row, f) => f.calc ? f.calc(row) : row[f.k];
+
+ // ---- field visibility (persisted) ----
+ const VIS = (function () {
+ let s = {}; try { s = JSON.parse(localStorage.getItem(KEY + 'Fields') || '{}'); } catch (e) {}
+ const o = {}; fields.forEach(f => o[f.k] = (f.k in s) ? !!s[f.k] : !!f.def); return o;
+ })();
+ const saveVis = () => { try { localStorage.setItem(KEY + 'Fields', JSON.stringify(VIS)); } catch (e) {} };
+
+ // ---- auto-derive facets if not provided ----
+ let facets = O.facets;
+ if (!facets) {
+ facets = [];
+ fields.forEach(f => {
+ if (f.badge || f.k === 'title') return;
+ const vals = data.map(r => fval(r, f)).filter(v => v != null && v !== '');
+ if (!vals.length) return;
+ const numeric = f.money || f.num || f.pct || vals.every(v => typeof v === 'number' || /^\d+(\.\d+)?$/.test(v));
+ const uniq = new Set(vals.map(String));
+ if (!numeric && uniq.size <= 24) facets.push({ k: f.k, l: f.l, type: 'chip', calc: f.calc });
+ else if (numeric) facets.push({ k: f.k, l: f.l, type: 'range', calc: f.calc, money: f.money });
+ });
+ }
+
+ // ---- filter state ----
+ const F = { q: '', chips: {}, ranges: {} };
+ facets.forEach(fc => { if (fc.type === 'chip') F.chips[fc.k] = new Set(); else F.ranges[fc.k] = { min: null, max: null }; });
+
+ const gv = (row, fc) => fc.calc ? fc.calc(row) : row[fc.k];
+ function pass(row) {
+ for (const fc of facets) {
+ if (fc.type === 'chip') { const set = F.chips[fc.k]; if (set.size && !set.has(String(gv(row, fc)))) return false; }
+ else { const r = F.ranges[fc.k], v = gv(row, fc);
+ if (r.min != null && !(+v >= r.min)) return false;
+ if (r.max != null && !(+v <= r.max)) return false; }
+ }
+ if (F.q) { const blob = (O.search ? O.search(row) : fields.map(f => fval(row, f)).join(' ')).toLowerCase(); if (!blob.includes(F.q)) return false; }
+ return true;
+ }
+
+ // ---- rail ----
+ function buildRail() {
+ const rail = $(O.rail);
+ let html = '';
+ facets.forEach(fc => {
+ if (fc.type === 'chip') {
+ const counts = {};
+ data.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="cg-sec collapsed"><h4>${esc(fc.l)}</h4><div class="cg-chips" data-facet="${esc(fc.k)}">` +
+ ent.map(([k, c]) => `<span class="cg-chip" data-v="${esc(k)}">${esc(k)}<span class="cg-ct">${c}</span></span>`).join('') + `</div></div>`;
+ } else {
+ html += `<div class="cg-sec collapsed"><h4>${esc(fc.l)}</h4><div class="cg-row2" data-facet="${esc(fc.k)}">` +
+ `<div class="cg-fld"><label>Min</label><input type="number" class="cg-min" placeholder="any"></div>` +
+ `<div class="cg-fld"><label>Max</label><input type="number" class="cg-max" placeholder="any"></div></div></div>`;
+ }
+ });
+ html += `<div class="cg-sec"><h4>Fields — show / hide</h4><div class="cg-ftogs">` +
+ fields.map(f => `<label class="cg-ftog"><input type="checkbox" data-fk="${esc(f.k)}" ${VIS[f.k] ? 'checked' : ''}><span>${esc(f.l)}</span>${f.col ? '<span class="cg-ct" title="List column">▤</span>' : ''}</label>`).join('') +
+ `</div><div class="cg-mini"><button data-all="1">All</button><button data-none="1">None</button><button data-def="1">Default</button></div></div>`;
+ rail.innerHTML = html;
+
+ // wire facets
+ rail.querySelectorAll('.cg-chips').forEach(box => box.addEventListener('click', e => {
+ const c = e.target.closest('.cg-chip'); if (!c) return;
+ const set = F.chips[box.dataset.facet]; const v = c.dataset.v;
+ set.has(v) ? set.delete(v) : set.add(v); c.classList.toggle('active'); render();
+ }));
+ rail.querySelectorAll('.cg-row2').forEach(box => {
+ const k = box.dataset.facet;
+ box.querySelector('.cg-min').addEventListener('input', e => { F.ranges[k].min = e.target.value ? +e.target.value : null; render(); });
+ box.querySelector('.cg-max').addEventListener('input', e => { F.ranges[k].max = e.target.value ? +e.target.value : null; render(); });
+ });
+ // field toggles
+ rail.querySelector('.cg-ftogs').addEventListener('change', e => { const cb = e.target.closest('input[data-fk]'); if (!cb) return; VIS[cb.dataset.fk] = cb.checked; saveVis(); render(); });
+ const mini = rail.querySelector('.cg-mini');
+ mini.addEventListener('click', e => {
+ const b = e.target.closest('button'); if (!b) return;
+ if (b.dataset.all) fields.forEach(f => VIS[f.k] = true);
+ else if (b.dataset.none) fields.forEach(f => VIS[f.k] = false);
+ else if (b.dataset.def) fields.forEach(f => VIS[f.k] = !!f.def);
+ saveVis(); buildRail(); render();
+ });
+ // collapsible headers
+ rail.querySelectorAll('.cg-sec > h4').forEach(h => h.addEventListener('click', () => h.parentElement.classList.toggle('collapsed')));
+ }
+
+ // ---- renderers ----
+ function cards(rows) {
+ const vis = fields.filter(f => VIS[f.k] && !f.badge && f.k !== 'title');
+ return rows.map(r => {
+ const t = O.title ? O.title(r) : { name: r.name || '', sub: '' };
+ const rc = O.onRow ? (O.onRow(r).cls || '') : '';
+ const nm = t.href ? `<a href="${esc(t.href)}" target="_blank" rel="noopener noreferrer" style="color:inherit">${esc(t.name)}</a>` : esc(t.name);
+ return `<div class="cg-card ${rc}">` +
+ `<div class="cg-name">${nm}</div>${t.sub ? `<div class="cg-sub">${esc(t.sub)}</div>` : ''}` +
+ (O.badgeHtml ? O.badgeHtml(r) : '') +
+ vis.map(f => `<div class="cg-r"><span class="cg-k">${esc(f.l)}</span><span>${fmt(fval(r, f), f)}</span></div>`).join('') +
+ `</div>`;
+ }).join('');
+ }
+ function list(rows) {
+ const cols = fields.filter(f => f.col && VIS[f.k] && !f.badge);
+ const head = `<th class="cg-stick">${esc((O.titleCol || 'Name'))}</th>` + cols.map(f => `<th>${esc(f.l)}</th>`).join('');
+ const body = rows.map(r => {
+ const t = O.title ? O.title(r) : { name: '', sub: '' };
+ const rc = O.onRow ? (O.onRow(r).cls || '') : '';
+ const nm = t.href ? `<a href="${esc(t.href)}" target="_blank" rel="noopener noreferrer" style="color:inherit">${esc(t.name)}</a>` : esc(t.name);
+ return `<tr class="${rc}"><td class="cg-stick"><span class="cg-la">${nm}</span>${t.sub ? `<div class="cg-lc">${esc(t.sub)}</div>` : ''}</td>` +
+ cols.map(f => `<td>${fmt(fval(r, f), f)}</td>`).join('') + `</tr>`;
+ }).join('');
+ return `<div class="cg-listwrap"><table class="cg-tbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
+ }
+
+ let VIEW = localStorage.getItem(KEY + 'View') || O.view || 'grid';
+ const CAP = O.cap || 1500; // DOM render cap for perf; count still reflects the full match set
+ function render() {
+ const rows = data.filter(pass);
+ const shown = rows.length > CAP ? rows.slice(0, CAP) : rows;
+ const g = $(O.mount + ' .cg-grid');
+ if (!rows.length) { g.className = 'cg-grid'; g.innerHTML = '<div class="cg-empty">no matches for these filters.</div>'; }
+ else if (VIEW === 'list') { g.className = 'cg-grid cg-listhost'; g.innerHTML = list(shown); }
+ else { g.className = 'cg-grid'; g.innerHTML = cards(shown); }
+ const cnt = $(O.mount + ' .cg-count');
+ if (cnt) cnt.innerHTML = `<b>${rows.length.toLocaleString()}</b> of ${data.length.toLocaleString()}` + (rows.length > CAP ? ` <span style="opacity:.7">(first ${CAP.toLocaleString()} shown)</span>` : '');
+ }
+
+ // ---- topbar ----
+ function buildTop() {
+ const m = $(O.mount);
+ m.insertAdjacentHTML('afterbegin',
+ `<div class="cg-top">` +
+ `<input type="text" class="cg-q" placeholder="search…">` +
+ `<span class="cg-seg"><button data-view="grid" class="${VIEW === 'grid' ? 'active' : ''}">▦ Grid</button><button data-view="list" class="${VIEW === 'list' ? 'active' : ''}">☰ List</button></span>` +
+ `<label class="cg-dens">Density <input type="range" class="cg-cols" min="1" max="12" step="1" value="${O.cols || 3}"></label>` +
+ `<span class="cg-count"></span></div><div class="cg-grid"></div>`);
+ const q = m.querySelector('.cg-q');
+ q.addEventListener('input', () => { clearTimeout(q._t); q._t = setTimeout(() => { F.q = q.value.trim().toLowerCase(); render(); }, 160); });
+ m.querySelector('.cg-seg').addEventListener('click', e => { const b = e.target.closest('button[data-view]'); if (!b) return; VIEW = b.dataset.view; localStorage.setItem(KEY + 'View', VIEW); m.querySelectorAll('.cg-seg button').forEach(x => x.classList.toggle('active', x.dataset.view === VIEW)); render(); });
+ const cols = m.querySelector('.cg-cols');
+ const setCols = v => { m.style.setProperty('--cg-cols', v); localStorage.setItem(KEY + 'Cols', v); };
+ cols.addEventListener('input', e => setCols(e.target.value));
+ const sc = localStorage.getItem(KEY + 'Cols'); if (sc) { cols.value = sc; }
+ setCols(cols.value);
+ }
+
+ buildTop(); buildRail(); render();
+ return { render, setData(d) { O.data.length = 0; d.forEach(x => O.data.push(x)); buildRail(); render(); } };
+ }
+
+ global.CrcpGrid = { mount: opts => new CrcpGrid(opts) };
+})(window);
diff --git a/public/crcp.html b/public/crcp.html
index b0a0a28..35168cf 100644
--- a/public/crcp.html
+++ b/public/crcp.html
@@ -204,6 +204,7 @@
border-radius:6px;padding:9px 13px;font-size:12.5px;color:var(--del);}
#crcpConnBanner.show{display:block;}
</style>
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -933,5 +934,6 @@ loadSegments(); setInterval(loadSegments, 20000);
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',start); else start();
})();
</script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/credit-unions.html b/public/credit-unions.html
index 5874d74..4bf87e2 100644
--- a/public/credit-unions.html
+++ b/public/credit-unions.html
@@ -6,6 +6,7 @@
<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='%230c0f15'/%3E%3Ctext x='8' y='12' font-size='9' text-anchor='middle' fill='%23C8A24B'%3E%F0%9F%A4%9D%3C/text%3E%3C/svg%3E">
<title>Credit Unions — with LA County offices (NCUA)</title>
<link rel="stylesheet" href="/lending-grid.css">
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -74,5 +75,6 @@ window.GRID_CONFIG = {
<script src="/lending-grid.js" defer></script>
<script src="/col-resize.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/deals-flow.html b/public/deals-flow.html
index 53abd32..749d6d2 100644
--- a/public/deals-flow.html
+++ b/public/deals-flow.html
@@ -98,7 +98,8 @@ select{background:var(--card);border:1px solid var(--line);color:var(--ink);bord
.roleopt.sel{border-color:var(--acc);background:rgba(63,185,80,.12)}
.roleopt b{display:block;color:#fff;font-size:12.5px}
.roleopt span{font-size:10px;color:var(--mut)}
-</style></head><body>
+</style> <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+</head><body>
<header>
<h1><b>Closed Deal Flow</b> · LA Commercial</h1>
<span class="badge">100% public record</span>
@@ -463,4 +464,5 @@ Promise.all([
render();loadMe();
}).catch(()=>{$('#count').textContent='failed to load deal data';});
</script><script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body></html>
diff --git a/public/deals.html b/public/deals.html
index f7e1df2..39891b5 100644
--- a/public/deals.html
+++ b/public/deals.html
@@ -95,6 +95,7 @@
.ctrl-btn.spin .ico{animation:none;}
}
</style>
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<div class="wrap">
@@ -291,5 +292,6 @@ pull(); // initial load
<script src="/col-resize.js" defer></script>
<script src="/global-search.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/fha-leads.html b/public/fha-leads.html
index 232ec5f..cf209b6 100644
--- a/public/fha-leads.html
+++ b/public/fha-leads.html
@@ -32,7 +32,8 @@ tr:hover td{background:#131922}
.prov{font-size:11px;color:var(--mut);margin:2px 0 12px;line-height:1.5}
.stat{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:9px;padding:8px 12px;margin:0 8px 10px 0}
.stat b{font-size:19px;color:#fff}.stat span{display:block;font-size:10px;text-transform:uppercase;color:var(--mut);letter-spacing:.4px}
-</style></head><body>
+</style> <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+</head><body>
<header>
<h1><b>Assumable-Loan Leads</b> · FHA Multifamily</h1>
<span class="badge">100% public data · resellable</span>
@@ -140,4 +141,5 @@ $('#dens').value=dens;
fetch('/data/fha-loans.json').then(r=>r.json()).then(j=>{DATA=(j.rows||[]).filter(r=>r.rate!=null);buildRail();render();})
.catch(()=>{$('#count').textContent='failed to load /data/fha-loans.json';});
</script><script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body></html>
diff --git a/public/fha-loans.html b/public/fha-loans.html
index b7d3337..024e114 100644
--- a/public/fha-loans.html
+++ b/public/fha-loans.html
@@ -137,6 +137,7 @@
@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">
@@ -526,5 +527,6 @@ load();
})();
</script>
<script src="/crcp-theme.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/gov-agents.html b/public/gov-agents.html
index 38a5d0d..122c652 100644
--- a/public/gov-agents.html
+++ b/public/gov-agents.html
@@ -62,6 +62,7 @@
#modal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
#modal .litem{display:flex;justify-content:space-between;gap:12px;padding:5px 0;border-bottom:1px solid #20262f;font-size:13px;} #modal .litem .m{color:var(--mut);}
</style>
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -301,5 +302,6 @@ loadIndex();
<script src="/column-manager.js" defer></script>
<script src="/col-resize.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/graphics.html b/public/graphics.html
index e010903..83d7f20 100644
--- a/public/graphics.html
+++ b/public/graphics.html
@@ -40,6 +40,7 @@
@media(max-width:820px){ main{grid-template-columns:1fr;} }
</style>
<link rel="stylesheet" href="/crcp-theme.css">
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -240,5 +241,6 @@ const HOVER=(evt,els)=>{ const t=evt&&evt.native&&evt.native.target; if(t) t.sty
<script src="/col-resize.js" defer></script>
<script src="/corner-nav.js" defer></script>
<script src="/crcp-theme.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/index.html b/public/index.html
index dca815a..3ebb3a6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -380,6 +380,7 @@
</style>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-7R2X3B2V18"></script><script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-7R2X3B2V18');</script>
<link rel="stylesheet" href="/crcp-theme.css">
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -2080,5 +2081,6 @@ function loadRanked(_retry){ return fetch('data/ranked.json').then(r=>{ if(!r.ok
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',mount); else mount();
})();
</script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/just-listed.html b/public/just-listed.html
index 1f21974..73838b3 100644
--- a/public/just-listed.html
+++ b/public/just-listed.html
@@ -73,7 +73,8 @@ td.wrap{white-space:normal;min-width:220px;max-width:380px;color:var(--mut)}
td a.f{color:var(--blue);text-decoration:none}
td a.f:hover{text-decoration:underline}
.empty{color:var(--mut);padding:40px;text-align:center}
-</style></head><body>
+</style> <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+</head><body>
<header>
<h1><b>Just Listed</b> · Active Inventory</h1>
<span class="badge" id="srcBadge">active listings</span>
@@ -296,4 +297,5 @@ fetch('/data/listings.json').then(r=>r.json()).then(d=>{
</script> <script src="/global-search.js" defer></script>
<script src="/sort-bar.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body></html>
diff --git a/public/lenders.html b/public/lenders.html
index 54116f5..b1ac01a 100644
--- a/public/lenders.html
+++ b/public/lenders.html
@@ -6,6 +6,7 @@
<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='%230c0f15'/%3E%3Ctext x='8' y='12' font-size='9' text-anchor='middle' fill='%23C8A24B'%3E%F0%9F%8F%A6%3C/text%3E%3C/svg%3E">
<title>Lenders — Banks with LA County branches (FDIC)</title>
<link rel="stylesheet" href="/lending-grid.css">
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -77,5 +78,6 @@ window.GRID_CONFIG = {
<script src="/lending-grid.js" defer></script>
<script src="/col-resize.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/licensed-agents.html b/public/licensed-agents.html
index 6f0e8ff..b719612 100644
--- a/public/licensed-agents.html
+++ b/public/licensed-agents.html
@@ -60,6 +60,7 @@
#modal .kv{margin-top:8px;display:grid;grid-template-columns:auto 1fr;gap:5px 12px;font-size:13px;}
#modal .kv .k{color:var(--mut);} #modal .kv .v a{color:var(--blue);text-decoration:none;}
</style>
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -283,5 +284,6 @@ if(localStorage.getItem('laMode')==='brokers') setMode('brokers'); else { $('#fi
</script>
<script src="/col-resize.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/licensed.html b/public/licensed.html
index e1999a9..ba90f6a 100644
--- a/public/licensed.html
+++ b/public/licensed.html
@@ -53,7 +53,8 @@ input#q{min-width:230px}
#modal h3{margin:0 0 3px;font-size:19px}#modal .x{position:absolute;top:11px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;line-height:1}
#modal .mrow{display:flex;justify-content:space-between;gap:12px;padding:5px 0;border-bottom:1px solid var(--line);font-size:13px}
#modal .mrow .mk{color:var(--mut);text-transform:capitalize}#modal .mrow .mv{text-align:right;word-break:break-word}
-</style></head><body>
+</style> <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+</head><body>
<header>
<div><h1>📋 Licensed Directory <span class="sub" id="scope"></span></h1></div>
<div class="tabs" id="tabs"></div>
@@ -210,4 +211,5 @@ boot().catch(e=>{$('#count').textContent='Failed to load: '+e.message;});
</script> <script src="/global-search.js" defer></script>
<script src="/sort-bar.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body></html>
diff --git a/public/loan-officers.html b/public/loan-officers.html
index 63953b3..499c129 100644
--- a/public/loan-officers.html
+++ b/public/loan-officers.html
@@ -12,6 +12,7 @@
.compliance a{color:var(--blue);} .compliance .lk{display:inline-block;margin-top:8px;margin-right:14px;}
.empty{margin:20px;padding:40px 24px;text-align:center;color:var(--mut);border:1px dashed var(--line);border-radius:12px;}
</style>
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -85,5 +86,6 @@ fetch('/api/lending/loan-officers', { credentials:'same-origin' }).then(r=>r.jso
}).catch(()=>{});
</script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/mls.html b/public/mls.html
index 865f715..a31f7f9 100644
--- a/public/mls.html
+++ b/public/mls.html
@@ -109,6 +109,7 @@
.heatleg .hb { width:52px; height:8px; border-radius:4px; background:linear-gradient(90deg,hsl(0,70%,45%),hsl(60,70%,45%),hsl(120,70%,45%)); }
</style>
<link rel="stylesheet" href="/crcp-theme.css">
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -680,5 +681,6 @@ document.addEventListener('click', async e=>{
<script src="/sort-bar.js" defer></script>
<script src="/corner-nav.js" defer></script>
<script src="/crcp-theme.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/mortgage-market.html b/public/mortgage-market.html
index 853e66d..c375a16 100644
--- a/public/mortgage-market.html
+++ b/public/mortgage-market.html
@@ -6,6 +6,7 @@
<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='%230c0f15'/%3E%3Ctext x='8' y='12' font-size='9' text-anchor='middle' fill='%23C8A24B'%3E%F0%9F%93%8A%3C/text%3E%3C/svg%3E">
<title>Residential Mortgage Market — LA County lenders (CFPB / HMDA)</title>
<link rel="stylesheet" href="/lending-grid.css">
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -66,5 +67,6 @@ window.GRID_CONFIG = {
<script src="/lending-grid.js" defer></script>
<script src="/col-resize.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/nav-agent/nav-agent.css b/public/nav-agent/nav-agent.css
new file mode 100644
index 0000000..702094c
--- /dev/null
+++ b/public/nav-agent/nav-agent.css
@@ -0,0 +1,93 @@
+/* ============================================================================
+ * Nav Agent — universal grid-controls drop-in (v1.0)
+ * Left field-toggle panel + sort-every-field + grouping + movable/removable/
+ * expandable data panels. Zero dependencies. Additive & defensive.
+ * Honors the house internal-line-viewer spec (left panel, density type-scale,
+ * tight cards, localStorage-persisted).
+ * ==========================================================================*/
+
+:root{
+ --na-acc:var(--acc,#1f9e8f);
+ --na-line:var(--line,#2a2f3a);
+ --na-bg:var(--panel,#12151c);
+ --na-bg2:var(--bg,#0c0e13);
+ --na-fg:var(--fg,#e7e9ee);
+ --na-mut:var(--mut,#8b93a3);
+}
+
+/* ---- launcher (bottom-left cog) --------------------------------------- */
+.na-launch{position:fixed;left:14px;bottom:14px;z-index:2147483000;
+ width:42px;height:42px;border-radius:50%;border:1px solid var(--na-line);
+ background:var(--na-bg);color:var(--na-fg);font-size:19px;cursor:pointer;
+ display:flex;align-items:center;justify-content:center;
+ box-shadow:0 4px 18px rgba(0,0,0,.45);transition:transform .15s}
+.na-launch:hover{transform:scale(1.08);border-color:var(--na-acc)}
+
+/* ---- left field/sort/group panel -------------------------------------- */
+.na-panel{position:fixed;left:0;top:0;bottom:0;width:290px;z-index:2147483001;
+ background:var(--na-bg);border-right:1px solid var(--na-line);color:var(--na-fg);
+ display:flex;flex-direction:column;font:13px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
+ transform:translateX(-100%);transition:transform .2s ease;box-shadow:2px 0 24px rgba(0,0,0,.4)}
+.na-panel.open{transform:translateX(0)}
+.na-panel header{display:flex;align-items:center;gap:8px;padding:12px 14px;
+ border-bottom:1px solid var(--na-line);font-weight:600;letter-spacing:.3px}
+.na-panel header .na-title{flex:1}
+.na-panel header button{background:none;border:0;color:var(--na-mut);cursor:pointer;font-size:16px;padding:2px 6px;border-radius:4px}
+.na-panel header button:hover{color:var(--na-fg);background:rgba(255,255,255,.06)}
+.na-body{flex:1;overflow:auto;padding:8px 12px 40px}
+.na-sec{border-top:1px solid var(--na-line);padding:10px 0}
+.na-sec:first-child{border-top:0}
+.na-sec>.na-h{display:flex;align-items:center;gap:6px;cursor:pointer;
+ font-weight:600;color:var(--na-mut);text-transform:uppercase;font-size:11px;letter-spacing:.6px;user-select:none}
+.na-sec>.na-h::before{content:'▸';font-size:9px;transition:transform .15s}
+.na-sec.open>.na-h::before{transform:rotate(90deg)}
+.na-sec .na-c{display:none;padding:8px 2px 2px}
+.na-sec.open .na-c{display:block}
+.na-search{width:100%;box-sizing:border-box;background:var(--na-bg2);border:1px solid var(--na-line);
+ color:var(--na-fg);border-radius:6px;padding:8px 10px;margin:4px 0 10px;font-size:13px}
+.na-fieldrow{display:flex;align-items:center;gap:8px;padding:4px 2px;cursor:pointer;border-radius:4px}
+.na-fieldrow:hover{background:rgba(255,255,255,.04)}
+.na-fieldrow input{accent-color:var(--na-acc);cursor:pointer}
+.na-fieldrow .na-cnt{margin-left:auto;color:var(--na-mut);font-size:11px}
+.na-row{display:flex;gap:6px;align-items:center;margin:6px 0}
+.na-row label{color:var(--na-mut);min-width:52px}
+.na-row select,.na-row input[type=range]{flex:1;background:var(--na-bg2);border:1px solid var(--na-line);
+ color:var(--na-fg);border-radius:6px;padding:6px 8px;font-size:13px}
+.na-mini{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
+.na-mini button{background:var(--na-bg2);border:1px solid var(--na-line);color:var(--na-fg);
+ border-radius:6px;padding:5px 9px;font-size:12px;cursor:pointer}
+.na-mini button:hover,.na-mini button.on{border-color:var(--na-acc);color:var(--na-acc)}
+.na-reset{width:100%;margin-top:14px;background:var(--na-bg2);border:1px solid var(--na-line);
+ color:var(--na-fg);border-radius:6px;padding:9px;cursor:pointer;font-size:13px}
+.na-reset:hover{border-color:#c0563f;color:#e08a76}
+.na-note{color:var(--na-mut);font-size:11px;padding:2px;line-height:1.5}
+
+/* ---- grouping headers injected into the grid -------------------------- */
+.na-group-h{grid-column:1/-1;display:flex;align-items:center;gap:8px;
+ padding:8px 4px;margin:6px 0 2px;border-bottom:1px solid var(--na-line);
+ color:var(--na-acc);font-weight:600;font-size:13px;cursor:pointer;user-select:none}
+.na-group-h .na-gcount{color:var(--na-mut);font-weight:400}
+.na-group-h.collapsed + .na-group-body{display:none}
+table .na-group-h{display:table-row}
+table .na-group-h td{padding:8px 6px;color:var(--na-acc);font-weight:600;border-bottom:1px solid var(--na-line)}
+
+/* ---- movable / resizable / removable data-panel chrome ---------------- */
+.na-wrap{position:relative}
+.na-bar{display:flex;align-items:center;gap:6px;padding:4px 8px;cursor:move;
+ background:var(--na-bg);border:1px solid var(--na-line);border-bottom:0;
+ border-radius:6px 6px 0 0;color:var(--na-mut);font-size:12px;user-select:none}
+.na-bar .na-bt{flex:1;font-weight:600;color:var(--na-fg)}
+.na-bar button{background:none;border:0;color:var(--na-mut);cursor:pointer;font-size:14px;padding:1px 5px;border-radius:4px}
+.na-bar button:hover{color:var(--na-fg);background:rgba(255,255,255,.08)}
+.na-wrap.na-float{position:fixed;z-index:2000;box-shadow:0 10px 40px rgba(0,0,0,.55);
+ background:var(--na-bg2);border-radius:6px;resize:both;overflow:auto;min-width:280px;min-height:160px}
+.na-wrap.na-collapsed .na-inner{display:none}
+.na-wrap.na-removed{display:none}
+.na-wrap.na-max{position:fixed;inset:12px;z-index:2001;background:var(--na-bg2);
+ box-shadow:0 10px 60px rgba(0,0,0,.6);overflow:auto;resize:none}
+.na-restorebar{position:fixed;left:14px;bottom:66px;z-index:2147483000;display:flex;flex-direction:column;gap:6px}
+.na-restorebar button{background:var(--na-bg);border:1px solid var(--na-line);color:var(--na-fg);
+ border-radius:6px;padding:6px 10px;font-size:12px;cursor:pointer}
+.na-restorebar button:hover{border-color:var(--na-acc)}
+
+@media (max-width:640px){.na-panel{width:86vw}}
diff --git a/public/nav-agent/nav-agent.js b/public/nav-agent/nav-agent.js
new file mode 100644
index 0000000..89119e2
--- /dev/null
+++ b/public/nav-agent/nav-agent.js
@@ -0,0 +1,433 @@
+/* ============================================================================
+ * Nav Agent — universal grid-controls drop-in (v1.0)
+ *
+ * Drop one <script src=".../nav-agent.js" defer></script> (+ the CSS) onto any
+ * page. It auto-detects the primary data grid (a <table> or a .grid/.card list),
+ * then injects:
+ * • a LEFT collapsible panel: text search, per-FIELD show/hide toggles,
+ * sort-by-any-field (asc/desc), group-by-any-field, density slider
+ * • movable / resizable / collapsible / removable / maximizable panel chrome
+ * on the grid itself
+ * All state persists to localStorage, namespaced per page path, so every
+ * surface remembers its own layout. Strictly additive & wrapped in try/catch —
+ * if it can't understand a page it degrades (panel chrome + search + density)
+ * and never throws into the host page.
+ *
+ * Manual control: window.NavAgent.mount({ target:'#grid', title:'Deals' })
+ * Opt-out a page: <body data-nav-agent="off">
+ * Force a target: <div id="grid" data-nav-grid> ... </div>
+ * ==========================================================================*/
+(function () {
+ 'use strict';
+ if (window.NavAgent) return;
+
+ var _uid = 0;
+ var navKey = function (target) {
+ if (!target.getAttribute('data-na-uid')) target.setAttribute('data-na-uid', target.id || ('g' + (++_uid)));
+ return 'navagent:' + location.pathname + ':' + target.getAttribute('data-na-uid');
+ };
+ var load = function (key) { try { return JSON.parse(localStorage.getItem(key)) || {}; } catch (e) { return {}; } };
+ var save = function (key, s) { try { localStorage.setItem(key, JSON.stringify(s)); } catch (e) {} };
+ var slug = function (t) { return String(t || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'f'; };
+ var txt = function (el) { return (el && el.textContent || '').trim(); };
+ // magnitude-aware numeric parse — $2.1B > $780M > 42. Handles K/M/B/T + commas.
+ var num = function (v) {
+ var s = String(v).replace(/,/g, '').trim();
+ var m = s.match(/-?\d*\.?\d+/); if (!m) return null;
+ var n = parseFloat(m[0]); if (isNaN(n)) return null;
+ var suf = (s.slice(m.index + m[0].length).match(/[kmbt]/i) || [''])[0].toLowerCase();
+ var mult = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 }[suf] || 1;
+ return n * mult;
+ };
+ var el = function (tag, cls, html) { var e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; };
+
+ /* -- target detection -------------------------------------------------- */
+ function findTarget(opt) {
+ if (opt && opt.target) return document.querySelector(opt.target);
+ var q = document.querySelector('[data-nav-grid]');
+ if (q) return q;
+ // biggest visible table wins, else #grid, else first .grid, else .cards
+ var tables = [].slice.call(document.querySelectorAll('table'))
+ .filter(function (t) { return t.querySelector('tbody tr') && t.offsetParent !== null; })
+ .sort(function (a, b) { return b.querySelectorAll('tr').length - a.querySelectorAll('tr').length; });
+ if (tables[0] && tables[0].querySelectorAll('tr').length > 2) return tables[0];
+ return document.querySelector('#grid') || document.querySelector('.grid') ||
+ document.querySelector('.cards') || tables[0] || null;
+ }
+
+ /* -- field model ------------------------------------------------------- */
+ // Returns { kind:'table'|'cards', fields:[{key,label,idx?,cls?}], items() }
+ function model(target) {
+ if (target.tagName === 'TABLE') {
+ var ths = [].slice.call(target.querySelectorAll('thead th, thead td'));
+ if (!ths.length) { // header-less table: synthesize from first row
+ var r0 = target.querySelector('tbody tr, tr');
+ ths = r0 ? [].slice.call(r0.children).map(function (_, i) { var s = el('th'); s.textContent = 'Col ' + (i + 1); return s; }) : [];
+ }
+ var fields = ths.map(function (th, i) { return { key: slug(txt(th)) + '-' + i, label: txt(th) || ('Col ' + (i + 1)), idx: i }; });
+ return {
+ kind: 'table', fields: fields,
+ rows: function () { return [].slice.call(target.querySelectorAll('tbody tr')).filter(function (r) { return !r.classList.contains('na-group-h'); }); },
+ cellText: function (row, f) { var c = row.children[f.idx]; return txt(c); }
+ };
+ }
+ // card grid — infer fields from repeated child element classes / [data-field]
+ var cards = [].slice.call(target.children).filter(function (c) { return c.nodeType === 1 && !c.classList.contains('na-group-h') && !c.classList.contains('na-group-body'); });
+ var sample = cards.slice(0, 24), seen = {}, order = [];
+ sample.forEach(function (card) {
+ [].slice.call(card.querySelectorAll('[data-field]')).forEach(function (n) {
+ var k = n.getAttribute('data-field'); if (!seen[k]) { seen[k] = { key: slug(k), label: k, cls: '[data-field="' + k + '"]', df: k, hits: 0 }; order.push(seen[k]); } seen[k].hits++;
+ });
+ // also collect direct-child element classes as coarse fields when no data-field present
+ [].slice.call(card.children).forEach(function (n) {
+ var cls = (n.className && typeof n.className === 'string') ? n.className.split(/\s+/)[0] : '';
+ if (!cls || cls.indexOf('na-') === 0) return;
+ var k = 'c-' + cls;
+ if (!seen[k]) { seen[k] = { key: slug(cls), label: cls.replace(/[-_]/g, ' '), cls: '.' + cls, hits: 0 }; order.push(seen[k]); } seen[k].hits++;
+ });
+ });
+ // keep fields present on a majority of sampled cards (stable structure)
+ var min = Math.max(1, Math.floor(sample.length * 0.4));
+ var fields = order.filter(function (f) { return f.hits >= min; });
+ return {
+ kind: 'cards', cards: true, fields: fields,
+ rows: function () { return [].slice.call(target.children).filter(function (c) { return c.nodeType === 1 && !c.classList.contains('na-group-h') && !c.classList.contains('na-group-body') && c.offsetParent !== null || (c.nodeType === 1 && !c.classList.contains('na-group-h')); }); },
+ cellText: function (card, f) { var n = f.df ? card.querySelector('[data-field="' + f.df + '"]') : card.querySelector(f.cls); return txt(n); }
+ };
+ }
+
+ /* -- one instance ------------------------------------------------------ */
+ function Agent(target, opt) {
+ var key = navKey(target);
+ var st = load(key);
+ var M = model(target);
+ var self = { target: target, model: M, state: st, key: key };
+ var applyingSort = false;
+
+ /* field show/hide via CONTAINER classes (survives host re-renders) */
+ function fieldStyleTag() {
+ var id = 'na-style-' + key.replace(/[^a-z0-9]/gi, '');
+ var tag = document.getElementById(id) || (function () { var t = el('style'); t.id = id; document.head.appendChild(t); return t; })();
+ var css = '';
+ (st.hidden || []).forEach(function (key) {
+ var f = M.fields.filter(function (x) { return x.key === key; })[0]; if (!f) return;
+ if (M.kind === 'table') {
+ css += 'table[data-na] thead th:nth-child(' + (f.idx + 1) + '),table[data-na] tbody td:nth-child(' + (f.idx + 1) + '){display:none}';
+ } else {
+ css += target.getAttribute('data-na-sel') + ' ' + f.cls + '{display:none}';
+ }
+ });
+ tag.textContent = css;
+ }
+
+ function applySort() {
+ if (!st.sort) return;
+ var f = M.fields.filter(function (x) { return x.key === st.sort; })[0]; if (!f) return;
+ var dir = st.dir === 'desc' ? -1 : 1;
+ var rows = M.rows();
+ if (!rows.length) return;
+ applyingSort = true;
+ // detect numeric
+ var numeric = rows.slice(0, 12).every(function (r) { var v = M.cellText(r, f); return v === '' || num(v) !== null; });
+ rows.sort(function (a, b) {
+ var va = M.cellText(a, f), vb = M.cellText(b, f);
+ if (numeric) { var na = num(va), nb = num(vb); na = na == null ? -Infinity : na; nb = nb == null ? -Infinity : nb; return (na - nb) * dir; }
+ return va.localeCompare(vb, undefined, { numeric: true }) * dir;
+ });
+ var parent = M.kind === 'table' ? target.querySelector('tbody') : target;
+ rows.forEach(function (r) { parent.appendChild(r); });
+ applyGroup(); // grouping depends on sorted order
+ applyingSort = false;
+ }
+
+ function clearGroups() {
+ [].slice.call(target.querySelectorAll('.na-group-h')).forEach(function (n) { n.remove(); });
+ }
+
+ function applyGroup() {
+ clearGroups();
+ if (!st.group) return;
+ var f = M.fields.filter(function (x) { return x.key === st.group; })[0]; if (!f) return;
+ var rows = M.rows(), last = null, parent = M.kind === 'table' ? target.querySelector('tbody') : target;
+ var counts = {};
+ rows.forEach(function (r) { var v = M.cellText(r, f) || '—'; counts[v] = (counts[v] || 0) + 1; });
+ rows.forEach(function (r) {
+ var v = M.cellText(r, f) || '—';
+ if (v !== last) {
+ var h;
+ if (M.kind === 'table') { h = el('tr', 'na-group-h'); var td = el('td'); td.colSpan = M.fields.length; td.innerHTML = '▸ ' + esc(v) + ' <span class="na-gcount">(' + counts[v] + ')</span>'; h.appendChild(td); }
+ else { h = el('div', 'na-group-h', '▸ ' + esc(v) + ' <span class="na-gcount">(' + counts[v] + ')</span>'); }
+ parent.insertBefore(h, r); last = v;
+ }
+ });
+ }
+
+ function applySearch() {
+ var qraw = (st.q || '').toLowerCase();
+ M.rows().forEach(function (r) {
+ var show = !qraw || r.textContent.toLowerCase().indexOf(qraw) >= 0;
+ r.style.display = show ? '' : 'none';
+ });
+ }
+
+ function applyAll() { fieldStyleTag(); applySort(); applySearch(); }
+
+ /* re-apply after host re-renders (debounced, ignores our own mutations).
+ If the FIELD SET changed (async data arrived, filter repopulated), tell
+ mount() to rebuild the panel UI. */
+ function fieldSig(m) { return m.kind + ':' + m.fields.map(function (f) { return f.key; }).join(','); }
+ var sig = fieldSig(M);
+ var t;
+ var mo = new MutationObserver(function () {
+ if (applyingSort) return;
+ clearTimeout(t); t = setTimeout(function () {
+ M = model(target); self.model = M;
+ var ns = fieldSig(M);
+ if (ns !== sig) { sig = ns; if (self.onFields) try { self.onFields(); } catch (e) {} }
+ applyAll();
+ }, 140);
+ });
+ try { mo.observe(M.kind === 'table' ? (target.querySelector('tbody') || target) : target, { childList: true }); } catch (e) {}
+
+ self.applyAll = applyAll;
+ self.applySort = applySort;
+ self.applyGroup = applyGroup;
+ self.applySearch = applySearch;
+ self.save = function () { save(key, st); };
+ return self;
+ }
+
+ function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }
+
+ /* -- panel UI ---------------------------------------------------------- */
+ function buildPanel(agent, opt) {
+ var M = agent.model, st = agent.state;
+ var panel = el('aside', 'na-panel');
+ panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', 'View controls');
+ var title = (opt && opt.title) || document.title || 'View';
+ panel.appendChild(el('header', null,
+ '<span class="na-title">⚙ View · ' + esc(title) + '</span>' +
+ '<button class="na-x" title="Close">✕</button>'));
+ var body = el('div', 'na-body'); panel.appendChild(body);
+
+ // search
+ var search = el('input', 'na-search'); search.type = 'search'; search.placeholder = 'Search all fields…';
+ search.value = st.q || '';
+ search.addEventListener('input', function () { st.q = search.value; agent.save(); agent.applySearch(); });
+ body.appendChild(search);
+
+ function section(label, openDefault) {
+ var s = el('div', 'na-sec' + (openDefault ? ' open' : ''));
+ s.appendChild(el('div', 'na-h', label));
+ var c = el('div', 'na-c'); s.appendChild(c);
+ s.querySelector('.na-h').addEventListener('click', function () { s.classList.toggle('open'); });
+ body.appendChild(s); return c;
+ }
+
+ var haveFields = M.fields && M.fields.length;
+
+ // FIELDS toggle
+ if (haveFields) {
+ var fc = section('Fields (' + M.fields.length + ')', true);
+ st.hidden = st.hidden || [];
+ M.fields.forEach(function (f) {
+ var row = el('label', 'na-fieldrow');
+ var cb = el('input'); cb.type = 'checkbox'; cb.checked = st.hidden.indexOf(f.key) < 0;
+ cb.addEventListener('change', function () {
+ var i = st.hidden.indexOf(f.key);
+ if (cb.checked && i >= 0) st.hidden.splice(i, 1);
+ else if (!cb.checked && i < 0) st.hidden.push(f.key);
+ agent.save(); agent.applyAll();
+ });
+ row.appendChild(cb);
+ row.appendChild(el('span', null, esc(f.label)));
+ fc.appendChild(row);
+ });
+ var mini = el('div', 'na-mini',
+ '<button data-all>All</button><button data-none>None</button>');
+ mini.querySelector('[data-all]').onclick = function () { st.hidden = []; agent.save(); agent.applyAll(); refreshChecks(); };
+ mini.querySelector('[data-none]').onclick = function () { st.hidden = M.fields.map(function (f) { return f.key; }); agent.save(); agent.applyAll(); refreshChecks(); };
+ fc.appendChild(mini);
+ function refreshChecks() { [].slice.call(fc.querySelectorAll('.na-fieldrow input')).forEach(function (cb, i) { cb.checked = st.hidden.indexOf(M.fields[i].key) < 0; }); }
+ }
+
+ // SORT
+ if (haveFields) {
+ var sc = section('Sort', true);
+ var srow = el('div', 'na-row', '<label>Field</label>');
+ var sel = el('select'); sel.appendChild(new Option('— none —', ''));
+ M.fields.forEach(function (f) { sel.appendChild(new Option(f.label, f.key)); });
+ sel.value = st.sort || ''; srow.appendChild(sel); sc.appendChild(srow);
+ var dir = el('div', 'na-mini', '<button data-asc>▲ Asc</button><button data-desc>▼ Desc</button>');
+ function markDir() { dir.querySelector('[data-asc]').classList.toggle('on', st.dir !== 'desc'); dir.querySelector('[data-desc]').classList.toggle('on', st.dir === 'desc'); }
+ sel.addEventListener('change', function () { st.sort = sel.value; agent.save(); agent.applySort(); });
+ dir.querySelector('[data-asc]').onclick = function () { st.dir = 'asc'; markDir(); agent.save(); agent.applySort(); };
+ dir.querySelector('[data-desc]').onclick = function () { st.dir = 'desc'; markDir(); agent.save(); agent.applySort(); };
+ markDir(); sc.appendChild(dir);
+ }
+
+ // GROUP
+ if (haveFields) {
+ var gc = section('Group by', false);
+ var grow = el('div', 'na-row', '<label>Field</label>');
+ var gsel = el('select'); gsel.appendChild(new Option('— none —', ''));
+ M.fields.forEach(function (f) { gsel.appendChild(new Option(f.label, f.key)); });
+ gsel.value = st.group || ''; grow.appendChild(gsel); gc.appendChild(grow);
+ gsel.addEventListener('change', function () { st.group = gsel.value; agent.save(); agent.applyGroup(); });
+ }
+
+ // DENSITY (only if the target reacts to --cols or has a page density var)
+ var dc = section('Density', false);
+ var drow = el('div', 'na-row', '<label>Cols</label>');
+ var rng = el('input'); rng.type = 'range'; rng.min = 2; rng.max = 12; rng.value = st.cols || getComputedCols(agent.target) || 5;
+ rng.addEventListener('input', function () { st.cols = +rng.value; agent.save(); setCols(agent.target, +rng.value); });
+ drow.appendChild(rng); dc.appendChild(drow);
+ dc.appendChild(el('div', 'na-note', 'Adjusts grid columns where the layout supports it.'));
+ if (st.cols) setCols(agent.target, st.cols);
+
+ if (!haveFields) body.appendChild(el('div', 'na-note',
+ 'No structured fields detected on this grid — field toggles, sort & group need <code><table></code> headers or <code>[data-field]</code>/classed card elements. Search, density and the movable panel are active.'));
+
+ var reset = el('button', 'na-reset', '↺ Reset this page’s view');
+ reset.onclick = function () { try { localStorage.removeItem(agent.key); } catch (e) {} location.reload(); };
+ body.appendChild(reset);
+
+ panel.querySelector('.na-x').onclick = function () { panel.classList.remove('open'); };
+ document.body.appendChild(panel);
+ return panel;
+ }
+
+ function getComputedCols(t) { try { var v = getComputedStyle(t).getPropertyValue('--cols'); return v ? parseInt(v) : null; } catch (e) { return null; } }
+ function setCols(t, n) {
+ try {
+ t.style.setProperty('--cols', n);
+ // fallback for grids without a --cols hook
+ if (!/grid-template-columns/.test(t.style.cssText) && getComputedStyle(t).display === 'grid')
+ t.style.gridTemplateColumns = 'repeat(' + n + ',minmax(0,1fr))';
+ } catch (e) {}
+ }
+
+ /* -- movable / resizable / removable panel chrome ---------------------- */
+ function chrome(agent, opt) {
+ var t = agent.target;
+ if (t.closest('.na-wrap')) return;
+ var wrap = el('div', 'na-wrap');
+ var title = (opt && opt.title) || document.title || 'Data';
+ var bar = el('div', 'na-bar',
+ '<span class="na-bt">▤ ' + esc(title) + '</span>' +
+ '<button data-collapse title="Collapse">▁</button>' +
+ '<button data-max title="Maximize / float">⤢</button>' +
+ '<button data-remove title="Remove panel">✕</button>');
+ var inner = el('div', 'na-inner');
+ t.parentNode.insertBefore(wrap, t);
+ wrap.appendChild(bar); wrap.appendChild(inner); inner.appendChild(t);
+
+ var g = agent.state.geo || {};
+ bar.querySelector('[data-collapse]').onclick = function () { wrap.classList.toggle('na-collapsed'); persist(); };
+ bar.querySelector('[data-max]').onclick = function () { wrap.classList.toggle('na-max'); wrap.classList.remove('na-float'); persist(); };
+ bar.querySelector('[data-remove]').onclick = function () { wrap.classList.add('na-removed'); addRestore(); persist(); };
+
+ // drag to float/move
+ var down = null;
+ bar.addEventListener('mousedown', function (e) {
+ if (e.target.tagName === 'BUTTON') return;
+ var r = wrap.getBoundingClientRect();
+ if (!wrap.classList.contains('na-float')) { wrap.classList.add('na-float'); wrap.style.left = r.left + 'px'; wrap.style.top = r.top + 'px'; wrap.style.width = r.width + 'px'; }
+ down = { x: e.clientX, y: e.clientY, l: parseFloat(wrap.style.left) || r.left, t: parseFloat(wrap.style.top) || r.top };
+ e.preventDefault();
+ });
+ window.addEventListener('mousemove', function (e) {
+ if (!down) return;
+ wrap.style.left = (down.l + e.clientX - down.x) + 'px';
+ wrap.style.top = Math.max(0, down.t + e.clientY - down.y) + 'px';
+ });
+ window.addEventListener('mouseup', function () { if (down) { down = null; persist(); } });
+
+ function persist() {
+ agent.state.geo = {
+ float: wrap.classList.contains('na-float'), max: wrap.classList.contains('na-max'),
+ collapsed: wrap.classList.contains('na-collapsed'), removed: wrap.classList.contains('na-removed'),
+ left: wrap.style.left, top: wrap.style.top, width: wrap.style.width, height: wrap.style.height
+ };
+ agent.save();
+ }
+ function addRestore() {
+ var rb = document.querySelector('.na-restorebar') || (function () { var b = el('div', 'na-restorebar'); document.body.appendChild(b); return b; })();
+ var btn = el('button', null, '⟲ ' + esc(title));
+ btn.onclick = function () { wrap.classList.remove('na-removed'); btn.remove(); persist(); };
+ rb.appendChild(btn);
+ }
+ // restore saved geometry
+ if (g.float) { wrap.classList.add('na-float'); ['left', 'top', 'width', 'height'].forEach(function (k) { if (g[k]) wrap.style[k] = g[k]; }); }
+ if (g.max) wrap.classList.add('na-max');
+ if (g.collapsed) wrap.classList.add('na-collapsed');
+ if (g.removed) { wrap.classList.add('na-removed'); addRestore(); }
+ }
+
+ /* -- public mount ------------------------------------------------------ */
+ var API = {
+ instances: [],
+ mount: function (opt) {
+ opt = opt || {};
+ try {
+ var target = findTarget(opt);
+ if (!target) return null;
+ if (target.__naMounted) return target.__naAgent;
+ target.__naMounted = true;
+ if (target.tagName === 'TABLE') target.setAttribute('data-na', '1');
+ else target.setAttribute('data-na-sel', target.id ? '#' + target.id : '.' + (target.className.split(/\s+/)[0] || 'grid'));
+ var agent = Agent(target, opt);
+ target.__naAgent = agent;
+ // (re)build the panel — called again by the observer when fields change
+ function makePanel() {
+ var wasOpen = agent._panel && agent._panel.classList.contains('open');
+ if (agent._panel) agent._panel.remove();
+ agent._panel = buildPanel(agent, opt);
+ if (wasOpen) agent._panel.classList.add('open');
+ }
+ agent.onFields = makePanel;
+ makePanel();
+ chrome(agent, opt);
+ agent.applyAll();
+
+ // single launcher toggles every panel on the page
+ if (!document.querySelector('.na-launch')) {
+ var b = el('button', 'na-launch', '⚙'); b.title = 'View controls';
+ b.onclick = function () {
+ var anyOpen = document.querySelector('.na-panel.open');
+ [].slice.call(document.querySelectorAll('.na-panel')).forEach(function (p) { p.classList.toggle('open', !anyOpen); });
+ };
+ document.body.appendChild(b);
+ }
+ API.instances.push(agent);
+ return agent;
+ } catch (e) { if (window.console) console.warn('[nav-agent] mount skipped:', e); return null; }
+ }
+ };
+ window.NavAgent = API;
+
+ function boot() {
+ try {
+ var body = document.body;
+ if (!body || body.getAttribute('data-nav-agent') === 'off') return;
+ // wait a tick for API-driven grids to render their first batch
+ var tries = 0;
+ (function attempt() {
+ var t = findTarget();
+ var populated = false;
+ if (t) {
+ if (t.tagName === 'TABLE') populated = !!t.querySelector('tbody tr');
+ else {
+ var kids = [].slice.call(t.children).filter(function (c) { return c.nodeType === 1; });
+ // ignore an empty-state placeholder (a lone div, often "No … match")
+ populated = !!t.querySelector('.card') || kids.length >= 2;
+ }
+ }
+ if (populated || tries++ > 32) { API.mount(); return; } // mount anyway after ~8s; observer fills fields when data arrives
+ setTimeout(attempt, 250);
+ })();
+ } catch (e) {}
+ }
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
+ else boot();
+})();
diff --git a/public/parcel.html b/public/parcel.html
index 2be28a3..7fc4c3e 100644
--- a/public/parcel.html
+++ b/public/parcel.html
@@ -38,7 +38,8 @@ a.ext{color:var(--blue);text-decoration:none}a.ext:hover{text-decoration:underli
.full{grid-column:1/-1}
.loading{color:var(--mut);padding:40px 0}
.err{color:var(--bad);padding:24px 0}
-</style></head><body>
+</style> <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+</head><body>
<header>
<h1><b>Parcel Detail</b> · CRCP</h1>
<span class="pill county">100% public record</span>
@@ -154,4 +155,5 @@ function render(d){
load();
</script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body></html>
diff --git a/public/rent-rolls.html b/public/rent-rolls.html
index 218002b..d421827 100644
--- a/public/rent-rolls.html
+++ b/public/rent-rolls.html
@@ -110,6 +110,7 @@
#modal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--muted);font-size:22px;cursor:pointer;z-index:2;}
.hide{display:none;}
</style>
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<div class="wrap">
@@ -435,5 +436,6 @@ fetch('/api/rent-rolls').then(r=>r.json()).then(d=>{
<script src="/global-search.js" defer></script>
<script src="/sort-bar.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/sales.html b/public/sales.html
index 4b30313..68688e5 100644
--- a/public/sales.html
+++ b/public/sales.html
@@ -84,6 +84,7 @@
.rail .rsec.collapsed > h4::after { transform:rotate(-90deg); }
.rail .rsec.collapsed > *:not(h4) { display:none !important; }
</style>
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -412,5 +413,6 @@ fetch('/api/closed-sales?limit=5000').then(r=>r.json()).then(d=>{
<script src="/global-search.js" defer></script>
<script src="/sort-bar.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
diff --git a/public/sba-lenders.html b/public/sba-lenders.html
index ce8cc80..a3579b5 100644
--- a/public/sba-lenders.html
+++ b/public/sba-lenders.html
@@ -6,6 +6,7 @@
<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='%230c0f15'/%3E%3Ctext x='8' y='12' font-size='9' text-anchor='middle' fill='%23C8A24B'%3E%F0%9F%92%BC%3C/text%3E%3C/svg%3E">
<title>SBA Lenders — 7(a) & 504 originators (CA · LA breakout)</title>
<link rel="stylesheet" href="/lending-grid.css">
+ <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>
<header>
@@ -73,5 +74,6 @@ window.GRID_CONFIG = {
<script src="/lending-grid.js" defer></script>
<script src="/col-resize.js" defer></script>
<script src="/corner-nav.js" defer></script>
+ <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>
← 3fd8465 auto-data-snapshot: 2026-08-13T07:20:35 (1 data files) — dat
·
back to Commercialrealestate
·
condos.html: add compact-list view (3rd mode) + shared three badc059 →