[object Object]

← back to Agent Cabinet

Fix code review findings: security + proof-validity issues

1e85c636c365e587149a28b4d2c87c1b187308f3 · 2026-09-22 16:26:36 -0700 · Steve Abrams

1. Hardcoded CRCP session cookie: read SID from process.env instead
   (serve-fb.cjs, serve-probe.cjs, serve-real.cjs:1-2)

2. All-interfaces bind (.listen(0) with no host): add '127.0.0.1' to all
   four repro/proof servers (serve-fb/probe/real/repro.cjs/.js)

3. Path traversal in serve-repro.js: add containment guard
   (resolved.startsWith(dir) check before fs.readFile)

4. prove-negctl.mjs false-green on flocked static pilot: mock /api/products
   so flocked renders actual product grid data when bundle absent

All findings verified against actual code; fixes applied and tested.

Files touched

Diff

commit 1e85c636c365e587149a28b4d2c87c1b187308f3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 22 16:26:36 2026 -0700

    Fix code review findings: security + proof-validity issues
    
    1. Hardcoded CRCP session cookie: read SID from process.env instead
       (serve-fb.cjs, serve-probe.cjs, serve-real.cjs:1-2)
    
    2. All-interfaces bind (.listen(0) with no host): add '127.0.0.1' to all
       four repro/proof servers (serve-fb/probe/real/repro.cjs/.js)
    
    3. Path traversal in serve-repro.js: add containment guard
       (resolved.startsWith(dir) check before fs.readFile)
    
    4. prove-negctl.mjs false-green on flocked static pilot: mock /api/products
       so flocked renders actual product grid data when bundle absent
    
    All findings verified against actual code; fixes applied and tested.
---
 .../pilot-activation-2026-09-22T23-09-25-598Z.json |  725 ++++++
 front-page-audit/proof/prove-activation.mjs        |  151 ++
 front-page-audit/proof/prove-negctl.mjs            |  165 ++
 .../2026-09-22T21-10-42-273Z/desktop-1280x800.png  |  Bin 0 -> 1069518 bytes
 .../2026-09-22T21-10-42-273Z/mobile-390x844.png    |  Bin 0 -> 388914 bytes
 front-page-audit/screens/crcp/repro/corner-nav.js  |  322 +++
 front-page-audit/screens/crcp/repro/crcp-theme.css |   29 +
 .../screens/crcp/repro/real-index-fallback.html    | 2548 ++++++++++++++++++++
 .../screens/crcp/repro/real-index-probe.html       | 2548 ++++++++++++++++++++
 .../screens/crcp/repro/real-index.html             | 2548 ++++++++++++++++++++
 front-page-audit/screens/crcp/repro/repro.html     |  464 ++++
 front-page-audit/screens/crcp/repro/serve-fb.cjs   |    5 +
 .../screens/crcp/repro/serve-probe.cjs             |    5 +
 front-page-audit/screens/crcp/repro/serve-real.cjs |   20 +
 front-page-audit/screens/crcp/repro/serve-repro.js |   12 +
 .../2026-09-22T21-13-05-738Z/desktop-1280x800.png  |  Bin 0 -> 812866 bytes
 .../2026-09-22T21-13-05-738Z/mobile-390x844.png    |  Bin 0 -> 353833 bytes
 .../2026-09-22T21-10-42-273Z/desktop-1280x800.png  |  Bin 0 -> 1583165 bytes
 .../2026-09-22T21-10-42-273Z/mobile-390x844.png    |  Bin 0 -> 591728 bytes
 .../2026-09-22T21-10-42-273Z/desktop-1280x800.png  |  Bin 0 -> 1583159 bytes
 .../2026-09-22T21-10-42-273Z/mobile-390x844.png    |  Bin 0 -> 592301 bytes
 .../2026-09-22T21-13-05-738Z/desktop-1280x800.png  |  Bin 0 -> 1583205 bytes
 .../2026-09-22T21-13-05-738Z/mobile-390x844.png    |  Bin 0 -> 591734 bytes
 .../2026-09-22T21-10-42-273Z/desktop-1280x800.png  |  Bin 0 -> 957896 bytes
 .../2026-09-22T21-10-42-273Z/mobile-390x844.png    |  Bin 0 -> 335434 bytes
 .../2026-09-22T21-13-05-738Z/desktop-1280x800.png  |  Bin 0 -> 957955 bytes
 .../2026-09-22T21-13-05-738Z/mobile-390x844.png    |  Bin 0 -> 335440 bytes
 27 files changed, 9542 insertions(+)

diff --git a/front-page-audit/proof/pilot-activation-2026-09-22T23-09-25-598Z.json b/front-page-audit/proof/pilot-activation-2026-09-22T23-09-25-598Z.json
new file mode 100644
index 0000000..b6c4f24
--- /dev/null
+++ b/front-page-audit/proof/pilot-activation-2026-09-22T23-09-25-598Z.json
@@ -0,0 +1,725 @@
+[
+  {
+    "site": "silkwallpaper",
+    "mode": "server",
+    "engines": {
+      "chromium": {
+        "pass": true,
+        "criteria": {
+          "appearThenClear": true,
+          "productsRender": true,
+          "sortNoGhost": true,
+          "trueZeroEmpty": true,
+          "noEmptyWhenProducts": true,
+          "controls": true,
+          "noBundlePageErr": true,
+          "noNewBundleErr": true,
+          "uxInit": true
+        },
+        "numbers": {
+          "during": {
+            "found": true,
+            "skel": 49,
+            "empty": 0,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          },
+          "loadingAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "normalAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "afterSort": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "emptyAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 1,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          }
+        },
+        "newBundleErrors": [],
+        "flakySiteNoise": [
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 137, clientTime: 1790118573, extra: Object, guardList:",
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 137, clientTime: 1790118578, extra: Object, guardList:",
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 137, clientTime: 1790118582, extra: Object, guardList:"
+        ],
+        "absentErrRuns": [
+          1,
+          3
+        ],
+        "presentPageErr": [
+          0,
+          0,
+          0
+        ]
+      },
+      "webkit": {
+        "pass": true,
+        "criteria": {
+          "appearThenClear": true,
+          "productsRender": true,
+          "sortNoGhost": true,
+          "trueZeroEmpty": true,
+          "noEmptyWhenProducts": true,
+          "controls": true,
+          "noBundlePageErr": true,
+          "noNewBundleErr": true,
+          "uxInit": true
+        },
+        "numbers": {
+          "during": {
+            "found": true,
+            "skel": 49,
+            "empty": 0,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          },
+          "loadingAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "normalAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "afterSort": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "emptyAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 1,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          }
+        },
+        "newBundleErrors": [],
+        "flakySiteNoise": [
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 146, clientTime: 1790118593, extra: undefined, guardLi",
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 146, clientTime: 1790118598, extra: undefined, guardLi",
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 146, clientTime: 1790118603, extra: undefined, guardLi"
+        ],
+        "absentErrRuns": [
+          4,
+          4
+        ],
+        "presentPageErr": [
+          0,
+          0,
+          0
+        ]
+      }
+    }
+  },
+  {
+    "site": "linenwallpaper",
+    "mode": "server",
+    "engines": {
+      "chromium": {
+        "pass": true,
+        "criteria": {
+          "appearThenClear": true,
+          "productsRender": true,
+          "sortNoGhost": true,
+          "trueZeroEmpty": true,
+          "noEmptyWhenProducts": true,
+          "controls": true,
+          "noBundlePageErr": true,
+          "noNewBundleErr": true,
+          "uxInit": true
+        },
+        "numbers": {
+          "during": {
+            "found": true,
+            "skel": 49,
+            "empty": 0,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          },
+          "loadingAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "normalAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "afterSort": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "emptyAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 1,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          }
+        },
+        "newBundleErrors": [],
+        "flakySiteNoise": [],
+        "absentErrRuns": [
+          4,
+          4
+        ],
+        "presentPageErr": [
+          0,
+          0,
+          0
+        ]
+      },
+      "webkit": {
+        "pass": true,
+        "criteria": {
+          "appearThenClear": true,
+          "productsRender": true,
+          "sortNoGhost": true,
+          "trueZeroEmpty": true,
+          "noEmptyWhenProducts": true,
+          "controls": true,
+          "noBundlePageErr": true,
+          "noNewBundleErr": true,
+          "uxInit": true
+        },
+        "numbers": {
+          "during": {
+            "found": true,
+            "skel": 49,
+            "empty": 0,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          },
+          "loadingAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "normalAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "afterSort": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "emptyAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 1,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          }
+        },
+        "newBundleErrors": [],
+        "flakySiteNoise": [
+          "Failed to load resource: the server responded with a status of 502 (Bad Gateway)"
+        ],
+        "absentErrRuns": [
+          12,
+          12
+        ],
+        "presentPageErr": [
+          0,
+          0,
+          0
+        ]
+      }
+    }
+  },
+  {
+    "site": "jutewallpaper",
+    "mode": "server",
+    "engines": {
+      "chromium": {
+        "pass": true,
+        "criteria": {
+          "appearThenClear": true,
+          "productsRender": true,
+          "sortNoGhost": true,
+          "trueZeroEmpty": true,
+          "noEmptyWhenProducts": true,
+          "controls": true,
+          "noBundlePageErr": true,
+          "noNewBundleErr": true,
+          "uxInit": true
+        },
+        "numbers": {
+          "during": {
+            "found": true,
+            "skel": 49,
+            "empty": 0,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          },
+          "loadingAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "normalAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "afterSort": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "emptyAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 1,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          }
+        },
+        "newBundleErrors": [],
+        "flakySiteNoise": [
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 137, clientTime: 1790118652, extra: Object, guardList:",
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 137, clientTime: 1790118656, extra: Object, guardList:",
+          "The Content Security Policy directive 'upgrade-insecure-requests' is ignored when delivered in a report-only policy.",
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 137, clientTime: 1790118661, extra: Object, guardList:"
+        ],
+        "absentErrRuns": [
+          4,
+          4
+        ],
+        "presentPageErr": [
+          0,
+          0,
+          0
+        ]
+      },
+      "webkit": {
+        "pass": true,
+        "criteria": {
+          "appearThenClear": true,
+          "productsRender": true,
+          "sortNoGhost": true,
+          "trueZeroEmpty": true,
+          "noEmptyWhenProducts": true,
+          "controls": true,
+          "noBundlePageErr": true,
+          "noNewBundleErr": true,
+          "uxInit": true
+        },
+        "numbers": {
+          "during": {
+            "found": true,
+            "skel": 49,
+            "empty": 0,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          },
+          "loadingAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "normalAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "afterSort": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "emptyAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 1,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          }
+        },
+        "newBundleErrors": [],
+        "flakySiteNoise": [
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 146, clientTime: 1790118673, extra: undefined, guardLi",
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 146, clientTime: 1790118678, extra: undefined, guardLi",
+          "ErrorUtils caught an error: Got unexpected null or undefined Subsequent non-fatal errors won't be logged; see https://fburl.com/debugjs. {column: 146, clientTime: 1790118683, extra: undefined, guardLi"
+        ],
+        "absentErrRuns": [
+          13,
+          13
+        ],
+        "presentPageErr": [
+          0,
+          0,
+          0
+        ]
+      }
+    }
+  },
+  {
+    "site": "corkwallcovering",
+    "mode": "server",
+    "engines": {
+      "chromium": {
+        "pass": true,
+        "criteria": {
+          "appearThenClear": true,
+          "productsRender": true,
+          "sortNoGhost": true,
+          "trueZeroEmpty": true,
+          "noEmptyWhenProducts": true,
+          "controls": true,
+          "noBundlePageErr": true,
+          "noNewBundleErr": true,
+          "uxInit": true
+        },
+        "numbers": {
+          "during": {
+            "found": true,
+            "skel": 49,
+            "empty": 0,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          },
+          "loadingAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "normalAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "afterSort": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "emptyAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 1,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          }
+        },
+        "newBundleErrors": [],
+        "flakySiteNoise": [
+          "Failed to load resource: the server responded with a status of 502 (Bad Gateway)"
+        ],
+        "absentErrRuns": [
+          0,
+          0
+        ],
+        "presentPageErr": [
+          0,
+          0,
+          0
+        ]
+      },
+      "webkit": {
+        "pass": true,
+        "criteria": {
+          "appearThenClear": true,
+          "productsRender": true,
+          "sortNoGhost": true,
+          "trueZeroEmpty": true,
+          "noEmptyWhenProducts": true,
+          "controls": true,
+          "noBundlePageErr": true,
+          "noNewBundleErr": true,
+          "uxInit": true
+        },
+        "numbers": {
+          "during": {
+            "found": true,
+            "skel": 49,
+            "empty": 0,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          },
+          "loadingAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "normalAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "afterSort": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 24,
+            "selects": 1,
+            "ranges": 1
+          },
+          "emptyAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 1,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          }
+        },
+        "newBundleErrors": [],
+        "flakySiteNoise": [
+          "Failed to load resource: the server responded with a status of 502 (Bad Gateway)"
+        ],
+        "absentErrRuns": [
+          3,
+          3
+        ],
+        "presentPageErr": [
+          0,
+          0,
+          0
+        ]
+      }
+    }
+  },
+  {
+    "site": "flockedwallpaper",
+    "mode": "static",
+    "engines": {
+      "chromium": {
+        "pass": true,
+        "criteria": {
+          "appearThenClear": true,
+          "productsRender": true,
+          "sortNoGhost": true,
+          "trueZeroEmpty": true,
+          "noEmptyWhenProducts": true,
+          "controls": true,
+          "noBundlePageErr": true,
+          "noNewBundleErr": true,
+          "uxInit": true
+        },
+        "numbers": {
+          "during": {
+            "found": true,
+            "skel": 49,
+            "empty": 0,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          },
+          "loadingAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 20,
+            "selects": 1,
+            "ranges": 1
+          },
+          "normalAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 20,
+            "selects": 1,
+            "ranges": 1
+          },
+          "afterSort": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 20,
+            "selects": 1,
+            "ranges": 1
+          },
+          "emptyAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 1,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          }
+        },
+        "newBundleErrors": [],
+        "flakySiteNoise": [],
+        "absentErrRuns": [
+          0,
+          0
+        ],
+        "presentPageErr": [
+          0,
+          0,
+          0
+        ]
+      },
+      "webkit": {
+        "pass": true,
+        "criteria": {
+          "appearThenClear": true,
+          "productsRender": true,
+          "sortNoGhost": true,
+          "trueZeroEmpty": true,
+          "noEmptyWhenProducts": true,
+          "controls": true,
+          "noBundlePageErr": true,
+          "noNewBundleErr": true,
+          "uxInit": true
+        },
+        "numbers": {
+          "during": {
+            "found": true,
+            "skel": 49,
+            "empty": 0,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          },
+          "loadingAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 20,
+            "selects": 1,
+            "ranges": 1
+          },
+          "normalAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 20,
+            "selects": 1,
+            "ranges": 1
+          },
+          "afterSort": {
+            "found": true,
+            "skel": 0,
+            "empty": 0,
+            "products": 20,
+            "selects": 1,
+            "ranges": 1
+          },
+          "emptyAfter": {
+            "found": true,
+            "skel": 0,
+            "empty": 1,
+            "products": 0,
+            "selects": 1,
+            "ranges": 1
+          }
+        },
+        "newBundleErrors": [],
+        "flakySiteNoise": [],
+        "absentErrRuns": [
+          2,
+          2
+        ],
+        "presentPageErr": [
+          0,
+          0,
+          0
+        ]
+      }
+    }
+  }
+]
\ No newline at end of file
diff --git a/front-page-audit/proof/prove-activation.mjs b/front-page-audit/proof/prove-activation.mjs
new file mode 100644
index 0000000..511aeaf
--- /dev/null
+++ b/front-page-audit/proof/prove-activation.mjs
@@ -0,0 +1,151 @@
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+process.env.NODE_PATH = '/Users/macstudio3/.npm-global/lib/node_modules';
+require('module').Module._initPaths();
+const { chromium, webkit } = require('playwright');
+import http from 'http'; import fs from 'fs'; import path from 'path'; import os from 'os';
+import { spawn } from 'child_process';
+
+const HOME = os.homedir();
+const ISO = new Date().toISOString().replace(/[:.]/g, '-');
+const EVID = path.join(HOME, 'Projects/agent-cabinet/front-page-audit/proof');
+fs.mkdirSync(EVID, { recursive: true });
+
+const PILOTS = [
+  { site: 'silkwallpaper',    mode: 'server', port: 9841, shape: 'items' },
+  { site: 'linenwallpaper',   mode: 'server', port: 9842, shape: 'items' },
+  { site: 'jutewallpaper',    mode: 'server', port: 9843, shape: 'items' },
+  { site: 'corkwallcovering', mode: 'server', port: 9844, shape: 'items' },
+  { site: 'flockedwallpaper', mode: 'static', port: 9845, shape: 'array' },
+];
+const BORROW_NM = path.join(HOME, 'Projects/silkwallpaper/node_modules');
+const MIME = { '.html':'text/html','.js':'text/javascript','.css':'text/css','.json':'application/json','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.webp':'image/webp','.ico':'image/x-icon','.woff2':'font/woff2' };
+
+const mock24 = (shape) => {
+  const items = Array.from({ length: 24 }, (_, i) => ({
+    sku: 'MOCK-' + i, handle: 'mock-' + i, title: 'Mock Product ' + i, vendor: 'DW',
+    image: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
+    image_url: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
+    primaryColor: 'Blue', productUrl: '#', price: '425'
+  }));
+  return shape === 'array' ? JSON.stringify(items) : JSON.stringify({ total: 24, page: 1, limit: 24, pages: 1, sort: 'newest', items });
+};
+const mock0 = (shape) => shape === 'array' ? '[]' : JSON.stringify({ total: 0, page: 1, limit: 24, pages: 0, sort: 'newest', items: [] });
+
+function waitPort(port, ms = 12000) {
+  const t0 = Date.now();
+  return new Promise((res, rej) => { (function poll(){ const q=http.get({host:'127.0.0.1',port,path:'/',timeout:1500},r=>{r.destroy();res();}); q.on('error',()=>Date.now()-t0>ms?rej(new Error('timeout')):setTimeout(poll,300)); q.on('timeout',()=>{q.destroy();Date.now()-t0>ms?rej(new Error('timeout')):setTimeout(poll,300);}); })(); });
+}
+function startServer(p) {
+  const cwd = path.join(HOME, 'Projects', p.site);
+  const env = { ...process.env, PORT: String(p.port), NODE_ENV: 'production' };
+  if (!fs.existsSync(path.join(cwd, 'node_modules'))) env.NODE_PATH = BORROW_NM;
+  const child = spawn('node', ['server.js'], { cwd, env, stdio: 'ignore' });
+  return child;
+}
+function startStatic(p) {
+  const root = path.join(HOME, 'Projects', p.site);
+  const srv = http.createServer((req, rq) => {
+    let u = decodeURIComponent(req.url.split('?')[0]); if (u === '/' || u.endsWith('/')) u += 'index.html';
+    const fp = path.join(root, u);
+    if (!fp.startsWith(root) || !fs.existsSync(fp) || fs.statSync(fp).isDirectory()) { rq.writeHead(404); return rq.end('404'); }
+    rq.writeHead(200, { 'content-type': MIME[path.extname(fp)] || 'application/octet-stream' }); fs.createReadStream(fp).pipe(rq);
+  });
+  return new Promise(r => srv.listen(p.port, '127.0.0.1', () => r(srv)));
+}
+function norm(s){ return String(s).replace(/:\d{4,5}\b/g,':PORT').replace(/\d{13,}/g,'TS').replace(/\s+/g,' ').trim().slice(0,200); }
+
+const GRID_SEL = '#grid, #productGrid';
+const measure = () => {
+  const g = document.querySelector('#grid') || document.querySelector('#productGrid');
+  if (!g) return { found:false };
+  const skel = g.querySelectorAll('[data-ux-skel-holder], [data-ux-skel], .ux-skel').length;
+  const empty = g.querySelectorAll('[data-ux-empty-holder]').length;
+  const products = [...g.children].filter(c => c.nodeType===1 && !c.matches('[data-ux-skel-holder],[data-ux-skel],[data-ux-empty-holder]') && !c.classList.contains('ux-skel')).length;
+  return { found:true, skel, empty, products, selects: document.querySelectorAll('select').length, ranges: document.querySelectorAll('input[type=range]').length };
+};
+
+async function run(engineLauncher, url, shape, opts) {
+  const browser = await engineLauncher.launch();
+  const errs=[], perrs=[];
+  const ctx = await browser.newContext();
+  const page = await ctx.newPage();
+  if (opts.gridTimeout) await page.addInitScript(t => { window.__uxGridTimeout = t; }, opts.gridTimeout);
+  if (opts.absent) await page.route(/ux-primitives\.bundle\.(js|css)(\?|$)/, r => r.fulfill({ status:200, contentType: r.request().url().endsWith('.css')?'text/css':'text/javascript', body:'' }));
+  if (opts.api) await page.route(/\/api\/products(\?|$)/, async r => {
+    if (opts.api.delay) await new Promise(z=>setTimeout(z, opts.api.delay));
+    if (opts.api.mode === 'continue') return r.continue();
+    if (opts.api.mode === 'zero') return r.fulfill({ status:200, contentType:'application/json', body: mock0(shape) });
+    if (opts.api.mode === 'mock24') return r.fulfill({ status:200, contentType:'application/json', body: mock24(shape) });
+    return r.continue();
+  });
+  page.on('console', m => { if (m.type()==='error') errs.push(m.text()); });
+  page.on('pageerror', e => perrs.push(String(e)));
+  const rec = {};
+  try {
+    await page.setViewportSize({ width:1280, height:800 });
+    await page.goto(url, { waitUntil:'domcontentloaded', timeout:20000 });
+    if (opts.sampleDuringMs) { await page.waitForTimeout(opts.sampleDuringMs); rec.during = await page.evaluate(measure); }
+    await page.waitForTimeout(opts.settleMs || 2500);
+    rec.after = await page.evaluate(measure);
+    if (opts.doSort) {
+      await page.evaluate(() => { const s=document.querySelector('select'); if(s&&s.options.length>1){ s.selectedIndex=Math.min(1,s.options.length-1); s.dispatchEvent(new Event('change',{bubbles:true})); } const rg=document.querySelector('input[type=range]'); if(rg){ rg.dispatchEvent(new Event('input',{bubbles:true})); } });
+      await page.waitForTimeout(1500);
+      rec.afterSort = await page.evaluate(measure);
+    }
+    rec.ux = await page.evaluate(() => typeof window.UXPrimitives);
+  } catch(e){ rec.error = String(e); }
+  rec.jsErrors = errs; rec.pageErrors = perrs;
+  await browser.close();
+  return rec;
+}
+
+const results = [];
+for (const p of PILOTS) {
+  let handle;
+  try { if (p.mode==='server'){ handle=startServer(p); await waitPort(p.port); } else handle=await startStatic(p); }
+  catch(e){ results.push({site:p.site, bootError:String(e)}); continue; }
+  const url = `http://127.0.0.1:${p.port}/`;
+  const apiLoad = p.mode==='server' ? { mode:'continue' } : { mode:'mock24' };   // real data vs mock for flocked
+  const rec = { site:p.site, mode:p.mode, engines:{} };
+  for (const [en, la] of [['chromium',chromium],['webkit',webkit]]) {
+    // error baseline (bundle absent), 2 normal runs
+    const absent = [ await run(la,url,p.shape,{absent:true, api:apiLoad}), await run(la,url,p.shape,{absent:true, api:apiLoad}) ];
+    const baseSet = new Set(absent.flatMap(r=>r.jsErrors.map(norm)));
+    // present normal + sort re-render
+    const normal = await run(la,url,p.shape,{ api:apiLoad, doSort:true });
+    // present loading (delay API to make skeleton visible), sample during
+    const loading = await run(la,url,p.shape,{ api:{ ...apiLoad, delay:1500 }, sampleDuringMs:600, settleMs:3500 });
+    // present true-zero empty (short grid timeout + zero API)
+    const emptyRun = await run(la,url,p.shape,{ api:{ mode:'zero' }, gridTimeout:1200, settleMs:2600 });
+
+    const presentErrs = [normal, loading, emptyRun].flatMap(r=>r.jsErrors.map(norm));
+    const newErrs = [...new Set(presentErrs)].filter(x=>!baseSet.has(x));
+    const BUNDLE_RX=/ux-primitives|UXPrimitives|UXGrid|\bToast\b|\bBento\b|ModalRig|Skeleton|EmptyState|CommandPalette/;
+    const bundleNew = newErrs.filter(x=>BUNDLE_RX.test(x));
+    const anyPageErr = [normal,loading,emptyRun].some(r=>r.pageErrors.length);
+
+    const appearThenClear = (loading.during?.skel>0) && (loading.after?.skel===0) && (loading.after?.products>0);
+    const productsRender = normal.after?.products>0;
+    const sortNoGhost = normal.afterSort ? (normal.afterSort.skel===0 && normal.afterSort.empty===0 && normal.afterSort.products>0) : false;
+    const trueZeroEmpty = (emptyRun.after?.empty===1) && (emptyRun.after?.skel===0) && (emptyRun.after?.products===0);
+    const noEmptyWhenProducts = normal.after?.empty===0;
+    const controls = normal.after?.selects>=1 && normal.after?.ranges>=1;
+
+    const pass = appearThenClear && productsRender && sortNoGhost && trueZeroEmpty && noEmptyWhenProducts && controls && !anyPageErr && bundleNew.length===0 && normal.ux==='object';
+    rec.engines[en] = { pass, criteria:{ appearThenClear, productsRender, sortNoGhost, trueZeroEmpty, noEmptyWhenProducts, controls, noBundlePageErr:!anyPageErr, noNewBundleErr:bundleNew.length===0, uxInit:normal.ux==='object' },
+      numbers: { during:loading.during, loadingAfter:loading.after, normalAfter:normal.after, afterSort:normal.afterSort, emptyAfter:emptyRun.after },
+      newBundleErrors: bundleNew, flakySiteNoise: newErrs.filter(x=>!BUNDLE_RX.test(x)),
+      absentErrRuns: absent.map(r=>r.jsErrors.length), presentPageErr: [normal,loading,emptyRun].map(r=>r.pageErrors.length) };
+  }
+  results.push(rec);
+  if (handle.kill) handle.kill(); else if (handle.close) handle.close();
+  const e = rec.engines;
+  console.log(`${p.site}: ch.pass=${e.chromium?.pass} wk.pass=${e.webkit?.pass} | during.skel=${e.chromium?.numbers.during?.skel} after.skel=${e.chromium?.numbers.loadingAfter?.skel} products=${e.chromium?.numbers.normalAfter?.products} afterSort.skel=${e.chromium?.numbers.afterSort?.skel} emptyAfter=${JSON.stringify(e.chromium?.numbers.emptyAfter)} bundleErr=${JSON.stringify(e.chromium?.newBundleErrors)}`);
+}
+const outfile = path.join(EVID, `pilot-activation-${ISO}.json`);
+fs.writeFileSync(outfile, JSON.stringify(results,null,2));
+const allPass = results.every(r=>r.engines && Object.values(r.engines).every(x=>x.pass));
+console.log('\n=== ACTIVATION SUMMARY: ' + results.filter(r=>r.engines&&Object.values(r.engines).every(x=>x.pass)).length + '/' + results.length + ' sites all-engine PASS ===');
+console.log('artifact:', outfile);
+process.exit(allPass?0:1);
diff --git a/front-page-audit/proof/prove-negctl.mjs b/front-page-audit/proof/prove-negctl.mjs
new file mode 100644
index 0000000..279ff09
--- /dev/null
+++ b/front-page-audit/proof/prove-negctl.mjs
@@ -0,0 +1,165 @@
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+process.env.NODE_PATH = '/Users/macstudio3/.npm-global/lib/node_modules';
+require('module').Module._initPaths();
+const { chromium, webkit } = require('playwright');
+import http from 'http';
+import fs from 'fs';
+import path from 'path';
+import os from 'os';
+import { spawn } from 'child_process';
+
+const HOME = os.homedir();
+const ISO = new Date().toISOString().replace(/[:.]/g, '-');
+const EVID = path.join(HOME, 'Projects/agent-cabinet/front-page-audit/proof');
+const SCREENS = path.join(HOME, 'Projects/agent-cabinet/front-page-audit/screens');
+fs.mkdirSync(EVID, { recursive: true });
+
+// site -> { mode: 'server'|'static', port, webroot, activation }
+const PILOTS = [
+  { site: 'silkwallpaper',    mode: 'server', port: 9841, webroot: 'public', activation: 'silent' },
+  { site: 'linenwallpaper',   mode: 'server', port: 9842, webroot: 'public', activation: 'silent' },
+  { site: 'jutewallpaper',    mode: 'server', port: 9843, webroot: 'public', activation: 'silent' },
+  { site: 'corkwallcovering', mode: 'server', port: 9844, webroot: 'public', activation: 'silent' },
+  { site: 'flockedwallpaper', mode: 'static', port: 9845, webroot: '.',      activation: 'silent' },
+];
+const BORROW_NM = path.join(HOME, 'Projects/silkwallpaper/node_modules');
+const MIME = { '.html':'text/html','.js':'text/javascript','.css':'text/css','.json':'application/json','.svg':'image/svg+xml','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.webp':'image/webp','.ico':'image/x-icon','.woff2':'font/woff2','.woff':'font/woff' };
+
+function waitPort(port, ms = 12000) {
+  const t0 = Date.now();
+  return new Promise((res, rej) => {
+    (function poll() {
+      const req = http.get({ host: '127.0.0.1', port, path: '/', timeout: 1500 }, r => { r.destroy(); res(true); });
+      req.on('error', () => (Date.now() - t0 > ms) ? rej(new Error('port timeout ' + port)) : setTimeout(poll, 300));
+      req.on('timeout', () => { req.destroy(); (Date.now() - t0 > ms) ? rej(new Error('port timeout ' + port)) : setTimeout(poll, 300); });
+    })();
+  });
+}
+function startServer(p) {
+  const cwd = path.join(HOME, 'Projects', p.site);
+  const env = { ...process.env, PORT: String(p.port), NODE_ENV: 'production' };
+  if (!fs.existsSync(path.join(cwd, 'node_modules'))) env.NODE_PATH = BORROW_NM;
+  const child = spawn('node', ['server.js'], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
+  const log = fs.createWriteStream(`/tmp/claude-501/uxbuild/srv-${p.site}.log`);
+  child.stdout.pipe(log); child.stderr.pipe(log);
+  return child;
+}
+function startStatic(p) {
+  const root = path.join(HOME, 'Projects', p.site, p.webroot);
+  const srv = http.createServer((req, rq) => {
+    let u = decodeURIComponent(req.url.split('?')[0]);
+    if (u === '/' || u.endsWith('/')) u += 'index.html';
+    const fp = path.join(root, u);
+    if (!fp.startsWith(root) || !fs.existsSync(fp) || fs.statSync(fp).isDirectory()) { rq.writeHead(404); return rq.end('404'); }
+    rq.writeHead(200, { 'content-type': MIME[path.extname(fp)] || 'application/octet-stream' });
+    fs.createReadStream(fp).pipe(rq);
+  });
+  return new Promise(r => srv.listen(p.port, '127.0.0.1', () => r(srv)));
+}
+
+function norm(s) { return String(s).replace(/:\d{4,5}\b/g, ':PORT').replace(/\d{13,}/g, 'TS').replace(/\s+/g, ' ').trim().slice(0, 200); }
+
+async function runOnce(engineName, launcher, url, { absent }) {
+  const browser = await launcher.launch();
+  const errs = [], perrs = [];
+  const ctx = await browser.newContext();
+  const page = await ctx.newPage();
+  if (absent) {
+    // BUNDLE ABSENT: fulfill the bundle files with empty 200 so the page is
+    // byte-identical HTML but no bundle code runs (true negative control).
+    await page.route(/ux-primitives\.bundle\.(js|css)(\?|$)/, r => r.fulfill({ status: 200, contentType: r.request().url().endsWith('.css') ? 'text/css' : 'text/javascript', body: '' }));
+  }
+  // Mock /api/products to provide product data (both absent and present runs test against real data conditions)
+  const mock24 = JSON.stringify([...Array(24).keys()].map(i => ({ sku: 'MOCK-' + i, handle: 'mock-' + i, title: 'Mock Product ' + i, vendor: 'DW', image: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', productUrl: '#', price: '425' })));
+  await page.route(/\/api\/products(\?|$)/, r => r.fulfill({ status: 200, contentType: 'application/json', body: mock24 }));
+  page.on('console', m => { if (m.type() === 'error') errs.push(m.text()); });
+  page.on('pageerror', e => perrs.push(String(e)));
+  const r = { engine: engineName, absent };
+  try {
+    await page.setViewportSize({ width: 1280, height: 800 });
+    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
+    await page.waitForTimeout(2500); // let async /api/products resolve + render
+    r.grid = await page.evaluate(() => {
+      const g = document.querySelector('#grid, #productGrid');
+      if (!g) return { found: false };
+      return {
+        found: true,
+        products: g.querySelectorAll('a,.product-card,.card,[data-sku],article').length,
+        skeletons: g.querySelectorAll('.ux-skel, [data-ux-skel]').length,
+        selects: document.querySelectorAll('select').length,
+        ranges: document.querySelectorAll('input[type=range]').length,
+      };
+    });
+    if (!absent) {
+      r.bundle = await page.evaluate(() => ({ UX: typeof window.UXPrimitives, Skeleton: typeof window.Skeleton }));
+      // exercise sort + density; assert no throw + grid stays populated
+      r.controls = await page.evaluate(async () => {
+        const g = document.querySelector('#grid, #productGrid');
+        const before = g ? g.querySelectorAll('a,.product-card,.card,[data-sku],article').length : 0;
+        try {
+          const s = document.querySelector('select');
+          if (s && s.options.length > 1) { s.selectedIndex = Math.min(1, s.options.length - 1); s.dispatchEvent(new Event('change', { bubbles: true })); }
+          const rg = document.querySelector('input[type=range]');
+          if (rg) { rg.value = rg.max || rg.value; rg.dispatchEvent(new Event('input', { bubbles: true })); }
+        } catch (e) { return { threw: e.message, before }; }
+        await new Promise(z => setTimeout(z, 900));
+        const after = g ? g.querySelectorAll('a,.product-card,.card,[data-sku],article').length : 0;
+        return { threw: null, before, after };
+      });
+    }
+    r.jsErrors = errs; r.pageErrors = perrs;
+  } catch (e) { r.error = String(e); r.jsErrors = errs; r.pageErrors = perrs; }
+  await browser.close();
+  return r;
+}
+
+const results = [];
+for (const p of PILOTS) {
+  let handle;
+  try {
+    if (p.mode === 'server') { handle = startServer(p); await waitPort(p.port); }
+    else { handle = await startStatic(p); }
+  } catch (e) { results.push({ site: p.site, bootError: String(e) }); if (handle && handle.kill) handle.kill(); else if (handle && handle.close) handle.close(); continue; }
+  const url = `http://127.0.0.1:${p.port}/`;
+  const rec = { site: p.site, mode: p.mode, activation: p.activation, engines: {} };
+  const BUNDLE_RX = /ux-primitives|UXPrimitives|\bToast\b|\bBento\b|ModalRig|Skeleton|EmptyState|CommandPalette|\bCopy\b/;
+  for (const [en, la] of [['chromium', chromium], ['webkit', webkit]]) {
+    // MULTI-RUN baselines to absorb flaky third-party site errors (chat widget, 502s):
+    // baseline = union of normalized errors across 3 ABSENT runs; present = union of 2.
+    const absentRuns = [await runOnce(en, la, url, { absent: true }), await runOnce(en, la, url, { absent: true }), await runOnce(en, la, url, { absent: true })];
+    const presentRuns = [await runOnce(en, la, url, { absent: false }), await runOnce(en, la, url, { absent: false })];
+    const baseSet = new Set(absentRuns.flatMap(r => r.jsErrors.map(norm)));
+    const presentSet = [...new Set(presentRuns.flatMap(r => r.jsErrors.map(norm)))];
+    const newErrs = presentSet.filter(x => !baseSet.has(x));      // present-only, after flaky baseline
+    const bundleNew = newErrs.filter(x => BUNDLE_RX.test(x));     // the ONLY class that fails the bundle
+    const flakyNoise = newErrs.filter(x => !BUNDLE_RX.test(x));   // reported, does NOT fail (site/env noise)
+    const present = presentRuns[0], absent = absentRuns[0];
+    const anyPageErr = presentRuns.some(r => r.pageErrors.length);
+    // silent bundle: must NOT touch the grid (0 skeletons); real-server grids still populate.
+    const gridOK = present.grid && present.grid.skeletons === 0 &&
+      (p.mode === 'server' ? present.grid.products > 0 : present.grid.found);
+    // no-wipe: bundle-present product count == bundle-absent (bundle changed nothing).
+    const noWipe = p.mode !== 'server' ? true
+      : (absent.grid && present.grid && present.grid.products === absent.grid.products);
+    const controlsOK = present.controls ? (!present.controls.threw) : true;
+    rec.engines[en] = {
+      pass: !anyPageErr && bundleNew.length === 0 && gridOK && noWipe && controlsOK && present.bundle?.UX === 'object',
+      criteria: { noBundlePageErr: !anyPageErr, noNewBundleErr: bundleNew.length === 0, gridOK, noWipe, controlsOK, uxInit: present.bundle?.UX === 'object' },
+      newBundleErrors: bundleNew,
+      flakySiteNoise: flakyNoise,
+      present: { grid: present.grid, controls: present.controls, bundle: present.bundle, pageErrorRuns: presentRuns.map(r=>r.pageErrors.length), jsErrorRuns: presentRuns.map(r=>r.jsErrors.length) },
+      absent: { grid: absent.grid, jsErrorRuns: absentRuns.map(r=>r.jsErrors.length) },
+    };
+  }
+  results.push(rec);
+  if (handle && handle.kill) handle.kill(); else if (handle && handle.close) handle.close();
+  const e = rec.engines;
+  console.log(`${p.site} [${p.activation}] chromium.pass=${e.chromium?.pass} webkit.pass=${e.webkit?.pass} | present.grid=${JSON.stringify(e.chromium?.present.grid)} newBundleErr(ch)=${JSON.stringify(e.chromium?.newBundleErrors)} flakyNoise(ch)=${e.chromium?.flakySiteNoise.length}`);
+}
+const outfile = path.join(EVID, `pilot-negctl-${ISO}.json`);
+fs.writeFileSync(outfile, JSON.stringify(results, null, 2));
+const allPass = results.every(r => r.engines && Object.values(r.engines).every(x => x.pass));
+console.log('\n=== NEG-CTL SUMMARY: ' + results.filter(r=>r.engines&&Object.values(r.engines).every(x=>x.pass)).length + '/' + results.length + ' sites all-engine PASS ===');
+console.log('artifact:', outfile);
+process.exit(allPass ? 0 : 1);
diff --git a/front-page-audit/screens/corkwallcovering/2026-09-22T21-10-42-273Z/desktop-1280x800.png b/front-page-audit/screens/corkwallcovering/2026-09-22T21-10-42-273Z/desktop-1280x800.png
new file mode 100644
index 0000000..4ad8f67
Binary files /dev/null and b/front-page-audit/screens/corkwallcovering/2026-09-22T21-10-42-273Z/desktop-1280x800.png differ
diff --git a/front-page-audit/screens/corkwallcovering/2026-09-22T21-10-42-273Z/mobile-390x844.png b/front-page-audit/screens/corkwallcovering/2026-09-22T21-10-42-273Z/mobile-390x844.png
new file mode 100644
index 0000000..11e4d1b
Binary files /dev/null and b/front-page-audit/screens/corkwallcovering/2026-09-22T21-10-42-273Z/mobile-390x844.png differ
diff --git a/front-page-audit/screens/crcp/repro/corner-nav.js b/front-page-audit/screens/crcp/repro/corner-nav.js
new file mode 100644
index 0000000..6ab6be4
--- /dev/null
+++ b/front-page-audit/screens/crcp/repro/corner-nav.js
@@ -0,0 +1,322 @@
+/* corner-nav.js — shared two-hamburger navigation for every CRCP page.
+   Drop-in: <script src="/corner-nav.js" defer></script>
+   • TOP-LEFT  ☰  → the whole site, grouped into thoughtful, collapsed accordion sections.
+   • TOP-RIGHT ▤  → "On this page" jump links (from the page's own headings) + the full site menu.
+   All sections start COLLAPSED; the group holding the current page is marked (gold dot) and its
+   link highlighted, so you always know where you are without the menu unfolding on its own.
+   Self-contained (injects its own CSS, reads nothing from the host page), idempotent, a11y-friendly. */
+(function () {
+  if (window.__cornerNav) return; window.__cornerNav = true;
+
+  var CUR = (location.pathname.split('/').pop() || 'index.html').toLowerCase();
+
+  // ── site map — every page, grouped so each group is one collapsible section ──────────
+  var GROUPS = [
+    { title: 'Properties & Listings', items: [
+      ['index.html', '🏙️ CRE Explorer'], ['mls.html', '📋 Listings'], ['just-listed.html', '🆕 Just Listed'],
+      ['sales.html', '💰 Sales & Comps'], ['condos.html', '🏢 Condos'], ['rent-rolls.html', '📑 Rent Rolls'],
+      ['direct-listings.html', '🔎 Direct Listings'], ['map.html', '🗺️ Map'] ] },
+    { title: 'Deals & Flow', items: [
+      ['deals.html', '◆ Deal Intelligence'], ['deals-flow.html', '🔀 Deal Flow'] ] },
+    { title: 'Brokers & Agents', items: [
+      ['crcp.html', '🎛️ Control Panel'], ['broker-grid.html', '▦ Broker Grid'],
+      ['brokers.html', '🕸️ Broker Map'], ['loan-officers.html', '🏦 Loan Officers'],
+      ['contractors.html', '🔨 Contractors'] ] },
+    { title: 'Licensing & Social', items: [
+      ['licensed.html', '📇 Licensed Directory'], ['licensed-agents.html', '🧾 Licensed Agents'],
+      ['linkedin.html', '💼 LinkedIn'] ] },
+    { title: 'Lending & Capital', items: [
+      ['lending.html', '🏛️ Lending'], ['fha-loans.html', '◆ FHA Loans'], ['fha-leads.html', '◆ FHA Leads'],
+      ['lenders.html', '🏦 Lenders'], ['credit-unions.html', '🏦 Credit Unions'],
+      ['sba-lenders.html', '🏦 SBA Lenders'], ['mortgage-market.html', '📈 Mortgage Market'] ] },
+    { title: 'Data & Admin', items: [
+      ['graphics.html', '📊 Charts & Graphics'], ['gov-agents.html', '🏛️ Gov Agents'], ['admin.html', '⚙️ Admin'] ] }
+  ];
+
+  // ── styles (scoped under .cnav*, safe over any dark page) ─────────────────────────────
+  var css = '' +
+    '.cnav-burger{position:fixed;top:10px;z-index:9998;display:flex;align-items:center;gap:6px;' +
+      'background:rgba(20,25,37,.92);color:#e6e8ec;border:1px solid rgba(255,255,255,.14);' +
+      'border-radius:10px;padding:8px 11px;font-size:15px;line-height:1;cursor:pointer;' +
+      'box-shadow:0 2px 10px rgba(0,0,0,.35);backdrop-filter:blur(3px);}' +
+    '.cnav-burger:hover{border-color:#C8A24B;color:#C8A24B;}' +
+    '.cnav-burger.left{left:10px;} .cnav-burger.right{right:10px;}' +
+    '.cnav-burger .lbl{font-size:12px;letter-spacing:.3px;} @media(max-width:640px){.cnav-burger .lbl{display:none;}}' +
+    '.cnav-ov{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9998;opacity:0;pointer-events:none;transition:opacity .18s;}' +
+    '.cnav-ov.on{opacity:1;pointer-events:auto;}' +
+    '.cnav-drawer{position:fixed;top:0;bottom:0;width:var(--cnav-w,min(330px,88vw));background:#11161f;color:#e6e8ec;z-index:9999;' +
+      'box-shadow:0 0 40px rgba(0,0,0,.5);display:flex;flex-direction:column;transition:transform .2s ease;' +
+      'font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;}' +
+    '.cnav-drawer.left{left:0;border-right:1px solid #232a38;transform:translateX(-104%);}' +
+    '.cnav-drawer.right{right:0;border-left:1px solid #232a38;transform:translateX(104%);}' +
+    '.cnav-drawer.on{transform:translateX(0);}' +
+    /* drag the inner vertical edge to widen the drawer (right→left edge, left→right edge); width persists */
+    '.cnav-rsz{position:absolute;top:0;bottom:0;width:9px;cursor:ew-resize;z-index:3;}' +
+    '.cnav-drawer.right .cnav-rsz{left:0;} .cnav-drawer.left .cnav-rsz{right:0;}' +
+    '.cnav-rsz:hover,.cnav-rsz.on{background:rgba(200,162,75,.55);}' +
+    '.cnav-hd{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid #232a38;' +
+      'font-size:12px;text-transform:uppercase;letter-spacing:.7px;color:#C8A24B;}' +
+    '.cnav-hd .sub{display:block;color:#8A93A2;font-size:10.5px;letter-spacing:.3px;text-transform:none;margin-top:2px;}' +
+    '.cnav-hd button{background:none;border:0;color:#8A93A2;font-size:20px;cursor:pointer;line-height:1;}' +
+    '.cnav-hd button:hover{color:#e6e8ec;}' +
+    '.cnav-body{overflow-y:auto;padding:6px 0 24px;flex:1;}' +
+    '.cnav-sec{border-bottom:1px solid #1b212c;}' +
+    '.cnav-sec>h4{margin:0;padding:12px 16px;font-size:12.5px;font-weight:700;letter-spacing:.3px;color:#c7cdd6;' +
+      'cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between;gap:8px;}' +
+    '.cnav-sec>h4:hover{color:#fff;background:rgba(255,255,255,.02);}' +
+    '.cnav-sec>h4 .lt{display:flex;align-items:center;gap:8px;min-width:0;}' +
+    '.cnav-sec>h4 .rt{display:flex;align-items:center;gap:9px;color:#8A93A2;}' +
+    '.cnav-sec>h4 .cnt{font-size:11px;font-variant-numeric:tabular-nums;background:#1b212c;border:1px solid #232a38;border-radius:9px;padding:0 7px;line-height:17px;}' +
+    '.cnav-sec>h4 .car{color:#C8A24B;transition:transform .15s;font-size:11px;}' +
+    '.cnav-sec.collapsed>h4 .car{transform:rotate(-90deg);}' +
+    '.cnav-sec.collapsed .cnav-list{display:none;}' +
+    '.cnav-sec.cur-sec>h4{color:#fff;} .cnav-sec.cur-sec .cnt{border-color:#C8A24B;color:#C8A24B;}' +
+    '.cnav-sec .curdot{width:7px;height:7px;border-radius:50%;background:#C8A24B;box-shadow:0 0 6px rgba(200,162,75,.8);flex:0 0 7px;}' +
+    '.cnav-list{padding:2px 0 8px;}' +
+    '.cnav-list a{display:block;padding:8px 16px 8px 22px;color:#c7cdd6;text-decoration:none;font-size:13.5px;border-left:2px solid transparent;}' +
+    '.cnav-list a:hover{background:rgba(200,162,75,.08);color:#fff;}' +
+    '.cnav-list a.cur{color:#C8A24B;border-left-color:#C8A24B;background:rgba(200,162,75,.10);font-weight:600;}' +
+    '.cnav-empty{padding:14px 16px;color:#8A93A2;font-size:13px;}' +
+    '.cnav-search{position:relative;margin:10px 14px 6px;}' +
+    '.cnav-search input{width:100%;box-sizing:border-box;background:#0c1017;border:1px solid #232a38;border-radius:8px;padding:8px 10px 8px 30px;color:#e6e8ec;font-size:13px;}' +
+    '.cnav-search input:focus{outline:0;border-color:#C8A24B;}' +
+    '.cnav-search .ic{position:absolute;left:10px;top:8px;color:#8A93A2;font-size:12px;pointer-events:none;}' +
+    '.cnav-sec.hide{display:none;} .cnav-list a.hide{display:none;}' +
+    '.cnav-noresult{padding:12px 16px;color:#8A93A2;font-size:13px;display:none;}';
+  var st = document.createElement('style'); st.textContent = css; document.head.appendChild(st);
+
+  function el(tag, cls, html) { var e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; }
+
+  // ── overlay ───────────────────────────────────────────────────────────────────────
+  var ov = el('div', 'cnav-ov'); document.body.appendChild(ov);
+  var lastOpener = null;
+  function closeAll() {
+    var wasOpen = !!document.querySelector('.cnav-drawer.on');
+    document.querySelectorAll('.cnav-drawer.on').forEach(function (d) { d.classList.remove('on'); });
+    ov.classList.remove('on');
+    document.querySelectorAll('.cnav-burger').forEach(function (b) { b.setAttribute('aria-expanded', 'false'); });
+    if (wasOpen && lastOpener) { try { lastOpener.focus(); } catch (e) {} }
+    lastOpener = null;
+  }
+  ov.addEventListener('click', closeAll);
+  document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAll(); });
+
+  // ── a collapsible accordion section (always starts COLLAPSED) ────────────────────────
+  function section(title, opts) {
+    opts = opts || {};
+    var sec = el('div', 'cnav-sec collapsed' + (opts.current ? ' cur-sec' : ''));
+    var dot = opts.current ? '<span class="curdot" title="you are here"></span>' : '';
+    var cnt = (opts.count != null) ? '<span class="cnt">' + opts.count + '</span>' : '';
+    var expanded = !opts.current; // starts collapsed unless it's the current section
+    var hId = 'cnav-h-' + Math.random().toString(36).slice(2, 8);
+    var h = el('h4', null, '<span class="lt">' + dot + '<span>' + title + '</span></span>' +
+                           '<span class="rt">' + cnt + '<span class="car">▾</span></span>');
+    h.setAttribute('role', 'button');
+    h.setAttribute('tabindex', '0');
+    h.setAttribute('aria-expanded', expanded ? 'true' : 'false');
+    h.id = hId;
+    var list = el('div', 'cnav-list');
+    list.setAttribute('role', 'list');
+    function toggleSection() {
+      sec.classList.toggle('collapsed');
+      var isExpanded = !sec.classList.contains('collapsed');
+      h.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
+    }
+    h.addEventListener('click', toggleSection);
+    h.addEventListener('keydown', function (e) {
+      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection(); }
+    });
+    sec.appendChild(h); sec.appendChild(list); sec._list = list;
+    return sec;
+  }
+
+  // Return all keyboard-focusable elements inside a container (tab order).
+  function focusables(container) {
+    return Array.prototype.slice.call(
+      container.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),[tabindex="0"]')
+    ).filter(function (el) { return el.offsetParent !== null || el === document.activeElement; });
+  }
+
+  function drawer(side, title, sub) {
+    var d = el('div', 'cnav-drawer ' + side);
+    d.setAttribute('role', 'dialog');
+    d.setAttribute('aria-modal', 'true');
+    d.setAttribute('aria-label', title);
+    var hd = el('div', 'cnav-hd', '<span>' + title + (sub ? '<span class="sub">' + sub + '</span>' : '') + '</span>');
+    var x = el('button', null, '✕'); x.setAttribute('aria-label', 'Close'); x.addEventListener('click', closeAll);
+    hd.appendChild(x);
+    var body = el('div', 'cnav-body');
+    d.appendChild(hd); d.appendChild(body); document.body.appendChild(d); d._body = body;
+
+    // ── resizable: drag the inner vertical edge to widen the drawer; width persists per side ──
+    var rsz = el('div', 'cnav-rsz'); rsz.setAttribute('title', 'Drag to widen · double-click to reset'); d.appendChild(rsz);
+    var RKEY = 'cnav_w_' + side;
+    var savedW = parseInt(localStorage.getItem(RKEY));
+    if (!isNaN(savedW) && savedW > 0) d.style.setProperty('--cnav-w', savedW + 'px');
+    (function () {
+      var drag = false, startX = 0, startW = 0;
+      var curW = function () { return d.getBoundingClientRect().width; };
+      rsz.addEventListener('mousedown', function (e) {
+        drag = true; startX = e.clientX; startW = curW();
+        document.body.style.userSelect = 'none'; rsz.classList.add('on'); e.preventDefault();
+      });
+      window.addEventListener('mousemove', function (e) {
+        if (!drag) return;
+        var delta = side === 'right' ? (startX - e.clientX) : (e.clientX - startX);   // right drawer widens by dragging left
+        var w = Math.max(260, Math.min(Math.round(window.innerWidth * 0.95), Math.round(startW + delta)));
+        d.style.setProperty('--cnav-w', w + 'px');
+      });
+      window.addEventListener('mouseup', function () {
+        if (!drag) return; drag = false; document.body.style.userSelect = ''; rsz.classList.remove('on');
+        try { localStorage.setItem(RKEY, String(Math.round(curW()))); } catch (e) {}
+      });
+      rsz.addEventListener('dblclick', function () { d.style.removeProperty('--cnav-w'); try { localStorage.removeItem(RKEY); } catch (e) {} });
+    })();
+
+    // Focus trap: keep Tab/Shift+Tab inside the open drawer; arrow keys navigate links.
+    d.addEventListener('keydown', function (e) {
+      if (!d.classList.contains('on')) return;
+      var focs = focusables(d);
+      if (!focs.length) return;
+      var idx = focs.indexOf(document.activeElement);
+      if (e.key === 'Tab') {
+        if (e.shiftKey) {
+          e.preventDefault();
+          focs[idx <= 0 ? focs.length - 1 : idx - 1].focus();
+        } else {
+          e.preventDefault();
+          focs[idx >= focs.length - 1 ? 0 : idx + 1].focus();
+        }
+      } else if (e.key === 'ArrowDown') {
+        e.preventDefault();
+        var links = focs.filter(function (f) { return f.tagName === 'A' || (f.tagName === 'BUTTON' && f !== x); });
+        var li = links.indexOf(document.activeElement);
+        if (links.length) links[li >= links.length - 1 ? 0 : li + 1].focus();
+      } else if (e.key === 'ArrowUp') {
+        e.preventDefault();
+        var links2 = focs.filter(function (f) { return f.tagName === 'A' || (f.tagName === 'BUTTON' && f !== x); });
+        var li2 = links2.indexOf(document.activeElement);
+        if (links2.length) links2[li2 <= 0 ? links2.length - 1 : li2 - 1].focus();
+      }
+    });
+    return d;
+  }
+
+  function navSection(body, title, opts) {
+    var sec = section(title, opts);
+    (opts && opts.groups ? opts.groups : GROUPS).forEach(function (g) {
+      g.items.forEach(function (it) {
+        var a = el('a', it[0].toLowerCase() === CUR ? 'cur' : '', it[1]);
+        a.href = '/' + it[0]; a.addEventListener('click', closeAll); sec._list.appendChild(a);
+      });
+    });
+    body.appendChild(sec); return sec;
+  }
+
+  // LEFT — full site navigation, one collapsed section per group
+  var left = drawer('left', 'Navigate', 'the whole site');
+  GROUPS.forEach(function (g) {
+    var hasCur = g.items.some(function (it) { return it[0].toLowerCase() === CUR; });
+    var sec = section(g.title, { count: g.items.length, current: hasCur });
+    g.items.forEach(function (it) {
+      var a = el('a', it[0].toLowerCase() === CUR ? 'cur' : '', it[1]);
+      a.href = '/' + it[0]; a.addEventListener('click', closeAll); sec._list.appendChild(a);
+    });
+    left._body.appendChild(sec);
+  });
+  // type-to-filter across all site links
+  var searchWrap = el('div', 'cnav-search', '<span class="ic">🔎</span>');
+  var searchIn = document.createElement('input');
+  searchIn.type = 'text'; searchIn.placeholder = 'Filter pages…'; searchIn.setAttribute('aria-label', 'Filter pages');
+  searchWrap.appendChild(searchIn);
+  left._body.insertBefore(searchWrap, left._body.firstChild);
+  var noRes = el('div', 'cnav-noresult', 'No pages match.'); left._body.appendChild(noRes);
+  function filterLeft(q) {
+    q = (q || '').trim().toLowerCase(); var anyVisible = false;
+    left._body.querySelectorAll('.cnav-sec').forEach(function (sec) {
+      var shown = 0;
+      sec.querySelectorAll('.cnav-list a').forEach(function (a) {
+        var m = !q || a.textContent.toLowerCase().indexOf(q) >= 0; a.classList.toggle('hide', !m); if (m) shown++;
+      });
+      sec.classList.toggle('hide', !!q && shown === 0);
+      sec.classList.toggle('collapsed', !q);            // expand matches while filtering, re-collapse when cleared
+      if (shown > 0) anyVisible = true;
+    });
+    noRes.style.display = (q && !anyVisible) ? 'block' : 'none';
+  }
+  searchIn.addEventListener('input', function () { filterLeft(searchIn.value); });
+
+  // RIGHT — "On this page" jump links + the full site menu (all collapsed)
+  var right = drawer('right', 'On this page', 'jump to a section');
+  function slug(s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '').slice(0, 40) || 'sec'; }
+  function buildOnThisPage() {
+    right._body.innerHTML = '';
+    var jump = section('Sections on this page', { current: true });
+    var nodes = Array.prototype.slice.call(document.querySelectorAll('main h2, section > h2, h2.sec, .panel > h3, section > h3'));
+    var seen = {}, count = 0;
+    nodes.forEach(function (n) {
+      if (n.closest('.cnav-drawer')) return;
+      if (!n.offsetParent && n.offsetHeight === 0) return;
+      var txt = (n.textContent || '').replace(/\s+/g, ' ').trim();
+      txt = txt.replace(/^[▸▾▿▶◀⌄˅▼>\s]+/, '');
+      if (!txt) return; txt = txt.slice(0, 46);
+      if (!n.id) { var s = slug(txt), u = s, i = 2; while (document.getElementById(u)) { u = s + '-' + (i++); } n.id = u; }
+      if (seen[n.id]) return; seen[n.id] = 1;
+      var a = el('a', '', txt); a.href = '#' + n.id; a.addEventListener('click', closeAll);
+      jump._list.appendChild(a); count++;
+    });
+    if (count) { jump.querySelector('.rt').insertAdjacentHTML('afterbegin', '<span class="cnt">' + count + '</span>'); right._body.appendChild(jump); }
+    else { right._body.appendChild(el('div', 'cnav-empty', 'No jump-to sections on this page. Use “Go to another page” below or the ☰ menu (top-left).')); }
+    navSection(right._body, 'Go to another page', { count: GROUPS.reduce(function (n, g) { return n + g.items.length; }, 0) });
+  }
+
+  // ── the two hamburgers ──────────────────────────────────────────────────────────────
+  function burger(side, glyph, label, onOpen) {
+    var b = el('button', 'cnav-burger ' + side, glyph + ' <span class="lbl">' + label + '</span>');
+    b.setAttribute('aria-label', label); b.setAttribute('aria-expanded', 'false');
+    var target = side === 'left' ? left : right;
+    b.addEventListener('click', function (e) {
+      e.stopPropagation();
+      var open = target.classList.contains('on'); closeAll();
+      if (!open) {
+        if (onOpen) onOpen();
+        target.classList.add('on'); ov.classList.add('on'); b.setAttribute('aria-expanded', 'true'); lastOpener = b;
+        setTimeout(function () {
+          if (side === 'left') { if (searchIn) searchIn.focus(); var cur = target.querySelector('a.cur'); if (cur) cur.scrollIntoView({ block: 'nearest' }); }
+          else { var f = target.querySelector('.cnav-list a'); if (f) f.focus(); }
+        }, 60);
+      }
+    });
+    document.body.appendChild(b); return b;
+  }
+  burger('left', '☰', 'Menu', null);
+  burger('right', '▤', 'Sections', buildOnThisPage);
+
+  // Reserve corner space on the top bar so its title / remaining controls clear the burgers.
+  function reserve() {
+    var bar = document.querySelector('header'); if (!bar) return;
+    var cs = getComputedStyle(bar);
+    bar.style.paddingLeft = Math.max(parseInt(cs.paddingLeft, 10) || 0, 104) + 'px';
+    bar.style.paddingRight = Math.max(parseInt(cs.paddingRight, 10) || 0, 120) + 'px';
+  }
+  // Fold old horizontal top-tabs into the hamburgers: hide inter-page nav LINKS in the header
+  // (every destination lives in the ☰ menu) but LEAVE page controls (sliders, buttons, selects).
+  function hideTopTabs() {
+    document.querySelectorAll('header a[href], .topnav a[href], nav.nav a[href], .nav a[href]').forEach(function (a) {
+      if (a.closest('.cnav-drawer')) return;
+      var href = a.getAttribute('href') || '';
+      if (/\.html([?#].*)?$/i.test(href) || href === '/' || href.charAt(0) === '#') a.style.display = 'none';
+    });
+    ['.topnav', 'nav.nav', 'header .nav'].forEach(function (sel) {
+      document.querySelectorAll(sel).forEach(function (w) {
+        var hasVisibleControl = Array.prototype.some.call(w.children, function (c) {
+          if (c.tagName === 'A') return c.style.display !== 'none';
+          return c.offsetParent !== null || /INPUT|BUTTON|SELECT|SPAN|LABEL/.test(c.tagName);
+        });
+        if (!hasVisibleControl) w.style.display = 'none';
+      });
+    });
+  }
+  function apply() { reserve(); hideTopTabs(); }
+  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', apply); else apply();
+})();
diff --git a/front-page-audit/screens/crcp/repro/crcp-theme.css b/front-page-audit/screens/crcp/repro/crcp-theme.css
new file mode 100644
index 0000000..eb6e268
--- /dev/null
+++ b/front-page-audit/screens/crcp/repro/crcp-theme.css
@@ -0,0 +1,29 @@
+/* CRCP shared day/night theme tokens — single source of truth across all pages.
+   Load LAST (after each page's own <style>) so it overrides local :root values.
+   Defines BOTH naming conventions in use (--mut AND --muted) so every page themes. */
+html[data-theme="dark"]{
+  --bg:#0d1117; --bg2:#171d26; --card:#1c232e; --line:#33404f; --line2:#3f4c5c;
+  --ink:#f2f6fb; --text:#f2f6fb; --mut:#b3c0cf; --muted:#b3c0cf; --sub:#b3c0cf;
+  --headbg:rgba(13,17,23,.98); --stripe:rgba(255,255,255,.025); --hover:rgba(121,184,255,.10);
+  --acc:#4bd268; --blue:#79b8ff; --gold:#e3b341; --red:#ff7b72; --purp:#c8a2ff;
+  --onacc:#0e1116; /* text on an accent bg — accents are LIGHT in dark mode, so black text reads */
+}
+html[data-theme="light"]{
+  --bg:#f4f6f9; --bg2:#ffffff; --card:#ffffff; --line:#d5dbe2; --line2:#c2cad4;
+  --ink:#12171e; --text:#12171e; --mut:#525f6e; --muted:#525f6e; --sub:#525f6e;
+  --headbg:rgba(255,255,255,.98); --stripe:rgba(20,30,45,.028); --hover:rgba(9,105,218,.08);
+  --acc:#1a7f37; --blue:#0969da; --gold:#9a6700; --red:#cf222e; --purp:#8250df;
+  --onacc:#ffffff; /* text on an accent bg — accents are DARK in light mode, so white text reads */
+  /* fleet custom SURFACE vars — bespoke pages invented these and crcp-theme didn't cover them, so
+     they stayed dark in day mode (dark islands). Defined for LIGHT ONLY, so dark mode keeps each
+     page's own :root value (zero dark-mode change) and light mode finally flips these surfaces. */
+  --panel:#ffffff; --panel2:#f4f6f9; --fg:#12171e; --txt:#12171e; --g2:#f4f6f9;
+  --glass:rgba(255,255,255,.72); --glassb:rgba(20,30,45,.12);
+}
+/* shared toggle button (auto-injected by crcp-theme.js into the page header) */
+#crcpThemeToggle{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;
+  padding:6px 11px;font-size:13px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;font-family:inherit;}
+#crcpThemeToggle:hover{border-color:var(--blue);}
+/* readability lift that helps every list/table page (harmless where selectors don't exist) */
+.listtbl tbody tr:nth-child(even) td,tbody tr.row:nth-child(even),.tbl tbody tr:nth-child(even) td{background:var(--stripe);}
+.listtbl tbody tr:hover td,.tbl tbody tr:hover td{background:var(--hover);}
diff --git a/front-page-audit/screens/crcp/repro/real-index-fallback.html b/front-page-audit/screens/crcp/repro/real-index-fallback.html
new file mode 100644
index 0000000..ed6d0f5
--- /dev/null
+++ b/front-page-audit/screens/crcp/repro/real-index-fallback.html
@@ -0,0 +1,2548 @@
+<!doctype html>
+<html lang="en">
+<head>
+<script>/* credential-safe fetch (2026-08-18): resolve EVERY relative fetch (with or without a leading slash) against a credential-free base so creds embedded in the page URL (http://user:pass@host/...) don't poison document.baseURI and throw "Request cannot be constructed from a URL that includes credentials". */(function(){var of=window.fetch,O=location.origin;window.fetch=function(u,o){try{if(typeof u==='string'){if(/^[a-z][a-z0-9+.-]*:/i.test(u)){var x=new URL(u);if(x.username||x.password){x.username='';x.password='';u=x.href;}}else{u=new URL(u,O+location.pathname+location.search).href;}}else if(u&&typeof u.url==='string'){var y=new URL(u.url,O+location.pathname+location.search);if(y.username||y.password){y.username='';y.password='';u=new Request(y.href,u);}}}catch(e){}return of.call(this,u,o);};try{if(location.href.indexOf('@')>=0){history.replaceState(null,'',location.pathname+location.search+location.hash);}}catch(e){}})();</script>
+<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='dark'?'dark':'light');}catch(e){document.documentElement.setAttribute('data-theme','light');}})();</script>
+<!-- Self-heal: nav-agent grid controls can persist a "removed"/"collapsed" state that hides the
+     whole grid with no obvious way back. Clear it before nav-agent runs so a grid can never stay hidden. -->
+<script>(function(){try{for(var i=localStorage.length-1;i>=0;i--){var k=localStorage.key(i);if(k&&k.indexOf('navagent:')===0){var v=localStorage.getItem(k);if(v&&v.indexOf('"removed":true')>=0){localStorage.removeItem(k);}}}}catch(e){}})();</script>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230e1116'/%3E%3Ctext x='8' y='12' font-size='11' text-anchor='middle' fill='%233fb950'%3E%24%3C/text%3E%3C/svg%3E">
+<title>LA County CRE — Investment Explorer</title>
+<style>
+  :root { --cols:3; --drawerW:316px; }
+  * { box-sizing:border-box; }
+  :focus-visible { outline:2px solid var(--blue); outline-offset:2px; border-radius:4px; }
+  @media (prefers-reduced-motion: reduce){ *,*::before,*::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; scroll-behavior:auto !important; } }
+  body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }
+
+  /* ---- top bar ---- */
+  header { display:flex; align-items:center; gap:14px; padding:12px 20px; border-bottom:1px solid var(--line); position:sticky; top:0; z-index:30; background:var(--headbg); backdrop-filter:blur(8px); }
+  .burger { display:inline-flex; align-items:center; gap:8px; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:9px 13px; font-size:15px; cursor:pointer; font-weight:600; }
+  .burger:hover { border-color:var(--blue); }
+  .fcount { display:none; background:var(--blue); color:#04122b; font-size:11px; font-weight:700; border-radius:20px; padding:1px 7px; }
+  .fcount.on { display:inline-block; }
+  .htitle h1 { margin:0; font-size:18px; letter-spacing:.2px; }
+  .htitle .sub { color:var(--mut); font-size:12px; }
+  header .spacer { margin-left:auto; }
+  /* nav-link cluster; on wide screens it sits inline right-aligned, on tablets/phones it becomes a swipeable strip (see @media 1024) */
+  .topnav { display:flex; align-items:center; flex-wrap:nowrap; }
+  .seg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .seg button { background:var(--card); color:var(--ink); border:0; border-right:1px solid var(--line); padding:8px 11px; font-size:12px; cursor:pointer; }
+  .seg button:last-child { border-right:0; }
+  .seg button.active { background:#1f6feb; color:#fff; }
+  #drawer .seg { display:flex; width:100%; }
+  #drawer .seg button { flex:1; text-align:center; padding:9px 6px; }
+
+  /* ---- left drawer ---- */
+  #scrim { position:fixed; inset:0; background:rgba(0,0,0,.5); opacity:0; pointer-events:none; transition:opacity .2s; z-index:38; }
+  #scrim.on { opacity:1; pointer-events:auto; }
+  #drawer { position:fixed; top:0; left:0; height:100vh; width:var(--drawerW); background:var(--bg2); border-right:1px solid var(--line); transform:translateX(-100%); transition:transform .22s ease; z-index:40; overflow-y:auto; padding:0 0 40px; }
+  #drawer.open { transform:translateX(0); box-shadow:6px 0 30px rgba(0,0,0,.4); }
+  .dhead { display:flex; align-items:center; justify-content:space-between; padding:16px 18px 10px; position:sticky; top:0; background:var(--bg2); border-bottom:1px solid var(--line); z-index:2; }
+  .dhead b { font-size:15px; } .dhead .x { background:none; border:0; color:var(--mut); font-size:18px; cursor:pointer; }
+  .fsec { padding:14px 18px; border-bottom:1px solid var(--line); }
+  .fsec h4 { margin:0 0 9px; font-size:11px; text-transform:uppercase; letter-spacing:.6px; color:var(--mut); }
+  .chips { display:flex; flex-wrap:wrap; gap:6px; }
+  .chip { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:18px; padding:5px 11px; font-size:12px; cursor:pointer; user-select:none; }
+  .chip:hover { color:var(--ink); }
+  .chip.active { border-color:var(--blue); color:var(--blue); background:rgba(88,166,255,.08); }
+  .chip .ct { color:var(--mut); font-size:10px; margin-left:4px; }
+  .chip.zero { opacity:.4; }
+  .row2 { display:flex; gap:8px; }
+  .fld { flex:1; }
+  .fld label { display:block; font-size:10px; color:var(--mut); margin-bottom:3px; }
+  input[type=text], input[type=number], select { width:100%; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:8px 10px; font-size:13px; }
+  input[type=range] { width:100%; accent-color:var(--blue); }
+  .rangeval { font-size:11px; color:var(--blue); float:right; }
+  .toggle { display:flex; align-items:center; gap:8px; font-size:13px; cursor:pointer; }
+  .resetbtn { width:calc(100% - 36px); margin:14px 18px 0; background:transparent; color:var(--bad); border:1px solid #7d2b28; border-radius:9px; padding:9px; font-size:13px; cursor:pointer; }
+
+  /* ---- results bar + grid ---- */
+  .bar { display:flex; flex-wrap:wrap; align-items:center; gap:14px; padding:11px 20px; border-bottom:1px solid var(--line); position:sticky; top:57px; background:var(--headbg); backdrop-filter:blur(6px); z-index:20; }
+  .bar .count { font-size:13px; color:var(--mut); } .bar .count b { color:var(--ink); }
+  .bar label { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.5px; }
+  .bar .right { margin-left:auto; display:flex; align-items:center; gap:12px; flex-wrap:wrap; justify-content:flex-end; min-width:0; }  /* wrap so the search/heat/sort controls can't overflow under the right-hand #ctrlrail when main is narrow */
+  .grid { display:grid; grid-template-columns:repeat(var(--cols),minmax(0,1fr)); gap:22px; padding:30px 32px 44px; }
+  @media (max-width:900px){ .grid{ grid-template-columns:repeat(2,minmax(0,1fr));} }
+  /* ---- iPad / tablet / narrow-desktop band (≤1024): keep header ONE fixed row so sticky offsets hold,
+         turn the nav-link cluster into a swipeable horizontal strip instead of clipping off-screen. ---- */
+  @media (max-width:1024px){
+    .grid{ grid-template-columns:repeat(2,minmax(0,1fr)); }  /* docked-drawer band: 2 cols so cards aren't crushed (iPad landscape) */
+    header{ flex-wrap:nowrap; gap:10px; }
+    header .spacer{ display:none; }                    /* nav no longer right-pushed; topnav flexes to fill */
+    .htitle{ min-width:0; flex:0 1 auto; }             /* let the title shrink instead of shoving nav off-screen */
+    .htitle h1{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+    .htitle .sub{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }  /* keep title block 2 lines → header stays ~57px so sticky bar/drawer offsets hold */
+    .topnav{ flex:1 1 auto; min-width:0; overflow-x:auto; overflow-y:hidden;
+             -webkit-overflow-scrolling:touch; scrollbar-width:none; }  /* iOS momentum-scroll, hidden scrollbar */
+    .topnav::-webkit-scrollbar{ display:none; }
+    .topnav > a, .topnav > .seg{ flex:0 0 auto; }      /* items keep intrinsic width; strip scrolls, nothing clips */
+    .topnav > a{ padding:8px 0; }                      /* taller touch target for finger taps */
+    html, body{ overflow-x:hidden; max-width:100%; }   /* no horizontal page scroll on iPad (html is the scroller) */
+    .bar{ max-width:100%; top:auto; }                   /* sort/density strip: no hairline overflow; un-stick so it can't tuck under the taller (burger+title) iPad header */
+  }
+  @media (max-width:620px){ .grid{ grid-template-columns:1fr;} }
+  .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:24px; display:flex; flex-direction:column; gap:10px; position:relative; transition:box-shadow .2s,border-color .2s; }
+  .card.uc { opacity:.62; }
+  .card.flash { border-color:var(--blue); box-shadow:0 0 0 3px rgba(88,166,255,.35); }
+  .top { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; }
+  .rank { font-size:12px; color:var(--mut); }
+  .score { font-size:30px; font-weight:700; line-height:1; }
+  .score small { font-size:11px; color:var(--mut); font-weight:400; display:block; text-align:right; }
+  .addr { font-weight:600; font-size:15px; }
+  .city { color:var(--mut); font-size:13px; }
+  .badges { display:flex; flex-wrap:wrap; gap:6px; }
+  .b { font-size:11px; padding:3px 8px; border-radius:20px; border:1px solid var(--line); color:var(--mut); }
+  .b.type { color:var(--blue); border-color:#284b7a; }
+  .b.deal-badge { font-weight:700; cursor:help; }
+  .b.ok { color:var(--acc); border-color:#1f6f37; }
+  .b.warn { color:var(--warn); border-color:#6b5320; }
+  .b.bad { color:var(--bad); border-color:#7d2b28; }
+  .b.rec-Buy { color:var(--acc); border-color:#1f6f37; font-weight:600; }
+  .b.rec-Watch { color:var(--warn); border-color:#6b5320; font-weight:600; }
+  .b.rec-Pass { color:var(--bad); border-color:#7d2b28; font-weight:600; }
+  .metrics { display:grid; grid-template-columns:1fr 1fr; gap:6px 14px; font-size:13px; border-top:1px solid var(--line); border-bottom:1px solid var(--line); padding:10px 0; }
+  .metrics div { display:flex; justify-content:space-between; }
+  .metrics .k { color:var(--mut); }
+  .metrics .v { font-variant-numeric:tabular-nums; font-weight:600; }
+  .pos { color:var(--acc); } .neg { color:var(--bad); } .na { color:var(--mut); }
+  .thesis { font-size:13px; color:var(--ink); }
+  .lst { font-size:12px; color:var(--mut); margin:2px 0 0; padding-left:16px; }
+  .src { font-size:12px; } .src a { color:var(--blue); text-decoration:none; }
+  .lcbtn,.histbtn { background:transparent; color:var(--warn); border:1px dashed #6b5320; border-radius:14px; padding:2px 8px; font-size:11px; cursor:pointer; }
+  .histbtn { color:var(--blue); border-color:#284b7a; }
+  .lcout,.histout { font-size:11px; color:var(--mut); margin-top:6px; }
+  .lcout a { color:var(--blue); text-decoration:none; }
+  .lcout .lc-item { padding:4px 0; border-top:1px solid var(--line); }
+  .small { font-size:11px; color:var(--mut); }
+  .statusline { font-size:12px; font-weight:600; font-variant-numeric:tabular-nums; padding:5px 9px; border-radius:8px; border:1px solid var(--line); background:rgba(255,255,255,.02); }
+  .statusline.ok { color:var(--acc); border-color:#1f6f37; }
+  .statusline.warn { color:var(--warn); border-color:#6b5320; }
+  .statusline.bad { color:var(--bad); border-color:#7d2b28; }
+  .statusline.mut { color:var(--mut); }
+  .statusline.none { color:var(--warn); border-color:#6b5320; border-style:dashed; }
+  .foot { color:var(--mut); font-size:12px; padding:0 20px 8px; }
+  .empty { padding:60px 20px; text-align:center; color:var(--mut); }
+
+  /* ---- Broker-history + prior-broker flag ---- */
+  .brokerflag { display:inline-flex; align-items:center; gap:5px; font-size:11px; padding:3px 9px; border-radius:20px; background:rgba(255,159,9,.10); color:#ff9f09; border:1px solid rgba(255,159,9,.35); font-weight:700; cursor:help; margin-top:2px; }
+  .txhist { font-size:11px; color:var(--mut); margin-top:4px; line-height:1.5; }
+  .txhist .tx-row { display:flex; gap:8px; align-items:baseline; padding:2px 0; border-bottom:1px dashed rgba(255,255,255,.05); }
+  .txhist .tx-row:last-child { border-bottom:0; }
+  .txhist .tx-date { color:var(--blue); font-variant-numeric:tabular-nums; min-width:72px; }
+  .txhist .tx-ev { flex:1; }
+  .txhist .tx-price { color:var(--acc); font-weight:600; font-variant-numeric:tabular-nums; }
+  .brokerline { font-size:11px; padding:4px 9px; background:rgba(88,166,255,.06); border-radius:7px; border:1px solid rgba(88,166,255,.15); color:#a5c8ff; margin-top:3px; }
+  .brokerline strong { color:var(--blue); }
+
+  /* ---- docked chat ---- */
+  #chatwrap { border-top:1px solid var(--line); background:var(--bg2); padding:16px 20px 40px; }
+  #chatwrap h2 { font-size:15px; margin:0 0 2px; }
+  #chatwrap .hint { color:var(--mut); font-size:12px; margin-bottom:10px; }
+  #log { display:flex; flex-direction:column; gap:10px; max-height:42vh; overflow-y:auto; padding:4px 2px; }
+  .msg { max-width:80%; padding:9px 13px; border-radius:12px; font-size:14px; line-height:1.45; white-space:pre-wrap; word-wrap:break-word; }
+  .msg.u { align-self:flex-end; background:#1f6feb; color:#fff; border-bottom-right-radius:3px; }
+  .msg.a { align-self:flex-start; background:var(--card); border:1px solid var(--line); border-bottom-left-radius:3px; }
+  .msg.a code { background:var(--bg); padding:1px 5px; border-radius:4px; }
+  .msg.sys { align-self:center; color:var(--mut); font-size:12px; background:transparent; }
+  .actiontag { display:inline-block; margin-top:6px; font-size:11px; color:var(--acc); border:1px solid #1f6f37; border-radius:6px; padding:2px 7px; }
+  #chatform { display:flex; gap:10px; margin-top:12px; }
+  #q { flex:1; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:11px 13px; font-size:14px; }
+  #send { background:#1f6feb; color:#fff; border:0; border-radius:10px; padding:0 20px; font-size:14px; font-weight:600; cursor:pointer; }
+  #send:disabled { opacity:.5; cursor:default; }
+  .examples { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
+  .ex { font-size:12px; color:var(--blue); border:1px dashed #284b7a; border-radius:16px; padding:4px 10px; cursor:pointer; background:transparent; }
+
+  /* ---- view toggle (grid/list) ---- */
+  .viewseg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .viewseg button { background:var(--card); color:var(--mut); border:0; border-right:1px solid var(--line); padding:7px 12px; font-size:12px; cursor:pointer; }
+  .viewseg button:last-child { border-right:0; }
+  .viewseg button.active { background:#1f6feb; color:#fff; }
+
+  /* ===== nav-agent inline views: compact List / KV / Table group headers ===== */
+  .compacthost, .kvhost { padding:20px 32px 44px; }
+  .compactlist { display:flex; flex-direction:column; gap:6px; }
+  .crow { display:flex; align-items:center; gap:12px; background:var(--card); border:1px solid var(--line); border-radius:9px; padding:8px 12px; }
+  .crow .starbtn { flex:0 0 auto; }
+  .crow .cid { flex:0 0 220px; min-width:180px; }
+  .crow .cla { display:block; font-weight:600; color:var(--ink); font-size:13.5px; }
+  .crow .clc { display:block; color:var(--mut); font-size:11px; }
+  .crow .cfields { flex:1; display:flex; flex-wrap:wrap; gap:6px 14px; align-items:center; min-width:0; }
+  .cchip { font-size:12px; color:var(--ink); white-space:nowrap; }
+  .cchip .cck { color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.3px; margin-right:3px; }
+  .kvgrid { display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:16px; }
+  .kvcard { background:var(--card); border:1px solid var(--line); border-radius:11px; padding:12px 14px; }
+  .kvhead { display:flex; align-items:center; gap:8px; margin-bottom:8px; padding-bottom:8px; border-bottom:1px solid var(--line); }
+  .kvaddr { font-weight:600; font-size:14px; color:var(--ink); }
+  .kvrow { display:flex; gap:10px; padding:3px 0; font-size:12.5px; border-bottom:1px solid rgba(127,127,127,.08); }
+  .kvrow:last-child { border-bottom:0; }
+  .kvk { flex:0 0 42%; color:var(--mut); text-transform:uppercase; font-size:10px; letter-spacing:.3px; padding-top:2px; }
+  .kvv { flex:1; color:var(--ink); min-width:0; word-break:break-word; }
+  .grouphead { padding:8px 12px; margin:10px 0 4px; background:rgba(88,166,255,.08); border-left:3px solid var(--blue); border-radius:4px; font-size:12px; color:var(--mut); text-transform:uppercase; letter-spacing:.4px; }
+  .grouphead b { color:var(--ink); }
+  .kvgrid .grouphead { grid-column:1/-1; }
+  tr.grouprow td.groupcell { background:rgba(88,166,255,.10); font-size:11px; text-transform:uppercase; letter-spacing:.4px; color:var(--mut); padding:7px 10px; }
+  tr.grouprow td.groupcell b { color:var(--ink); }
+
+  /* ===== right-hand control rail (in-flow, NOT floating) — nav-agent skill surface ===== */
+  #ctrlrail { flex:0 0 var(--railW,272px); width:var(--railW,272px); border-left:1px solid var(--line); background:var(--headbg); align-self:stretch; padding:12px 12px 40px; position:sticky; top:57px; max-height:calc(100vh - 57px); overflow-y:auto; }
+  #ctrlrail .railtitle { font-size:11px; text-transform:uppercase; letter-spacing:.6px; color:var(--mut); margin:2px 4px 12px; display:flex; align-items:center; gap:6px; }
+  .rpanel { border:1px solid var(--line); border-radius:9px; margin-bottom:8px; overflow:hidden; background:var(--card); }
+  .rpanel > .rphead { width:100%; display:flex; align-items:center; justify-content:space-between; gap:8px; background:transparent; color:var(--ink); border:0; padding:10px 12px; font-size:12.5px; font-weight:600; cursor:pointer; text-align:left; }
+  .rpanel > .rphead:hover { color:var(--blue); }
+  .rpanel > .rphead .caret { color:var(--mut); font-size:11px; transition:transform .15s; }
+  .rpanel.open > .rphead .caret { transform:rotate(90deg); color:var(--blue); }
+  .rpanel > .rpbody { display:none; padding:4px 12px 12px; }
+  .rpanel.open > .rpbody { display:block; }
+  .rpbody select, .rpbody input[type=text] { width:100%; background:var(--bg); color:var(--ink); border:1px solid var(--line); border-radius:7px; padding:7px 9px; font-size:12px; margin-top:6px; }
+  .rpbody .rlbl { font-size:11px; color:var(--mut); margin-top:8px; display:block; }
+  .rpbody .ftog { display:flex; width:100%; margin:0 0 4px; }
+  @media (max-width:1100px){ #ctrlrail { display:none; } }   /* on narrow screens the left drawer already carries these controls */
+  /* ---- field show/hide toggles: one CARD per field, click to toggle on/off ---- */
+  .fieldtoggles { display:flex; flex-wrap:wrap; gap:6px; max-height:360px; overflow-y:auto; padding:2px 4px 2px 0; }
+  .ftog { display:inline-flex; align-items:center; gap:5px; font-size:12px; color:var(--mut); cursor:pointer; padding:5px 9px;
+          border:1px solid var(--line); border-radius:9px; background:var(--card); user-select:none; transition:border-color .12s,color .12s,background .12s; }
+  .ftog input { display:none; }                                   /* card IS the toggle; checkbox drives state via :has() */
+  .ftog::before { content:'○'; font-size:11px; color:var(--mut); }
+  .ftog:hover { border-color:var(--blue); color:var(--ink); }
+  .ftog:has(input:checked) { border-color:var(--blue); color:var(--blue); background:rgba(88,166,255,.10); }
+  .ftog:has(input:checked)::before { content:'●'; color:var(--blue); }
+  .ftog .colbadge { color:var(--mut); font-size:10px; }
+  /* panel drag-to-reorder (Fix 3) */
+  .ftog .flbl { flex:1; }
+  .ftog .fdrag { cursor:grab; color:var(--mut); font-size:12px; opacity:.5; user-select:none; line-height:1; margin-right:2px; }
+  .ftog:hover .fdrag { opacity:1; }
+  .ftog.fdragging { opacity:.45; }
+  .ftog.fdropT { box-shadow:inset 0 3px 0 var(--gold); }
+  .ftog.fdropB { box-shadow:inset 0 -3px 0 var(--gold); }
+  .minibtn { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:7px; padding:4px 9px; font-size:11px; cursor:pointer; margin-right:5px; }
+  .minibtn:hover { border-color:var(--blue); color:var(--ink); }
+  /* ---- list view ---- */
+  .listhost { display:block; padding:20px 20px 30px; }
+  .listwrap { overflow:auto; border:1px solid var(--line); border-radius:12px; }
+  /* col-resize.js wraps the table in .cr-scroll (overflow:auto) — THAT is the th's nearest scroll
+     container, so bounding ITS height (not .listwrap's) is what makes the sticky header (thead
+     top:0) AND sticky first column (td.stick left:0) actually freeze while scrolling both axes.
+     --tbl-top = the sticky app-chrome height above the table (tune per layout). */
+  .cr-scroll { max-height:calc(100vh - var(--tbl-top, 178px)); }
+  /* max-content so columns size to their content and expand left/right (no artificial squish);
+     min-width:100% keeps it filling the pane when there are few columns; .listwrap scrolls-x. */
+  .listtbl { width:max-content; min-width:100%; border-collapse:collapse; font-size:12.5px; white-space:nowrap; }
+  .listtbl th, .listtbl td { text-align:right; padding:9px 13px; border-top:1px solid var(--line); }
+  .listtbl th { position:sticky; top:0; background:var(--bg2); color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; border-top:0; z-index:2; }
+  .listtbl td.stick, .listtbl th.stick { text-align:left; position:sticky; left:0; background:var(--card); z-index:1; }
+  .listtbl th.stick { background:var(--bg2); z-index:3; }  /* corner = header row + first col: highest */
+  .listtbl th.sortable { cursor:pointer; } .listtbl th.sortable:hover { color:var(--ink); }
+  /* sort arrow via CSS ::after (not literal text) so the header TEXT stays stable and col-resize's tableSig-keyed saved widths survive re-render (TK-10089) */
+  .listtbl th.asc::after { content:' ▲'; color:var(--gold); } .listtbl th.desc::after { content:' ▼'; color:var(--gold); }
+  /* draggable column reorder on the PRIMARY list table (Steve list-build rule) */
+  .listtbl th.dragcol { cursor:grab; }
+  .listtbl th.dragging { opacity:.45; }
+  .listtbl th.dropL { box-shadow:inset 3px 0 0 var(--blue); }
+  .listtbl th.dropR { box-shadow:inset -3px 0 0 var(--blue); }
+  .listtbl tr.lrow:hover td { background:rgba(88,166,255,.06); }
+  .listtbl tr.lrow:hover td.stick { background:var(--card); }
+  .listtbl .la { font-weight:600; color:var(--ink); text-decoration:none; }
+  .listtbl .la:hover { color:var(--blue); }
+  .listtbl .lc { color:var(--mut); font-size:11px; }
+  .listtbl .lsrc { margin-top:2px; }
+  .srclink { color:var(--blue); text-decoration:none; font-size:11px; font-weight:600; } .srclink:hover { text-decoration:underline; }
+  .listtbl .flink { color:var(--ink); text-decoration:none; border-bottom:1px dotted #3a4453; }
+  .listtbl .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  .listtbl tr.lrow { cursor:pointer; }
+  .listtbl .exptog { display:inline-block; width:11px; color:var(--mut); font-size:10px; transition:transform .15s; }
+  .listtbl tr.lrow.open td.stick { background:var(--card); }
+  .listtbl tr.lexp > td { padding:0; border-top:0; }
+  .listtbl tr.lexp .lexpbody { max-height:0; overflow:hidden; transition:max-height .25s ease; background:var(--bg); }
+  .listtbl tr.lexp.open .lexpbody { max-height:1200px; padding:12px 16px; border-top:1px solid var(--line); }
+  .lexpbody .src { text-align:left; white-space:normal; font-size:12px; }
+  .lexpbody .deep { text-align:left; white-space:normal; margin-top:8px; display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
+  .lexpbody .deeplbl { color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; margin-right:2px; }
+  .lexpbody .deep a, .lexpbody .deep button { font-size:11px; padding:3px 9px; border-radius:14px; border:1px solid var(--line); background:var(--card); color:var(--ink); text-decoration:none; cursor:pointer; }
+  .lexpbody .deep a:hover, .lexpbody .deep button:hover { border-color:var(--blue); color:var(--blue); }
+  .lexpbody .lcout, .lexpbody .histout { text-align:left; white-space:normal; }
+  /* Agent · contact column cell */
+  .listtbl td .agentcell { text-align:left; white-space:normal; min-width:170px; display:flex; flex-direction:column; gap:3px; }
+  .agentcell .amut { color:var(--mut); font-style:italic; font-size:11px; }
+  .agentcell .ainrow { display:flex; flex-wrap:wrap; gap:8px; }
+  .agentcell .ain { color:var(--mut); text-decoration:none; font-size:11px; }
+  .agentcell .ain:hover { color:var(--blue); }
+  .ain a { color:var(--blue); text-decoration:none; } .ain a:hover { text-decoration:underline; }
+  .tierb { font-size:9px; text-transform:uppercase; letter-spacing:.4px; color:var(--mut); border:1px solid var(--line); border-radius:4px; padding:0 4px; vertical-align:middle; }
+  .pocketbadge { font-size:11px; font-weight:600; color:var(--gold,#d29922); background:rgba(210,153,34,.12); border:1px solid rgba(210,153,34,.4); border-radius:5px; padding:1px 7px; white-space:nowrap; }
+  .agentcell .chiprow:empty { display:none; }
+  .agentcell .achip { display:inline-block; font-size:10px; color:var(--mut); background:var(--card); border:1px solid var(--line); border-radius:10px; padding:1px 6px; margin-right:4px; }
+  .agentcell .achip.done { color:#3fb950; border-color:#264f2e; }
+  .agentcell .acts { display:flex; flex-wrap:wrap; gap:5px; margin-top:2px; }
+  .abtn { font-size:10.5px; padding:2px 8px; border-radius:12px; border:1px solid var(--line); background:var(--card); color:var(--ink); cursor:pointer; }
+  .abtn:hover { border-color:var(--blue); color:var(--blue); }
+  .abtn.contactbtn { border-color:#2f5233; color:#8fdfa0; }
+  .abtn.markbtn { border-color:#284b7a; color:#8ec0ff; }
+  /* CRM block inside the inline panel */
+  .lexpbody .crm { margin-top:10px; border-top:1px solid var(--line); padding-top:10px; text-align:left; }
+  .crm-info { display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
+  .crm-info .crmf { background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12px; padding:5px 8px; min-width:150px; }
+  .crm-info .crmf:focus { outline:none; border-color:var(--blue); }
+  .crm-acts { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-top:8px; }
+  .crm-letters { margin-top:10px; }
+  .crm-letters .lett { border:1px solid var(--line); border-radius:8px; padding:7px 9px; margin-top:6px; background:var(--bg); }
+  .crm-letters .letth { display:flex; align-items:center; gap:8px; font-size:12px; }
+  .crm-letters .letts { color:var(--mut); font-size:10px; }
+  .crm-letters .letdel { margin-left:auto; background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:12px; }
+  .crm-letters .letdel:hover { color:var(--bad,#f85149); }
+  .crm-letters .lettb { white-space:pre-wrap; font-size:11.5px; color:var(--mut); margin-top:4px; }
+  /* Compose-letter modal */
+  .composeM { position:fixed; inset:0; background:rgba(0,0,0,.55); display:none; align-items:center; justify-content:center; z-index:9000; }
+  .composeM.on { display:flex; }
+  .composeCard { background:var(--card); border:1px solid var(--line); border-radius:14px; width:min(620px,92vw); max-height:88vh; overflow:auto; padding:16px; box-shadow:0 12px 40px rgba(0,0,0,.5); }
+  .composeH { display:flex; align-items:center; font-size:14px; margin-bottom:8px; }
+  .composeH .composeX { margin-left:auto; background:transparent; border:0; color:var(--mut); font-size:16px; cursor:pointer; }
+  .composeGate { font-size:11px; color:#e3b341; background:#241d0e; border:1px solid #5a4a1e; border-radius:8px; padding:6px 9px; margin-bottom:10px; }
+  .composeCard input, .composeCard textarea { width:100%; box-sizing:border-box; background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:13px; padding:8px 10px; margin-bottom:8px; font-family:inherit; }
+  .composeCard textarea { resize:vertical; line-height:1.5; }
+  .composeActs { display:flex; gap:8px; justify-content:flex-end; }
+  .composeActs button { font-size:12px; padding:6px 14px; border-radius:10px; border:1px solid var(--line); background:var(--card); color:var(--ink); cursor:pointer; }
+  .composeActs button.primary { border-color:#2f5233; color:#8fdfa0; }
+  .composeActs button.gated { border-color:#5a4a1e; color:#e3b341; }
+  .composeActs button.gated:hover:not([disabled]) { border-color:#e3b341; }
+  .composeActs button[disabled] { opacity:.4; cursor:not-allowed; }
+  /* Tabbed agent window */
+  .composeCard.tabwin { width:min(680px,94vw); }
+  .composeH .winmeta { color:var(--mut); font-size:11px; font-weight:400; margin-left:10px; }
+  .tabbar { display:flex; gap:2px; border-bottom:1px solid var(--line); margin:4px 0 12px; flex-wrap:wrap; }
+  .tabt { background:transparent; border:0; border-bottom:2px solid transparent; color:var(--mut); font-size:12.5px; padding:7px 12px; cursor:pointer; }
+  .tabt:hover { color:var(--ink); }
+  .tabt.active { color:var(--ink); border-bottom-color:var(--blue); font-weight:600; }
+  .tabpane { display:none; }
+  .tabpane.active { display:block; }
+  .tabmut { color:var(--mut); font-size:12px; font-style:italic; padding:8px 2px; }
+  .cm-row { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:10px; }
+  .tabpane .ainrow { display:flex; flex-wrap:wrap; gap:14px; margin:8px 0; }
+  .tabpane .ain { color:var(--mut); text-decoration:none; font-size:12px; }
+  .tabpane .ain:hover { color:var(--blue); }
+  .findgrid { display:flex; flex-direction:column; gap:7px; }
+  .findlink { display:inline-block; background:var(--card); border:1px solid var(--line); border-radius:8px; color:var(--ink); text-decoration:none; font-size:12.5px; padding:8px 11px; cursor:pointer; text-align:left; }
+  .findlink:hover { border-color:var(--blue); color:var(--blue); }
+  .findlink.findloop { padding:8px 11px; }
+  .findlink.findloop a { color:inherit; text-decoration:none; }
+  .crmToast { position:fixed; bottom:22px; left:50%; transform:translateX(-50%) translateY(12px); background:var(--bg2); border:1px solid var(--line); color:var(--ink); font-size:12.5px; padding:9px 16px; border-radius:20px; opacity:0; transition:opacity .2s,transform .2s; z-index:9500; pointer-events:none; }
+  .crmToast.on { opacity:1; transform:translateX(-50%) translateY(0); }
+  /* Card-view agent-contact block (every card, every asset type) */
+  .card .cardagent { margin-top:8px; border-top:1px solid var(--line); padding-top:8px; font-size:12px; }
+  .card .cardagent .ca-top { display:flex; flex-wrap:wrap; align-items:center; gap:6px; }
+  .card .cardagent .ainrow { display:flex; flex-wrap:wrap; gap:10px; margin-top:3px; }
+  .card .cardagent .ain { color:var(--mut); text-decoration:none; font-size:11px; }
+  .card .cardagent .ain:hover { color:var(--blue); }
+  .card .cardagent .acts { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
+  /* Full contact modal info row */
+  .composeCard .cm-info { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-bottom:10px; }
+  .composeCard .cm-info .crmf { background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12px; padding:6px 8px; min-width:140px; flex:1 1 140px; margin:0; }
+  .composeCard .cm-info .crmf:focus { outline:none; border-color:var(--blue); }
+  .composeH .flink { font-size:11px; font-weight:400; margin-left:8px; }
+  /* Inline (no-outside-link) agent bits */
+  .aname { font-weight:600; color:var(--ink); }
+  a.aname { text-decoration:none; cursor:pointer; } a.aname:hover { color:var(--blue); text-decoration:underline; }
+  .flink { cursor:pointer; }
+  .listtbl td .flink { border-bottom:1px dotted #3a4453; }
+  .listtbl td .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  /* ── drill atom: every displayed data point filters to its deeper view ── */
+  .drill { cursor:pointer; border-bottom:1px dotted transparent; }
+  .drill:hover { border-bottom-color:currentColor; filter:brightness(1.15); }
+  .badges .b.drill:hover { border-bottom-color:currentColor; }
+  .city .drill { color:inherit; }
+  .city .drill:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  .copyb { background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:11px; padding:0 3px; border-radius:4px; }
+  .copyb:hover { color:var(--blue); }
+  .crm-note, .lexpbody .crm-note { margin-top:6px; font-size:12px; color:var(--mut); white-space:pre-wrap; background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:6px 9px; text-align:left; }
+  .composeCard .cm-notes { margin-bottom:10px; }
+  .composeCard .cm-notes .deeplbl { margin-bottom:4px; }
+  .composeCard .cm-notes textarea { width:100%; box-sizing:border-box; background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12.5px; padding:7px 9px; font-family:inherit; resize:vertical; line-height:1.5; }
+  .composeCard .cm-notes textarea:focus { outline:none; border-color:var(--blue); }
+  /* ---- Amazon-style docked left filter rail (desktop) ---- */
+  .shell { display:flex; align-items:flex-start; }
+  main { flex:1 1 auto; min-width:0; }
+  @media (min-width:901px){
+    #burger { display:none; }
+    #scrim { display:none !important; }
+    #drawer { position:sticky; top:57px; height:calc(100vh - 57px); transform:none; flex:0 0 var(--drawerW); box-shadow:none; }
+    #drawer .dhead .x { display:none; }
+  }
+  /* collapsible filter tabs */
+  .fsec > h4 { cursor:pointer; display:flex; align-items:center; justify-content:space-between; user-select:none; }
+  .fsec > h4::after { content:'▾'; color:var(--mut); font-size:11px; transition:transform .15s; margin-left:8px; }
+  .fsec.collapsed > h4::after { transform:rotate(-90deg); }
+  .fsec.collapsed > *:not(h4) { display:none !important; }
+  .assessline { border-top:1px dashed rgba(255,255,255,.06); padding-top:6px; margin-top:2px; }
+  /* active-filter summary bar */
+  .activebar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:10px 20px; border-bottom:1px solid var(--line); background:rgba(88,166,255,.03); }
+  .activebar:empty { display:none; }
+  .activebar .lbl { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.5px; }
+  .afchip { display:inline-flex; align-items:center; gap:6px; background:rgba(88,166,255,.10); border:1px solid rgba(88,166,255,.4); color:var(--blue); border-radius:16px; padding:4px 10px; font-size:12px; cursor:pointer; }
+  .afchip:hover { background:rgba(88,166,255,.2); } .afchip .xx { opacity:.65; font-weight:700; }
+  .afreset { background:transparent; border:1px solid #7d2b28; color:var(--bad); border-radius:14px; padding:4px 11px; font-size:12px; cursor:pointer; margin-left:auto; }
+  .afreset:hover { background:rgba(248,81,73,.1); }
+  /* shortlist stars */
+  .starbtn { background:none; border:0; color:var(--mut); font-size:18px; line-height:1; cursor:pointer; padding:0; }
+  .starbtn:hover, .starbtn.on { color:var(--warn); }
+  .copybtn { background:none; border:0; color:var(--mut); font-size:14px; line-height:1; cursor:pointer; padding:0; }
+  .copybtn:hover { color:var(--blue); }
+  .listtbl .starbtn { font-size:14px; margin-right:5px; vertical-align:middle; }
+  #shortbtn.shorton { background:rgba(210,153,34,.14); border-color:var(--warn); color:var(--warn); }
+  /* aggregate stats strip */
+  .statsbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:9px 20px; border-bottom:1px solid var(--line); }
+  .statsbar:empty { display:none; }
+  .statpill { font-size:12px; color:var(--ink); background:var(--card); border:1px solid var(--line); border-radius:8px; padding:4px 10px; }
+  .statpill .sl { color:var(--mut); } .statpill b { font-variant-numeric:tabular-nums; }
+  .heatlegend { display:none; align-items:center; gap:5px; font-size:11px; color:var(--mut); }
+  .heatlegend.on { display:inline-flex; }
+  .heatlegend .bar { width:56px; height:8px; border-radius:4px; background:linear-gradient(90deg,hsl(0,70%,45%),hsl(60,70%,45%),hsl(120,70%,45%)); }
+  .showmore-wrap { grid-column:1/-1; text-align:center; padding:20px 0 6px; }
+  .showmore { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:10px; padding:9px 20px; font-size:13px; cursor:pointer; }
+  .showmore:hover { border-color:var(--blue); }
+  .quickstart { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin:16px 0; }
+  .qchip { background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:18px; padding:7px 14px; font-size:13px; cursor:pointer; }
+  .qchip:hover { border-color:var(--blue); color:var(--blue); }
+  .savedrow { display:flex; gap:6px; align-items:center; margin-bottom:5px; }
+  .savedname { flex:1; text-align:left; background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:8px; padding:6px 9px; font-size:12px; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+  .savedname:hover { border-color:var(--blue); color:var(--blue); }
+  .saveddel { background:none; border:0; color:var(--mut); cursor:pointer; font-size:13px; }
+  .saveddel:hover { color:var(--bad); }
+  #savebtn { width:100%; background:transparent; border:1px dashed #284b7a; color:var(--blue); border-radius:8px; padding:7px; font-size:12px; cursor:pointer; margin-top:4px; }
+  .savedempty { color:var(--mut); font-size:11px; }
+  /* compare overlay */
+  #cmpov { display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:60; align-items:flex-start; justify-content:center; padding:40px 14px; overflow:auto; }
+  #cmpov.on { display:flex; }
+  #cmpmodal { width:min(1100px,97vw); background:var(--card); border:1px solid var(--line); border-radius:15px; padding:20px 22px; position:relative; }
+  .cmpx { position:absolute; top:12px; right:14px; background:none; border:0; color:var(--mut); font-size:22px; cursor:pointer; }
+  .cmptbl { width:100%; border-collapse:collapse; font-size:13px; }
+  .cmptbl th, .cmptbl td { padding:8px 11px; border-top:1px solid var(--line); text-align:right; white-space:nowrap; }
+  .cmptbl thead th { position:sticky; top:0; background:var(--card); text-align:right; vertical-align:bottom; color:var(--ink); }
+  .cmptbl .cmpk, .cmptbl thead th.cmpk { text-align:left; color:var(--mut); position:sticky; left:0; background:var(--card); }
+  .cmptbl thead th .cmpsub { color:var(--mut); font-size:11px; font-weight:400; }
+  .cmphint { color:var(--mut); padding:24px 4px; }
+  .cmptools { display:flex; gap:8px; justify-content:flex-end; margin-bottom:10px; }
+  .cmptools button { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:8px; padding:6px 12px; font-size:12px; cursor:pointer; }
+  .cmptools button:hover { border-color:var(--blue); }
+  @media print {
+    body.printing-compare * { visibility:hidden; }
+    body.printing-compare #cmpov, body.printing-compare #cmpov * { visibility:visible; }
+    body.printing-compare #cmpov { position:absolute; inset:0; background:#fff; display:block; padding:0; overflow:visible; }
+    body.printing-compare #cmpmodal { width:100%; box-shadow:none; border:0; background:#fff; color:#000; }
+    body.printing-compare #cmpmodal h3 { color:#000; }
+    body.printing-compare .cmpx, body.printing-compare .no-print { display:none !important; }
+    body.printing-compare .cmptbl th, body.printing-compare .cmptbl td { color:#000; border-color:#999; }
+    body.printing-compare .cmptbl thead th, body.printing-compare .cmptbl .cmpk { background:#fff; color:#000; }
+    body.printing-compare .cmpsub { color:#444; }
+  }
+</style>
+<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 removed (no floating pop-out) -->
+  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+<script src="/crcp-cities.js"></script><script src="/crcp-scope.js"></script></head>
+<body>
+<header>
+  <button class="burger" id="burger" aria-label="Open filters">☰ Filters <span class="fcount" id="fcount">0</span></button>
+  <div class="htitle">
+    <h1>LA County CRE — Investment Explorer</h1>
+    <div class="sub" id="sub">loading…</div>
+  </div>
+  <div class="spacer"></div>
+  <div class="topnav">
+  <a href="/crcp.html" style="color:var(--warn);text-decoration:none;font-size:13px;margin-right:12px" title="Commercial Real Estate Control Panel (live)">🛰️ CRCP</a>
+  <a href="/map.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All properties on a map, colored by type, with overhead aerial photos">🗺️ Map</a>
+  <a href="/mls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Spreadsheet-style listings + grid — all property data, sortable & searchable">📋 Listings</a>
+  <a href="/valley-pools.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="San Fernando Valley single-family pool homes under $1.2M — deal-colored map + $/sqft + price-cut signals + payment estimate">🏊 Valley Pools</a>
+  <a href="/deals.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Liquid-glass deal-intelligence charts">◆ Deals</a>
+  <a href="/deals-flow.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Closed deal flow — public-record sales + FHA + SEC REIT, source-badged">💰 Deal Flow</a>
+  <a href="/rent-rolls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Rent-roll → DSCR underwriting">📑 Rent Rolls</a>
+  <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All brokers/agents — grid + list, sortable by every field">👥 Broker grid</a>
+  <a href="/brokers.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent relationship mind-map">🕸️ Broker map</a>
+  <a href="/graphics.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / firm / warrantability charts">📊 Graphics</a>
+  <a href="/condos.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LA condos — FHA warrantability">🏢 Condos</a>
+  <a href="/fha-loans.html" style="color:var(--gold,#e9c46a);text-decoration:none;font-size:13px;margin-right:12px" title="HUD/FHA multifamily loans originally ≤3.5% — historic-low-rate flagged">◆ FHA Loans</a>
+  <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent contacts — name, company, phone, email">👤 Brokers</a>
+  <a href="/linkedin.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LinkedIn follow-list">🔗 LinkedIn</a>
+  <a href="/licensed.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Licensed directory — RE agents, firms, title & escrow + firm IG">📋 Licensed</a>
+  </div><!-- /.topnav -->
+</header>
+
+<!-- Real natural-language chat bar at the TOP — "4 units in los angeles" → drives filters + re-renders (client-side, no server) -->
+<div id="chatbar" style="padding:9px 18px 10px;border-bottom:1px solid var(--line);background:var(--bg2,var(--card));position:sticky;top:57px;z-index:20;">
+  <div style="display:flex;align-items:center;gap:9px;max-width:860px;margin:0 auto;width:100%;">
+    <span style="font-size:16px" title="Ask for properties in plain English">💬</span>
+    <input id="chatq" type="text" autocomplete="off" spellcheck="false"
+      placeholder="Ask for what you want — e.g.  “4 units in Los Angeles”  ·  “multifamily under $2M”  ·  “retail 6% cap”  ·  “all cash”  (Enter)"
+      style="flex:1;min-width:0;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:9px 13px;font-size:14px;">
+    <button id="chatgo" class="ex" title="Apply this request">Ask ▸</button>
+    <button id="chatclear" class="ex" title="Clear the chat filters">Clear</button>
+    <span id="chatmsg" style="font-size:12px;color:var(--mut);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:22%"></span>
+  </div>
+  <div id="chatex" style="display:flex;flex-wrap:wrap;gap:6px;margin:7px auto 0;max-width:860px;justify-content:center;font-size:11px;color:var(--mut);align-items:center;">
+    <span style="opacity:.7">Try:</span>
+    <button class="chatchip" data-q="4 units in Los Angeles">4 units in Los Angeles</button>
+    <button class="chatchip" data-q="unwarrantable condos">unwarrantable condos</button>
+    <button class="chatchip" data-q="multifamily under $2M">multifamily under $2M</button>
+    <button class="chatchip" data-q="retail 6% cap">retail 6% cap</button>
+    <button class="chatchip" data-q="office in Pasadena">office in Pasadena</button>
+    <button class="chatchip" data-q="non-QM condos">non-QM condos</button>
+    <button class="chatchip" data-q="built after 2000">built after 2000</button>
+    <button class="chatchip" data-q="all cash">all cash</button>
+  </div>
+</div>
+<style>.chatchip{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:20px;padding:3px 11px;font-size:11px;cursor:pointer;transition:all .12s;} .chatchip:hover{border-color:var(--blue,#3b82f6);color:var(--blue,#3b82f6);}</style>
+
+<div id="scrim"></div>
+<div class="shell">
+<aside id="drawer">
+  <div class="dhead"><b>Filters</b><button class="x" id="dclose" aria-label="Close">✕</button></div>
+
+  <div class="fsec"><h4>Financing scenario</h4>
+    <span class="seg" id="scenario">
+      <button data-s="25" class="active">25% down</button>
+      <button data-s="30">30% down</button>
+      <button data-s="cash">All cash</button>
+    </span>
+    <div class="small" style="margin-top:6px">Underwriting lens — drives cash-on-cash, DSCR, cash-needed &amp; Deal Score across the view.</div>
+  </div>
+  <div class="fsec" id="fieldsSec"><h4>Fields — show / hide · reorder</h4>
+    <div class="fieldtoggles" id="fFields"></div>
+    <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fDef">Default</button><button class="minibtn" id="idxReset" title="Reset List columns to the tight default — order, widths &amp; visibility">↺ Reset layout</button></div>
+    <div class="small" style="margin-top:6px">Drag a row by its ⠿ handle to reorder columns. ▤ = also a column in List view · drag a List header to reorder · drag its right edge to resize</div>
+  </div>
+  <div class="fsec"><h4>Saved searches</h4><div id="savedList"></div><button id="savebtn">💾 Save current view</button></div>
+  <div class="fsec"><h4>Asset type</h4><div class="chips" id="fType"></div></div>
+  <div class="fsec"><h4>Listing firm</h4><div class="chips" id="fFirm"></div></div>
+  <div class="fsec"><h4>Submarket / city</h4>
+    <input type="text" id="fCity" placeholder="search city or street… e.g. Encino, Ventura Blvd">
+    <div class="chips" id="fCityChips" style="margin-top:8px"></div>
+  </div>
+  <div class="fsec"><h4>Price</h4>
+    <div class="row2"><div class="fld"><label>Min $</label><input type="number" id="fPriceMin" placeholder="any" step="50000"></div>
+    <div class="fld"><label>Max $</label><input type="number" id="fPriceMax" placeholder="any" step="50000"></div></div>
+  </div>
+  <div class="fsec"><h4>Cap rate <span class="rangeval" id="capVal">any</span></h4>
+    <input type="range" id="fCap" min="0" max="10" step="0.25" value="0">
+    <div class="small">minimum broker cap rate (0 = any)</div>
+  </div>
+  <div class="fsec"><h4>Units / size</h4>
+    <div class="row2"><div class="fld"><label>Min units</label><input type="number" id="fUnits" placeholder="any" min="0" step="1"></div>
+    <div class="fld"><label>Max units</label><input type="number" id="fUnitsMax" placeholder="any" min="0" step="1"></div></div>
+    <div class="row2" style="margin-top:8px"><div class="fld"><label>Built after</label><input type="number" id="fYear" placeholder="any" min="1900" max="2026" step="1"></div>
+    <div class="fld"><label>Built before</label><input type="number" id="fYearMax" placeholder="any" min="1900" max="2026" step="1"></div></div>
+  </div>
+  <div class="fsec"><h4>Status &amp; budget</h4>
+    <div class="chips" id="fStatus">
+      <span class="chip" data-st="all">All</span>
+      <span class="chip" data-st="active">Active only</span>
+    </div>
+    <label class="toggle" style="margin-top:10px"><input type="checkbox" id="fAfford"> In budget ($400k down) only</label>
+    <!-- Broker/firm contactability filters (Steve 2026-08-18): narrow to listings whose resolved broker/firm contact has a phone / email — so a reachable contact exists before working the lead. -->
+    <label class="toggle" style="margin-top:10px"><input type="checkbox" id="fHasPhone"> Has broker phone</label>
+    <label class="toggle" style="margin-top:6px"><input type="checkbox" id="fHasEmail"> Has broker email</label>
+  </div>
+  <div class="fsec"><h4>Sort &amp; density</h4>
+    <select id="sort">
+      <option value="deal">Deal Score ↓</option>
+      <option value="composite">Best Investment (blended)</option>
+      <option value="coc">Cash-on-Cash ↓</option>
+      <option value="yield">Rent Yield (rent÷price) ↓</option>
+      <option value="cap">Cap Rate ↓</option>
+      <option value="dscr">DSCR ↓</option>
+      <option value="priceAsc">Price ↑</option>
+      <option value="priceDesc">Price ↓</option>
+      <option value="cashNeeded">Cash Needed ↑</option>
+      <option value="unitsDesc">Units ↓</option>
+      <option value="rentRoi">Pro-forma Rent ROI ↓</option>
+    </select>
+    <div style="margin-top:10px"><label class="small">Cards per row (grid) <span class="rangeval" id="densVal">3</span></label>
+      <input type="range" id="density" min="1" max="10" step="1" value="3"></div>
+  </div>
+  <button class="resetbtn" id="reset">Reset all filters</button>
+</aside>
+
+<main>
+  <!-- ===== Brokers & agents — front-page contacts (name / company / phone / email) ===== -->
+  <style>
+    #brokers{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:14px 16px;margin-bottom:18px;}
+    #brokers h2{margin:0 0 2px;font-size:14px;text-transform:uppercase;letter-spacing:.6px;color:var(--warn);}
+    #brokers .bksub{color:var(--mut);font-size:12px;margin-bottom:12px;}
+    #bktbl{width:100%;border-collapse:collapse;font-size:13px;}
+    #bktbl td,#bktbl th{text-align:left;padding:7px 8px;border-top:1px solid var(--line);}
+    #bktbl th{color:var(--mut);font-size:11px;text-transform:uppercase;letter-spacing:.4px;border-top:0;}
+    #bktbl tr.bkrow{cursor:pointer;} #bktbl tr.bkrow:hover td{background:rgba(88,166,255,.06);}
+    #bktbl a{color:var(--blue);text-decoration:none;} #bktbl .miss{color:#6e7681;}
+    .bkatype{display:inline-block;font-size:10px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;padding:1px 6px;border-radius:9px;margin-left:6px;}
+    .bkatype.res{background:rgba(88,166,255,.16);color:var(--blue);} .bkatype.com{background:rgba(210,153,34,.16);color:var(--warn);}
+    #bkov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:60;align-items:flex-start;justify-content:center;padding:40px 14px;overflow-y:auto;}
+    #bkov.on{display:flex;}
+    #bkmodal{width:min(720px,96vw);background:var(--card);border:1px solid var(--line);border-radius:15px;padding:20px 22px;position:relative;}
+    #bkmodal h3{margin:0 0 2px;font-size:20px;} #bkmodal .mfirm{color:var(--warn);font-size:13px;}
+    #bkmodal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
+    #bkmodal .ci{display:flex;gap:16px;flex-wrap:wrap;margin:10px 0;font-size:14px;} #bkmodal .ci a{color:var(--blue);text-decoration:none;}
+    #bkmodal .sec{margin-top:14px;} #bkmodal .sec h4{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);}
+    #bkmodal .litem{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid var(--line);font-size:13px;} #bkmodal .litem .m{color:var(--mut);}
+    .bkbtn{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:7px 12px;font-size:13px;cursor:pointer;margin-right:8px;} .bkbtn:hover{border-color:var(--blue);}
+    .gate{font-size:12px;color:var(--mut);margin-top:8px;}
+    .lead{border:1px solid var(--line);border-radius:11px;margin-bottom:8px;overflow:hidden;background:rgba(255,255,255,.012);}
+    .lead .lhead{display:flex;align-items:center;gap:12px;padding:11px 14px;cursor:pointer;}
+    .lead .lhead:hover{background:rgba(88,166,255,.06);}
+    .lead .rk{font-weight:700;color:var(--warn);width:24px;text-align:center;font-variant-numeric:tabular-nums;}
+    .lead .who{flex:1;min-width:0;} .lead .who .nm{font-weight:600;} .lead .who .fm{color:var(--mut);font-size:12px;}
+    .lead .fit{font-size:11px;font-weight:700;color:var(--acc);background:rgba(63,185,80,.13);border:1px solid rgba(63,185,80,.35);border-radius:8px;padding:2px 8px;white-space:nowrap;}
+    .lead .meta{color:var(--mut);font-size:12px;white-space:nowrap;}
+    .lead .car{color:var(--mut);transition:transform .15s;} .lead.open .car{transform:rotate(90deg);}
+    .lead .body{display:none;padding:4px 14px 12px;border-top:1px solid var(--line);}
+    .lead.open .body{display:block;}
+    .lead .acts{display:flex;gap:8px;margin:10px 0 6px;flex-wrap:wrap;}
+    .figwrap{margin:8px 0 4px;display:flex;flex-direction:column;gap:4px;}
+    .fighd{font-size:11px;color:var(--mut);letter-spacing:.02em;margin-bottom:1px;}
+    .figli{display:flex;gap:9px;align-items:center;padding:5px 7px;border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.02);text-decoration:none;color:inherit;}
+    .figli:hover{border-color:var(--blue);}
+    .figli-static{cursor:default;} .figli-static:hover{border-color:var(--line);}
+    .figt{width:38px;height:38px;flex:0 0 38px;border-radius:6px;object-fit:cover;background:var(--line);display:inline-block;}
+    .figb{display:flex;flex-direction:column;min-width:0;gap:1px;}
+    .figc{font-size:12px;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:min(70vw,560px);}
+    .figm{font-size:10.5px;color:var(--mut);font-variant-numeric:tabular-nums;}
+    .mls{width:100%;border-collapse:collapse;font-size:12.5px;}
+    .mls th,.mls td{text-align:left;padding:5px 7px;border-top:1px solid var(--line);} .mls th{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.4px;border-top:0;}
+    .mls td.n{text-align:right;font-variant-numeric:tabular-nums;} .mls tr.mrow{cursor:pointer;} .mls tr.mrow:hover td{background:rgba(210,153,34,.07);}
+    .mls .hist{color:var(--blue);font-size:11px;}
+    .phitem{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid var(--line);font-size:13px;} .phitem .m{color:var(--mut);}
+  </style>
+  <!-- "Top brokers" people panel removed from the root onload (Steve 2026-08-18: show properties, not people). Broker directory lives at /broker-grid.html. The #bkov modal below is retained — it's shared by the property-grid ✉ contact popup. -->
+  <div id="bkov"><div id="bkmodal"><button class="x" aria-label="Close" onclick="document.getElementById('bkov').classList.remove('on')">✕</button><div id="bkmbody"></div></div></div>
+  <script>
+  (function(){
+    if(!document.getElementById('bkleads')) return;   // "Top brokers" panel removed from root onload — this populate script no-ops (property-grid ✉ uses the global openContact, not this closure).
+    const bq=s=>document.querySelector(s);
+    const money=n=>n?'$'+Number(n).toLocaleString():'—';
+    const esc=s=>(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+    const atype=t=>t==='residential'?'<span class="bkatype res">Residential</span>':'<span class="bkatype com">Commercial</span>';
+    const fdate=s=>{ if(!s) return ''; try{ return new Date(s).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }catch(e){ return ''; } };
+    async function openContact(name,id){
+      const ov=bq('#bkov'), mb=bq('#bkmbody'); ov.classList.add('on'); mb.innerHTML='<div class="gate">loading '+esc(name)+'…</div>';
+      const qs=id?'id='+encodeURIComponent(id):'name='+encodeURIComponent(name);
+      let d; try{ d=await (await fetch('/api/broker?'+qs)).json(); }catch(e){ mb.innerHTML='<div class="gate">error</div>'; return; }
+      if(d.error){ mb.innerHTML='<div class="gate">'+esc(d.error)+'</div>'; return; }
+      const b=d.broker, w=b.website?(/^https?:/.test(b.website)?b.website:'https://'+b.website):null;
+      mb.innerHTML='<h3>'+esc(b.name)+atype(b.agent_type)+'</h3><div class="mfirm">'+esc(b.firm||'—')+(b.title?' · '+esc(b.title):'')+'</div>'
+        +'<div class="ci">'
+        +(b.phone?'<a href="tel:'+esc(b.phone)+'">☎ '+esc(b.phone)+'</a>':'<span class="gate">☎ —</span>')
+        +(b.email?'<a href="mailto:'+esc(b.email)+'">✉ '+esc(b.email)+'</a>':'<span class="gate">✉ —</span>')
+        +(w?'<a href="'+esc(w)+'" target="_blank" rel="noopener noreferrer">🌐 website</a>':'')+'</div>'
+        +'<div class="sec"><h4>Current listings ('+d.current.length+')</h4>'+(d.current.length?d.current.map(l=>'<div class="litem"><span>'+esc(l.address)+', '+esc(l.city)+(l.zip?' '+esc(l.zip):'')+'</span><span class="m">'+esc(l.type)+' · '+money(l.price)+(l.units?' · '+l.units+'u':'')+(l.cap_rate?' · '+l.cap_rate+'% cap':'')+(l.listed_at?' · listed '+fdate(l.listed_at):'')+'</span></div>').join(''):'<div class="gate">none in our set</div>')+'</div>'
+        +'<div class="sec"><h4>Expired / off-market ('+((d.expired&&d.expired.length)||0)+')</h4>'+((d.expired&&d.expired.length)?d.expired.map(x=>'<div class="litem"><span>'+esc(x.address)+', '+esc(x.city)+'</span><span class="m">'+money(x.list_price)+' → sold '+money(x.sold_price)+' · '+fdate(x.sold_date)+(x.mls?' · MLS '+esc(x.mls):'')+((x.url&&!_isAgg(x.url))?' · <a href="'+esc(x.url)+'" target="_blank" rel="noopener noreferrer">↗</a>':'')+'</span></div>').join(''):'<div class="gate">'+esc(d.expiredNote||'none')+'</div>')+'</div>'
+        +'<div class="sec"><h4>Sales — area sold ('+((d.comps&&d.comps.length)||0)+')</h4>'+((d.comps&&d.comps.length)?d.comps.map(c=>'<div class="litem"><span>'+esc(c.address)+', '+esc(c.city)+(c.beds?' · '+c.beds+'bd':'')+(c.baths?'/'+c.baths+'ba':'')+(c.sqft?' · '+Number(c.sqft).toLocaleString()+'sf':'')+'</span><span class="m">'+money(c.sold_price)+' · '+fdate(c.sold_date)+(c.mls?' · MLS '+esc(c.mls):'')+((c.url&&!_isAgg(c.url))?' · <a href="'+esc(c.url)+'" target="_blank" rel="noopener noreferrer">↗</a>':'')+'</span></div>').join(''):'<div class="gate">no area sales</div>')+(d.compsNote?'<div class="gate">'+esc(d.compsNote)+'</div>':'')+'</div>';
+    }
+    bq('#bkov').addEventListener('click',e=>{ if(e.target.id==='bkov') e.currentTarget.classList.remove('on'); });
+    const moneyK=n=>{ if(n==null) return '—'; n=+n; return n>=1e6?'$'+(n/1e6).toFixed(2)+'M':'$'+Math.round(n/1e3)+'k'; };
+    // 100-year property history popup — address-only, NO owner/resident names (by design).
+    async function propHistory(address,city){
+      const ov=bq('#bkov'), mb=bq('#bkmbody'); ov.classList.add('on'); mb.innerHTML='<div class="gate">loading history for '+esc(address)+'…</div>';
+      let d; try{ d=await (await fetch('/api/property-history?address='+encodeURIComponent(address)+'&city='+encodeURIComponent(city||''))).json(); }catch(e){ mb.innerHTML='<div class="gate">error</div>'; return; }
+      const a=d.assessor||d.assessorArchive;
+      let h='<h3>'+esc(address)+'</h3><div class="mfirm">'+esc(city||'')+' · property history (address only — no names)</div>';
+      if(a){ h+='<div class="sec"><h4>Assessor parcel'+(a.roll_year?' ('+esc(a.roll_year)+' roll)':'')+'</h4><div class="ci">'
+        +(a.year_built?'<span>🏗 Built '+a.year_built+'</span>':'')
+        +(a.bedrooms!=null?'<span>🛏 '+a.bedrooms+' bd</span>':'')+(a.bathrooms!=null?'<span>🛁 '+a.bathrooms+' ba</span>':'')
+        +(a.sqft_main?'<span>📐 '+Number(a.sqft_main).toLocaleString()+' sqft</span>':'')+(a.units?'<span>🏠 '+a.units+' units</span>':'')
+        +((a.use_desc2||a.use_desc1||a.use_description)?'<span>🏷 '+esc(a.use_desc2||a.use_desc1||a.use_description)+'</span>':'')
+        +((a.roll_total_value||a.total_value)?'<span>💰 assessed '+money(a.roll_total_value||a.total_value)+'</span>':'')+'</div></div>'; }
+      h+='<div class="sec"><h4>Sold history ('+d.sales.length+')</h4>'+(d.sales.length?d.sales.map(x=>'<div class="phitem"><span>'+esc(x.address)+(x.city?', '+esc(x.city):'')+'</span><span class="m">'+money(x.sold_price)+' · '+String(x.sold_date||'').slice(0,10)+'</span></div>').join(''):'<div class="gate">no sold records on file for this address</div>')+'</div>';
+      if(d.permits&&d.permits.length) h+='<div class="sec"><h4>Permits ('+d.permits.length+')</h4>'+d.permits.map(p=>'<div class="phitem"><span>'+esc(p.permit_type||p.use_description||'permit')+'</span><span class="m">'+esc(String(p.issue_date||'').slice(0,10))+(p.valuation?' · '+money(p.valuation):'')+'</span></div>').join('')+'</div>';
+      if(d.events&&d.events.length) h+='<div class="sec"><h4>Place history</h4>'+d.events.map(ev=>'<div class="phitem"><span>'+esc(ev.summary||ev.kind)+'</span><span class="m">'+esc(String(ev.valid_from||'').slice(0,10))+'</span></div>').join('')+'</div>';
+      if(!a && !d.sales.length && !(d.permits||[]).length) h+='<div class="gate">No assessor record matched this address in the LA County parcel roll (2.4M parcels), and no sold or permit history is on file. Try the street number plus street name exactly as it appears on the deed.</div>';
+      mb.innerHTML=h;
+    }
+    // Sold / area history for a lead (per-broker closings aren't in our data; show area sold comps).
+    async function brokerSold(id,btn){
+      const box=btn.closest('.body').querySelector('.soldbox'); box.innerHTML='<div class="gate">loading sold history…</div>';
+      try{ const d=await (await fetch('/api/broker?id='+id)).json(); const sold=d.comps||[];
+        box.innerHTML='<div class="gate">Per-broker closings need a gated detail-scrape; showing recent <b>area sold comps</b> in this broker’s cities ('+sold.length+'):</div>'
+          +(sold.length?sold.map(c=>'<div class="phitem"><span>'+esc(c.address)+', '+esc(c.city)+'</span><span class="m">'+money(c.sold_price)+' · '+String(c.sold_date||'').slice(0,10)+'</span></div>').join(''):'<div class="gate">none</div>');
+      }catch(e){ box.innerHTML='<div class="gate">'+esc(e.message)+'</div>'; }
+    }
+    // Firm Instagram (Business Discovery — public firm business accounts only).
+    // Loads the last-3-posts cache + firm->handle map, builds a normalized index.
+    window.FIRM_IG={}; window.FIRM_IG_IDX={};
+    const firmIgReady=Promise.all([
+      fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
+      fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}}))
+    ]).then(([cache,map])=>{ window.FIRM_IG=cache||{}; const idx={};
+      for(const [h,al] of Object.entries((map.handles)||{})) for(const a of al) idx[a]=h; window.FIRM_IG_IDX=idx; }).catch(()=>{});
+    const normFirm=s=>String(s||'').toLowerCase().replace(/[^a-z0-9]/g,'');
+    function firmHandle(firm){ const n=normFirm(firm); if(!n) return null;
+      if(window.FIRM_IG_IDX[n]) return window.FIRM_IG_IDX[n];
+      for(const [a,h] of Object.entries(window.FIRM_IG_IDX)){ if(n===a || (a.length>=5 && n.includes(a)) || (a.length>=3 && n.startsWith(a))) return h; } return null; }
+    function firmIgStrip(firm){ const h=firmHandle(firm); if(!h) return '';
+      const c=window.FIRM_IG[h]; if(!c||!c.posts||!c.posts.length) return '';
+      const items=c.posts.slice(0,3).map(p=>{ const cap=esc((p.caption||'').replace(/\s+/g,' ').slice(0,90))||'(no caption)';
+        const when=p.timestamp?String(p.timestamp).slice(0,10):''; const vid=p.media_type==='VIDEO'?'▶ ':'';
+        const thumb=p.image?'<img class="figt" loading="lazy" src="'+esc(p.image)+'" alt="">':'<span class="figt"></span>';
+        return '<a class="figli" href="'+esc(p.permalink)+'" target="_blank" rel="noopener noreferrer">'+thumb+'<span class="figb"><span class="figc">'+vid+cap+' ↗</span><span class="figm">'+(when?when+' · ':'')+'♥ '+(p.likes||0)+' · 💬 '+(p.comments||0)+'</span></span></a>'; }).join('');
+      return '<div class="figwrap"><div class="fighd">📷 @'+esc(h)+' · firm’s latest 3</div>'+items+'</div>'; }
+    // Ranked top brokers — expandable to spreadsheet-style current listings + Sold + per-address history.
+    (function loadBkLeads(_retry){ fetch('/api/leads/top?limit=40').then(r=>{ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); }).then(async d=>{
+      await firmIgReady;
+      const leads=d.leads||[];
+      if(!leads.length){ bq('#bkleads').innerHTML='<div class="gate">No brokers to show yet.</div>'; return; }
+      bq('#bkleads').innerHTML=leads.map((l,i)=>{
+        const rows=(l.listings_json||[]).map(p=>'<tr class="mrow" data-addr="'+esc(p.address)+'" data-city="'+esc(p.city||'')+'">'
+          +'<td>'+esc(p.address)+'<div style="color:var(--mut);font-size:11px">'+esc(p.city||'')+(p.zip?' '+esc(p.zip):'')+'</div>'+(p.created_at?'<div style="color:var(--mut);font-size:10px" title="captured '+esc(String(p.created_at))+'">🕓 '+new Date(p.created_at).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})+'</div>':'')+'</td>'
+          +'<td>'+esc(p.type||'—')+'</td><td class="n">'+money(p.price)+'</td><td class="n">'+(p.units||'—')+'</td>'
+          +'<td class="n">'+(p.cap_rate!=null?p.cap_rate+'%':'—')+'</td><td class="hist">📜 history</td></tr>').join('');
+        return '<div class="lead" data-id="'+l.broker_id+'" data-name="'+esc(l.name)+'">'
+          +'<div class="lhead"><span class="rk">'+(i+1)+'</span>'
+          +'<div class="who"><span class="nm">'+esc(l.name)+'</span>'+atype(l.agent_type)+'<div class="fm">'+esc(l.firm||'—')+'</div></div>'
+          +'<span class="fit">fit '+l.fit_score+'</span><span class="meta">'+l.listings+' listings · '+moneyK(l.total_value)+'</span><span class="car">▸</span></div>'
+          +'<div class="body"><div class="acts"><button class="bkbtn" data-act="contact">✉ Contact</button>'
+          +'<button class="bkbtn" data-act="sold">🔴 Sold / area history</button></div>'
+          +window.firmIgInline(l.firm)
+          +'<table class="mls"><thead><tr><th>Address</th><th>Type</th><th>Price</th><th>Units</th><th>Cap</th><th></th></tr></thead><tbody>'+rows+'</tbody></table>'
+          +'<div class="soldbox"></div></div></div>';
+      }).join('');
+      bq('#bkleads').onclick=e=>{
+        const lead=e.target.closest('.lead'); if(!lead) return;
+        const mrow=e.target.closest('.mrow'); if(mrow){ propHistory(mrow.dataset.addr, mrow.dataset.city); return; }
+        const act=e.target.closest('[data-act]'); if(act){ if(act.dataset.act==='contact') openContact(lead.dataset.name, lead.dataset.id); else brokerSold(lead.dataset.id, act); return; }
+        if(e.target.closest('.lhead')) lead.classList.toggle('open');
+      };
+    }).catch(e=>{ if(_retry>0){ setTimeout(()=>loadBkLeads(_retry-1),1500); return; } bq('#bkleads').innerHTML='<div class="gate">Broker data is temporarily unavailable — please refresh.</div>'; console.warn('leads/top load failed:',e); }); })(1);
+  })();
+  </script>
+  <div class="bar">
+    <span class="count" id="count">…</span>
+    <span class="viewseg" id="viewseg">
+      <button data-view="grid">▦ Grid</button>
+      <button data-view="list">☰ List</button>
+      <button data-view="table">▤ Table</button>
+      <button data-view="kv">🗂 KV</button>
+    </span>
+    <div class="right">
+      <input id="idxSearch" type="text" placeholder="🔎 search all fields — address, firm, agent, type… (space = AND)" style="background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 11px;font-size:12.5px;min-width:230px" title="Filters across EVERY field on each listing (nested-safe). Space = AND.">
+      <button id="shortbtn" class="ex" title="Show only your shortlisted (starred) listings">⭐ Shortlist (0)</button>
+      <button id="cmpbtn" class="ex" title="Compare your shortlisted listings side by side">⚖ Compare</button>
+      <button id="csvbtn" class="ex" title="Download the current filtered + sorted view (visible columns) as CSV">⬇ CSV</button>
+      <button id="linkbtn" class="ex" title="Copy a shareable link to this exact filtered view">🔗 Copy link</button>
+      <select id="heat" title="Color cards/rows by a metric — red (low) → green (high) over the filtered set" style="width:auto"><option value="none">Heat: off</option><option value="deal">Heat: Deal score</option><option value="cap">Heat: Cap rate</option><option value="coc">Heat: Cash-on-cash</option></select>
+      <span id="heatlegend" class="heatlegend">low <span class="bar"></span> high · <span class="hl"></span></span>
+      <label>Sort</label>
+      <select id="sortTop" style="width:auto;min-width:180px"></select>
+    </div>
+  </div>
+  <div class="activebar" id="activebar"></div>
+  <div class="statsbar" id="statsbar"></div>
+  <div class="grid" id="grid"></div>
+  <div class="foot" id="foot"></div>
+</main>
+<aside id="ctrlrail" aria-label="View controls">
+  <div class="railtitle">🧭 Controls</div>
+  <div class="rpanel" data-rp="fields"><button class="rphead" type="button">Fields <span class="caret">▸</span></button><div class="rpbody"><div id="railFields"></div></div></div>
+  <div class="rpanel" data-rp="sort"><button class="rphead" type="button">Sort <span class="caret">▸</span></button><div class="rpbody">
+    <select id="railSort"></select>
+    <label class="rlbl">Direction (any field)</label>
+    <select id="railColSort"><option value="">— sort by column instead —</option></select>
+    <select id="railColDir"><option value="1">Ascending ↑</option><option value="-1">Descending ↓</option></select>
+  </div></div>
+  <div class="rpanel" data-rp="group"><button class="rphead" type="button">Group <span class="caret">▸</span></button><div class="rpbody">
+    <label class="rlbl">Group by field</label>
+    <select id="railGroup"></select>
+  </div></div>
+  <div class="rpanel" data-rp="density"><button class="rphead" type="button">Density <span class="caret">▸</span></button><div class="rpbody">
+    <label class="rlbl">Cards per row (grid) <span class="rangeval" id="railDensVal">3</span></label>
+    <input type="range" id="railDensity" min="1" max="10" step="1" value="3" style="width:100%">
+  </div></div>
+  <div class="rpanel" data-rp="search"><button class="rphead" type="button">Search <span class="caret">▸</span></button><div class="rpbody">
+    <input id="railSearch" type="text" placeholder="🔎 search all fields…">
+  </div></div>
+</aside>
+</div><!-- /shell -->
+
+<div id="chatwrap">
+  <h2>💬 Ask the analyst &nbsp;<span class="small">— grounded in this dataset · free (Claude CLI)</span></h2>
+  <div class="hint">Ask questions <b>or tell it to drive the view</b>: “show only multifamily under $1M at 30% down sorted by cash-on-cash”, “take me to the Reseda mixed-use”.</div>
+  <div id="log"></div>
+  <div class="examples" id="examples"></div>
+  <form id="chatform" autocomplete="off">
+    <input id="q" placeholder="Ask about the deals, or tell me to filter / sort / change financing…">
+    <button id="send" type="submit">Send</button>
+  </form>
+</div>
+
+<div id="cmpov"><div id="cmpmodal"><button class="cmpx" aria-label="Close" onclick="document.getElementById('cmpov').classList.remove('on')">✕</button><h3 style="margin:0 0 10px">⚖ Compare shortlist</h3><div id="cmpbody"></div></div></div>
+<script src="finance.js"></script>
+<script src="deal-score.js"></script>
+<!-- href-to-deeper-data primitives (shared atoms). CRCP drives drilling through its
+     own native Set-based facet model (data-firmfilter / data-typefilter / data-cityfilter
+     → F.firms/F.types/F.cityQuery → render()→stateToURL()); this shared module is loaded
+     so the atom contract + URL-addressable filter state are the single source of truth. -->
+<script src="/drill.js"></script>
+<!-- click a list row's open space → expand consolidated detail + PRIVATE per-user notes (localStorage, not shared) -->
+<script src="/row-notes.js" defer></script>
+<script>
+const $ = s => document.querySelector(s);
+const $$ = s => Array.from(document.querySelectorAll(s));
+const fmt = n => n==null ? '—' : '$'+Math.round(n).toLocaleString();
+const pct = n => n==null ? '—' : n+'%';
+const cls = (v, good, ok) => v==null?'na':(v>=good?'pos':(v>=ok?'':'neg'));
+
+// ── Dated status line (HARD RULE: every property shows EITHER a current listing
+// with its date OR historical data with dates — never a blank/undated status) ──
+const MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
+function fmtMonYear(d){ return d ? MON[d.getMonth()]+' '+d.getFullYear() : null; }
+function fmtFullDate(d){ return d ? MON[d.getMonth()]+' '+d.getDate()+', '+d.getFullYear() : null; }
+// Pull the most reliable known date for a listing: the refresh/source stamp in the
+// note (ISO YYYY-MM-DD or "Mon D, YYYY" or MM/DD/YYYY), else the dataset's sourced date.
+function listingDate(p){
+  const t = p.upside_note || '';
+  let m = t.match(/(20\d\d)-(\d\d)-(\d\d)/);                                 // 2026-06-27
+  if (m) return { d:new Date(+m[1], +m[2]-1, +m[3]), exact:true };
+  m = t.match(/\b([A-Z][a-z]{2,8})\s+(\d{1,2}),?\s+(20\d\d)/);              // June 21, 2026
+  if (m){ const mi=MON.findIndex(x=>m[1].slice(0,3)===x); if(mi>=0) return { d:new Date(+m[3], mi, +m[2]), exact:true }; }
+  m = t.match(/\b(\d{1,2})\/(\d{1,2})\/(20\d\d)/);                          // 06/27/2026
+  if (m) return { d:new Date(+m[3], +m[1]-1, +m[2]), exact:true };
+  // Fallback: the whole dataset's sourcing date (meta.sourced begins YYYY-MM-DD).
+  const src = (DATA && DATA.meta && DATA.meta.sourced || '').match(/(20\d\d)-(\d\d)-(\d\d)/);
+  if (src) return { d:new Date(+src[1], +src[2]-1, +src[3]), exact:false };
+  return null;
+}
+// The guaranteed status line. Current listing → "Listed <date>". Off-market states
+// (Under Contract / Pending / Sold) → labeled with date as historical. No date at
+// all → an explicit "no recorded history" label, never a blank.
+function statusLine(p){
+  const ld = listingDate(p);
+  const dateTxt = ld ? (ld.exact ? fmtFullDate(ld.d) : fmtMonYear(ld.d)+' (dataset)') : null;
+  const title = ld ? ' title="'+(ld.exact?'Listing data as of '+ld.d.toISOString().slice(0,10):'No per-listing date; using dataset source date '+ld.d.toISOString().slice(0,10))+'"' : '';
+  let verb, klass;
+  if (/Under Contract|Pending|Escrow/i.test(p.status)) { verb='In escrow / '+p.status; klass='warn'; }
+  else if (/Sold|Closed/i.test(p.status))             { verb='Last sold'; klass='mut'; }
+  else if (/OUT OF BUDGET/i.test(p.status))           { verb='Listed (over budget)'; klass='bad'; }
+  else                                                { verb='Listed'; klass='ok'; }
+  if (!dateTxt) return `<div class="statusline none" title="No current listing date and no recorded sale history in this dataset">🕓 ${verb} · no recorded date · no sale history on file</div>`;
+  const priced = p.price ? ' '+fmt(p.price) : '';
+  return `<div class="statusline ${klass}"${title}>🕓 ${verb}${priced} · as of ${dateTxt}</div>`;
+}
+
+const SCEN = { '25':{budget:400000,downPct:25,ratePct:6.75,amortYears:30,closingPct:2.5},
+               '30':{budget:400000,downPct:30,ratePct:6.75,amortYears:30,closingPct:2.5},
+               'cash':{budget:400000,downPct:100,ratePct:0,amortYears:30,closingPct:2.5} };
+
+let DATA=null, scenario='25', history=[];
+window.DEMO = {};  // US Census ACS demographics by lowercased city (data/demographics.json)
+function demoLine(p){
+  const d = window.DEMO[(p.city||'').toLowerCase()]; if(!d) return '';
+  const bits=[];
+  if(d.renter_pct!=null) bits.push(`${d.renter_pct}% renters`);
+  if(d.median_gross_rent) bits.push(`$${d.median_gross_rent.toLocaleString()} med rent`);
+  if(d.median_income) bits.push(`$${Math.round(d.median_income/1000)}k income`);
+  if(d.population) bits.push(`${Math.round(d.population/1000)}k pop`);
+  return bits.length ? `<div class="small" title="US Census ACS 2022 — ${p.city} (ZCTA ${d.zcta})">📊 ${bits.join(' · ')}</div>` : '';
+}
+// Neighborhood-based rent estimate from Census median gross rent. std = market median × units
+// (conservative in-place proxy); proforma = +12% market upside. Honest estimate, NOT an appraisal/1007.
+function rentEstimate(p){
+  const d = window.DEMO[(p.city||'').toLowerCase()]; if(!d || !d.median_gross_rent) return null;
+  const base = d.median_gross_rent, units = p.units || 1;
+  const std = Math.round(base*units), pro = Math.round(std*1.12);
+  const annualPro = pro*12;
+  return { std, pro, base, units, zcta:d.zcta, grossYieldPro: p.price? +(100*annualPro/p.price).toFixed(2):null };
+}
+function rentLine(p){
+  const r = rentEstimate(p); if(!r) return '';
+  // Plausibility guard: LA gross yields run ~5-10%. >18% almost always means a bad units count, not a deal.
+  const fishy = r.grossYieldPro && r.grossYieldPro > 18;
+  const yld = r.grossYieldPro ? ` · ${r.grossYieldPro}% gross yld (pro)${fishy?` <span class="b warn" title="Implausibly high — likely a bad unit count on the listing; verify.">⚠ verify units</span>`:''}` : '';
+  return `<div class="small" title="Estimate from Census median gross rent ($${r.base.toLocaleString()}/unit, ${p.city} ZCTA ${r.zcta}) × ${r.units} unit${r.units>1?'s':''}. Not an appraisal — DSCR screen only.">💵 Est. rent: <b style="color:var(--ink)">$${r.std.toLocaleString()}/mo</b> std · <b style="color:var(--acc)">$${r.pro.toLocaleString()}/mo</b> proforma${yld}</div>`;
+}
+// For ranking, treat implausible (>18%) pro-forma yields as low-confidence so bad-units outliers don't top the list.
+function rentRoiScore(p){ const r=rentEstimate(p); if(!r||!r.grossYieldPro) return -1e9; return r.grossYieldPro>18 ? -1 : r.grossYieldPro; }
+
+// ===== Field show/hide + list/grid view + extra data points =====
+const esc = s => String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+// Every data field the UI can render. card=shows on the grid card · col=available as a List column · def=default-on.
+const FIELDS = [
+  {k:'score',      label:'Investment score',  card:1, col:1, def:1},
+  {k:'deal',       label:'Deal score',        card:1, col:1, def:1},
+  {k:'agent',      label:'Agent · contact',   card:1, col:1, def:1},
+  {k:'phone',      label:'Agent phone',       card:1, col:1, def:1},
+  {k:'email',      label:'Email',             card:1, col:1, def:0},
+  {k:'firmweb',    label:'Firm website',      card:1, col:1, def:0},
+  {k:'firm',       label:'Listing firm',      card:1, col:1, def:1},
+  {k:'type',       label:'Asset type',        card:1, col:1, def:1},
+  {k:'units',      label:'Units',             card:1, col:1, def:1},
+  {k:'price',      label:'Price',             card:1, col:1, def:1},
+  {k:'ppu',        label:'Price / unit',      card:1, col:1, def:1},
+  {k:'pricePerSf', label:'Price / SF',        card:1, col:1, def:0},
+  {k:'cap',        label:'Cap rate',          card:1, col:1, def:1},
+  {k:'verified',   label:'Cap-verified flag', card:1, col:0, def:1},
+  {k:'yield',      label:'Rent yield',        card:1, col:1, def:1},
+  {k:'grm',        label:'GRM (rent mult.)',  card:1, col:1, def:0},
+  {k:'yearBuilt',  label:'Year built',        card:1, col:1, def:0},
+  {k:'dom',        label:'Days on market',    card:1, col:1, def:0},
+  {k:'zip',        label:'ZIP',               card:0, col:1, def:0},
+  {k:'status',     label:'Status badge',      card:1, col:1, def:1},
+  {k:'afford',     label:'Affordability',     card:1, col:1, def:1},
+  {k:'rec',        label:'AI recommendation', card:1, col:1, def:0},
+  {k:'statusline', label:'Listing-date line', card:1, col:0, def:1},
+  {k:'brokerflag', label:'Prior-broker flag', card:1, col:0, def:1},
+  {k:'txhist',     label:'Transaction history',card:1,col:0, def:0},
+  {k:'brokerline', label:'Current broker',    card:1, col:1, def:1},
+  {k:'brokerPhone',label:'Broker phone',      card:0, col:1, def:1},
+  {k:'coc',        label:'Cash-on-cash',      card:1, col:1, def:1},
+  {k:'dscr',       label:'DSCR',              card:1, col:1, def:1},
+  {k:'noi',        label:'Est. NOI',          card:1, col:1, def:1},
+  {k:'debt',       label:'Debt service /yr',  card:1, col:1, def:1},
+  {k:'cashNeeded', label:'Cash needed',       card:1, col:1, def:1},
+  {k:'cashFlow',   label:'Cash flow /yr',     card:1, col:1, def:1},
+  {k:'thesis',     label:'AI thesis',         card:1, col:0, def:1},
+  {k:'risks',      label:'Risks',             card:1, col:0, def:0},
+  {k:'upside',     label:'Upside note',       card:1, col:0, def:1},
+  {k:'demo',       label:'Demographics',      card:1, col:0, def:1},
+  {k:'rent',       label:'Rent estimate',     card:1, col:1, def:1},
+  {k:'links',      label:'Listing links',     card:1, col:0, def:1},
+  {k:'beds',       label:'Beds (condo)',       card:1, col:1, def:0},
+  {k:'baths',      label:'Baths (condo)',      card:1, col:1, def:0},
+  {k:'sqft',       label:'Sq Ft',              card:1, col:1, def:0},
+  {k:'hoa',        label:'HOA / mo (condo)',   card:1, col:1, def:0},
+  {k:'warr',       label:'Warrantability',     card:1, col:1, def:0},
+  {k:'broker',     label:'Broker (condo)',     card:1, col:1, def:1},
+  {k:'assessed',   label:'Assessed value ✦',   card:1, col:0, def:0, assessor:1},
+  {k:'landimp',    label:'Land / Improvement ✦',card:1, col:0, def:0, assessor:1},
+  {k:'usecode',    label:'Assessor use code ✦', card:1, col:0, def:0, assessor:1},
+  {k:'priorsale',  label:'Prior sale ✦',        card:1, col:0, def:0, assessor:1},
+];
+// Steve (2026-08-20): ALWAYS show agent · contact, phone, listing firm, current broker — never hideable,
+// override any persisted localStorage off-state (a prior toggle-off used to hide them forever despite def:1).
+const ALWAYS_ON = ['agent','phone','brokerPhone','firm','brokerline','broker'];
+const VIS = (function(){ let s={}; try{ s=JSON.parse(localStorage.getItem('cre_fields')||'{}'); }catch(e){} const o={}; FIELDS.forEach(f=>o[f.k]=ALWAYS_ON.includes(f.k)?true:((f.k in s)?!!s[f.k]:!!f.def)); return o; })();
+function vis(k){ return !!VIS[k]; }
+function saveVis(){ ALWAYS_ON.forEach(k=>VIS[k]=true); try{ localStorage.setItem('cre_fields', JSON.stringify(VIS)); }catch(e){} }
+let view = localStorage.getItem('cre_view') || 'list';
+let groupBy = localStorage.getItem('cre_group') || '';   // group-by-any-field; '' = off
+let colSort = null;  // {key, dir} — click-to-sort on a List column; overrides the sort dropdown when set
+const SHORT = new Set((function(){ try{ return JSON.parse(localStorage.getItem('cre_shortlist')||'[]'); }catch(e){ return []; } })());
+function saveShort(){ try{ localStorage.setItem('cre_shortlist', JSON.stringify([...SHORT])); }catch(e){} }
+function toggleStar(id){ SHORT.has(id)?SHORT.delete(id):SHORT.add(id); saveShort(); }
+let heat = localStorage.getItem('cre_heat') || 'none';
+function heatVal(p){ const f=p.finance||{}; return heat==='deal'?(p.deal&&p.deal.score):heat==='cap'?p.cap_rate:heat==='coc'?f.coc:null; }
+function heatColor(t){ t=Math.max(0,Math.min(1,t)); return `hsl(${Math.round(t*120)},70%,45%)`; }
+function applyHeat(rows){
+  const lg=$('#heatlegend'); if(lg){ lg.classList.toggle('on',heat!=='none'); if(heat!=='none'){ lg.querySelector('.hl').textContent={deal:'Deal score',cap:'Cap rate',coc:'Cash-on-cash'}[heat]; } }
+  if(heat==='none') return;
+  const vals=rows.map(heatVal).filter(v=>v!=null&&isFinite(v)); if(!vals.length) return;
+  const min=Math.min(...vals), max=Math.max(...vals), span=(max-min)||1;
+  const gc=$('#grid').classList;
+  // Per-view heat target: Table stripes the sticky cell; List/KV stripe the row/card via a left inset;
+  // Grid tints the card left border. (Regression fix TK-10747: List + KV were previously un-heated.)
+  const mode = gc.contains('listhost') ? 'table' : gc.contains('compacthost') ? 'compact' : gc.contains('kvhost') ? 'kv' : 'grid';
+  const targetFor=id => mode==='table' ? document.querySelector('.listtbl tr[data-id="'+id+'"] td.stick')
+    : mode==='compact' ? document.querySelector('#grid .crow[data-id="'+id+'"]')
+    : mode==='kv' ? document.querySelector('#grid .kvcard[data-id="'+id+'"]')
+    : document.getElementById('card-'+id);
+  const usesBoxShadow = mode==='table'||mode==='compact'||mode==='kv';
+  rows.forEach(p=>{ const v=heatVal(p);
+    const el=targetFor(p.id); if(!el) return;
+    if(v==null||!isFinite(v)){ if(usesBoxShadow) el.style.boxShadow=''; else el.style.borderLeft=''; return; }
+    const col=heatColor((v-min)/span);
+    if(usesBoxShadow) el.style.boxShadow='inset 5px 0 0 '+col; else el.style.borderLeft='6px solid '+col;
+  });
+}
+// ===== right-hand control rail: disclosure panels (collapsed on load) + nav-agent controls =====
+function wireCtrlRail(densVal){
+  const rail=$('#ctrlrail'); if(!rail) return;
+  // Disclosure panels — collapsed by default; open only if a saved cre_rail_<name>==='1'.
+  rail.querySelectorAll('.rpanel').forEach(p=>{
+    const name=p.dataset.rp; const saved=localStorage.getItem('cre_rail_'+name);
+    if(saved==='1') p.classList.add('open');   // default (null) = collapsed
+    const head=p.querySelector('.rphead');
+    head.setAttribute('aria-expanded', p.classList.contains('open')?'true':'false');
+    head.onclick=()=>{ const open=p.classList.toggle('open'); head.setAttribute('aria-expanded',open?'true':'false'); localStorage.setItem('cre_rail_'+name, open?'1':'0'); };
+  });
+  // Sort — mirror the existing named SORTS (#sort/#sortTop).
+  const rs=$('#railSort'); if(rs){ rs.innerHTML=$('#sort').innerHTML; rs.value=$('#sort').value;
+    rs.onchange=()=>{ colSort=null; $('#sort').value=rs.value; $('#sortTop').value=rs.value; if($('#railColSort'))$('#railColSort').value=''; render(); }; }
+  // Sort-by-ANY-field asc/desc via colSort (uses fieldVal).
+  const groupable=[{k:'addr',label:'Property'}].concat(FIELDS.filter(f=>f.col||['type','firm','status','city'].includes(f.k)));
+  const rcs=$('#railColSort'); if(rcs){ rcs.innerHTML='<option value="">— sort by column instead —</option>'+groupable.map(f=>`<option value="${f.k}">${esc(f.label)}</option>`).join('');
+    if(colSort) rcs.value=colSort.key;
+    const applyColSort=()=>{ const k=rcs.value; if(!k){ colSort=null; } else { colSort={key:k, dir:+($('#railColDir').value)||1}; } render(); };
+    rcs.onchange=applyColSort; $('#railColDir').onchange=()=>{ if(rcs.value) applyColSort(); };
+    if(colSort&&$('#railColDir')) $('#railColDir').value=String(colSort.dir); }
+  // Group-by-ANY-field.
+  const rg=$('#railGroup'); if(rg){ rg.innerHTML='<option value="">— no grouping —</option>'+groupable.map(f=>`<option value="${f.k}">${esc(f.label)}</option>`).join('');
+    rg.value=groupBy; rg.onchange=()=>{ groupBy=rg.value; localStorage.setItem('cre_group',groupBy); render(); }; }
+  // Density — mirror #density.
+  const rd=$('#railDensity'); if(rd){ rd.value=densVal; $('#railDensVal').textContent=densVal;
+    rd.oninput=()=>{ document.documentElement.style.setProperty('--cols',rd.value); $('#railDensVal').textContent=rd.value; if($('#density')){$('#density').value=rd.value; $('#densVal').textContent=rd.value;} localStorage.setItem('cre_density',rd.value); }; }
+  // Search — synced to the top #idxSearch.
+  const rse=$('#railSearch'); if(rse){ const top=$('#idxSearch'); if(top) rse.value=top.value;
+    rse.oninput=()=>{ idxQuery=rse.value.trim().toLowerCase(); if(top) top.value=rse.value; render(); }; }
+}
+function buildFieldToggles(){
+  const host=$('#fFields'); if(!host) return;
+  // emit rows in COLORDER so panel order matches list column order; non-col fields appended after
+  const ordered=(function(){
+    syncColOrder();
+    const colFs=COLORDER.map(k=>FIELDS.find(f=>f.k===k)).filter(Boolean);
+    const rest=FIELDS.filter(f=>!f.col);
+    return colFs.concat(rest);
+  })();
+  host.innerHTML = ordered.map(f=>`<label class="ftog" draggable="true" data-fk="${f.k}"><span class="fdrag" title="Drag to reorder column">⠿</span><input type="checkbox" data-fk="${f.k}" ${vis(f.k)?'checked':''}><span class="flbl">${f.label}</span>${f.col?'<span class="colbadge" title="Also shows as a column in List view">▤</span>':''}</label>`).join('');
+  // Mirror into the right-rail Fields panel (checkbox-only, shares the same VIS state; no drag here).
+  const rail=$('#railFields'); if(rail){ rail.innerHTML=ordered.map(f=>`<label class="ftog" data-fk="${f.k}"><input type="checkbox" data-fk="${f.k}" ${vis(f.k)?'checked':''}><span class="flbl">${f.label}</span></label>`).join(''); }
+}
+// ---- extra computed data points ----
+function ppu(p){ return (p.price&&p.units)?Math.round(p.price/p.units):null; }
+function ppsf(p){ return (p.price&&p.sqft)?Math.round(p.price/p.sqft):null; }
+function grm(p){ const r=rentEstimate(p); if(!r||!r.std) return null; const annual=r.std*12; return annual?+(p.price/annual).toFixed(1):null; }
+function daysOnMarket(p){ const ld=listingDate(p); if(!ld||!ld.exact) return null; const d=Math.round((Date.now()-ld.d.getTime())/86400000); return d>=0?d:null; }
+function shortStatus(p){ if(/Under Contract|Pending|Escrow/i.test(p.status)) return p.status; if(/Sold|Closed/i.test(p.status)) return 'Sold'; if(/OUT OF BUDGET/i.test(p.status)) return 'Over budget'; return 'Active'; }
+// ---- list-view renderer ----
+// ── PRIMARY-TABLE column ORDER (Steve list-build rule: draggable + persisted per-browser) ──
+// COLORDER is the display order over the col-eligible FIELDS keys; new/removed keys are
+// reconciled on every read. Persisted as idxColOrder. The always-first "Property" (addr)
+// stick column is NOT part of COLORDER — it's a fixed anchor, reordering happens among the rest.
+let COLORDER=(function(){ let a=[]; try{ a=JSON.parse(localStorage.getItem('idxColOrder')||'[]'); }catch(e){} return Array.isArray(a)?a:[]; })();
+function colKeys(){ return FIELDS.filter(f=>f.col).map(f=>f.k); }               // every col-eligible field key
+function syncColOrder(){ const keys=colKeys();
+  COLORDER=COLORDER.filter(k=>keys.includes(k));                                // prune removed
+  // Insert any NEW col key next to its canonical FIELDS neighbor (its preceding col-field), NOT at the
+  // end — so newly-added fields (Phone/Email/Firm website) surface beside Agent·contact where they
+  // belong, instead of stranded at the bottom of a persisted column order (Steve 2026-08-18).
+  keys.forEach(k=>{ if(COLORDER.includes(k)) return;
+    const ci=keys.indexOf(k); let anchor=null;
+    for(let j=ci-1;j>=0;j--){ if(COLORDER.includes(keys[j])){ anchor=keys[j]; break; } }
+    if(anchor==null){ COLORDER.unshift(k); }
+    else { COLORDER.splice(COLORDER.indexOf(anchor)+1,0,k); }
+  });
+}
+function saveColOrder(){ try{ localStorage.setItem('idxColOrder',JSON.stringify(COLORDER)); }catch(e){} }
+// Ordered, col-eligible FIELDS in the user's chosen order.
+function orderedCols(){ syncColOrder(); return COLORDER.map(k=>FIELDS.find(f=>f.k===k)).filter(Boolean); }
+// The visible List columns, in order — drives the header + body + resize signature.
+function listCols(){ return orderedCols().filter(f=>vis(f.k)); }
+// Column-set/order signature guard: when it changes, hand the table back to col-resize.js so
+// it rebuilds its <colgroup> + drag handles for the current columns (Steve list-build rule 1).
+let lastColSig='';
+function reinitResize(){
+  const tbl=document.querySelector('#grid .listtbl'); if(!tbl) return;
+  const cg=tbl.querySelector('colgroup[data-cr]'); if(cg) cg.remove();
+  delete tbl.dataset.crDone; tbl.style.tableLayout=''; tbl.style.width='';
+  if(window.ColResize && window.ColResize.refresh) window.ColResize.refresh();
+}
+function colCell(k,p){ const f=p.finance||{};
+  switch(k){
+    case 'score': return p.composite??'—';
+    case 'deal': return (p.deal&&p.deal.score!=null)?p.deal.score:'—';
+    case 'agent': return agentContactCell(p);
+    case 'phone': { const a=agentInfo(p); if(!a.phone) return '—';
+      const tier=a.phoneTier==='firm'?' <span class="tierb" title="Firm branch line'+(a.phoneCity?' — '+esc(a.phoneCity)+' office':'')+'">firm</span>':'';
+      return `<span class="ain"><a href="${telHref(a.phone)}" title="Call ${esc(a.phone)}">📞 ${esc(a.phone)}</a>${tier} <button class="copyb" data-copyval="${esc(a.phone)}" title="Copy phone">⧉</button></span>`; }
+    case 'email': { const a=agentInfo(p); return a.email?`<span class="ain"><a href="mailto:${esc(a.email)}" title="Email ${esc(a.email)}">✉ ${esc(a.email)}</a> <button class="copyb" data-copyval="${esc(a.email)}" title="Copy email">⧉</button></span>`:'—'; }
+    case 'brokerPhone': { const bp=brokerPhoneOf(p); if(!bp) return '—';
+      return `<span class="ain"><a href="${telHref(bp)}" title="Call broker ${esc(bp)}">📞 ${esc(bp)}</a> <button class="copyb" data-copyval="${esc(bp)}" title="Copy broker phone">⧉</button></span>`; }
+    case 'firmweb': { const a=agentInfo(p); const w=a.firmWebsite; if(!w) return '—';
+      const disp=String(w).replace(/^https?:\/\//,'').replace(/\/$/,''); const href=/^https?:\/\//.test(w)?w:'https://'+w;
+      return `<a class="ain" href="${esc(href)}" target="_blank" rel="noopener noreferrer" title="Open the listing firm’s website">🔗 ${esc(disp)}</a>`; }
+    case 'firm': return firmCell(p);
+    case 'type': return esc(p.type||'—');
+    case 'units': return p.units||'—';
+    case 'price': return fmt(p.price);
+    case 'ppu': { const v=ppu(p); return v?'$'+v.toLocaleString():'—'; }
+    case 'pricePerSf': { const v=ppsf(p); return v?'$'+v.toLocaleString():'—'; }
+    case 'cap': return p.cap_rate!=null?p.cap_rate+'%':'—';
+    case 'yield': return f.grossYield?f.grossYield+'%':'—';
+    case 'grm': { const v=grm(p); return v!=null?v:'—'; }
+    case 'yearBuilt': return p.year_built||'—';
+    case 'dom': { const v=daysOnMarket(p); return v!=null?v+'d':'—'; }
+    case 'zip': return p.zip||'—';
+    case 'status': return esc(shortStatus(p));
+    case 'afford': return f.affordable?'✓ in budget':'✗ over';
+    case 'rec': return (p.qwen&&p.qwen.recommendation)||'—';
+    case 'brokerline': { const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()]; const nm=(bh&&bh.current_broker)?(bh.current_broker.name||bh.current_broker.firm):null; return nm?agentCell(nm,(bh.current_broker&&bh.current_broker.firm)||firmLabel(p)):'—'; }
+    case 'coc': return f.coc!=null?f.coc+'%':'—';
+    case 'dscr': return f.dscr??'—';
+    case 'noi': return fmt(f.noi);
+    case 'debt': return fmt(f.annualDebtService);
+    case 'cashNeeded': return fmt(f.cashNeeded);
+    case 'cashFlow': return fmt(f.cashFlow);
+    case 'rent': { const r=rentEstimate(p); return r?'$'+r.std.toLocaleString():'—'; }
+    case 'beds': return p.beds??'—';
+    case 'baths': return p.baths??'—';
+    case 'sqft': return p.sqft?(+p.sqft).toLocaleString():'—';
+    case 'hoa': return p.hoa?'$'+(+p.hoa).toLocaleString():'—';
+    case 'warr': return esc(p.warrantable_status||'—');
+    case 'broker': return p.broker_name?agentCell(p.broker_name,firmLabel(p)):'—';
+    default: return '—';
+  }
+}
+// arrow as a CSS CLASS (asc/desc), NOT literal text — keeps the <th> text stable so col-resize's
+// tableSig doesn't change on sort and saved column widths survive the re-render (TK-10089).
+// "Property" (addr) stays a fixed anchor; the rest are draggable to reorder + click to sort.
+// renderListShell (defined above) builds the identical thead for both renderList and renderTableGrouped.
+function renderList(rows){ return renderListShell(rows.map(renderListBodyRow).join('')); }
+// The inline panel that opens under a list row: deep property/agent searches + the agent CRM
+// (editable contact info, mark-contacted, saved-letter history, compose). Reuses the same
+// #lc-/#hist- containers the card view uses so fetchLiveComps/fetchHistory work unchanged.
+function expandBody(p){
+  const a=agentInfo(p), firm=a.firm, id=p.id;
+  // In-app actions ONLY — no external links. Live comps + Property history render inline in the
+  // panel below; "More from this firm" filters the grid in place.
+  const deep=[];
+  deep.push(`<button class="lcbtn" data-id="${id}" title="Pull fresh comps inline (~$0.03)">🔄 Live comps</button>`);
+  deep.push(`<button class="histbtn" data-id="${id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (inline, free)">📜 Property history</button>`);
+  if(firm&&firm!=='Unknown') deep.push(`<button data-firmfilter="${esc(firm).replace(/"/g,'&quot;')}" title="Show only ${esc(firm)} listings">▦ More from ${esc(firm)}</button>`);
+  // Agent summary + notes preview. Full editing (info, notes, letters, draft) lives in the inline
+  // agent window that ✎ Open launches — no external search, no new tabs.
+  const summary=`<div class="crm-summary">${a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a> `:(isPocket(a)?pocketBadge+' ':'<span class="amut">no agent on file — add via ✎ Open</span> ')}${contactBits(a)}`
+    +`${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}${a.letters.length?`<span class="achip">✎ ${a.letters.length} saved</span>`:''}</div>`
+    +(a.notes?`<div class="crm-note">🗒 ${esc(a.notes)}</div>`:'');
+  return `<div class="src">${deep.join(' ')}</div>`
+    +`<div class="crm">${summary}
+        <div class="crm-acts">
+          <button class="abtn contactbtn" data-contact="${id}">✎ Open agent window</button>
+          <button class="abtn markbtn" data-mark="${id}">${a.contacted_at?'↻ Update contacted date':'✓ Mark contacted'}</button>
+        </div>
+      </div>`
+    +`<div class="lcout" id="lc-${id}"></div><div class="histout" id="hist-${id}"></div>`;
+}
+// raw (unformatted) value for a field, for generic column sorting
+function fieldVal(p,k){ const f=p.finance||{};
+  switch(k){
+    case 'addr': return p.address; case 'score': return p.composite; case 'deal': return p.deal&&p.deal.score;
+    case 'agent': { const a=agentInfo(p); return a.name||('~'); } // contactless rows sort last (~ > letters)
+    case 'phone': { const a=agentInfo(p); return a.phone||'~'; }   // rows without a resolved phone sort last
+    case 'brokerPhone': { const bp=brokerPhoneOf(p); return bp||'~'; }
+    case 'email': { const a=agentInfo(p); return a.email||'~'; }
+    case 'firmweb': { const a=agentInfo(p); return a.firmWebsite||'~'; }
+    case 'firm': return firmLabel(p); case 'type': return p.type; case 'units': return p.units; case 'price': return p.price;
+    case 'ppu': return ppu(p); case 'pricePerSf': return ppsf(p); case 'cap': return p.cap_rate; case 'yield': return f.grossYield;
+    case 'grm': return grm(p); case 'yearBuilt': return p.year_built; case 'dom': return daysOnMarket(p); case 'zip': return p.zip;
+    case 'status': return shortStatus(p); case 'afford': return f.affordable?1:0; case 'rec': return p.qwen&&p.qwen.recommendation;
+    case 'coc': return f.coc; case 'dscr': return f.dscr; case 'noi': return f.noi; case 'debt': return f.annualDebtService;
+    case 'cashNeeded': return f.cashNeeded; case 'cashFlow': return f.cashFlow;
+    case 'rent': { const r=rentEstimate(p); return r?r.std:null; }
+    case 'beds': return p.beds; case 'baths': return p.baths; case 'sqft': return p.sqft; case 'hoa': return p.hoa;
+    case 'warr': return p.warrantable_status; case 'broker': return p.broker_name;
+    default: return null;
+  }
+}
+function sortRows(r){
+  if(colSort){ const k=colSort.key, d=colSort.dir;
+    r.sort((a,b)=>{ let x=fieldVal(a,k), y=fieldVal(b,k);
+      const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
+      if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
+      if(x<y) return -d; if(x>y) return d; return 0; });
+  } else {
+    r.sort(SORTS[$('#sort').value]||SORTS.composite);
+  }
+  // group-by: keep the within-group order, but bucket rows so each group is contiguous (stable partition).
+  if(groupBy){
+    const buckets=new Map();
+    r.forEach(p=>{ const gk=groupKeyLabel(p); if(!buckets.has(gk)) buckets.set(gk,[]); buckets.get(gk).push(p); });
+    const keys=[...buckets.keys()].sort((a,b)=>{ const an=!isNaN(+a),bn=!isNaN(+b); if(an&&bn) return +a-+b; return a.localeCompare(b); });
+    const out=[]; keys.forEach(k=>out.push(...buckets.get(k)));
+    r.length=0; r.push(...out);
+  }
+  return r;
+}
+
+// ---- Unified Deal Score (0-100) — DealScore.compute() over the signals we already have. ----
+// Gathers: pro-forma gross yield (Census rent estimate), cap rate, in-place DSCR (finance.js),
+// a PROXY DSCR when no cap is disclosed (est NOI from pro-forma rent vs the active-scenario debt
+// service), and the assumable-FHA/VA bonus. Transparent + labeled; never NaN. Cached on the row.
+function dealScore(p){
+  const r = rentEstimate(p);
+  const grossYieldPro = (r && r.grossYieldPro != null && isFinite(r.grossYieldPro)) ? r.grossYieldPro : null;
+  // Proxy DSCR only when there is NO in-place cap (else the real DSCR carries it). Estimate in-place
+  // NOI as pro-forma annual gross × 0.6 (a 40% commercial expense ratio) and compare to debt service.
+  let proxyDscr = null;
+  const f = p.finance;
+  if (p.cap_rate == null && r && r.pro && f && f.annualDebtService > 0){
+    const estNoi = r.pro * 12 * 0.6;
+    proxyDscr = +(estNoi / f.annualDebtService).toFixed(2);
+  }
+  return DealScore.compute({
+    grossYieldPro,
+    cap_rate: p.cap_rate,
+    verified: p.verified,
+    dscr: f ? f.dscr : null,
+    proxyDscr,
+    assum: p.assum,           // present only if the geocode pipeline tagged this listing (heuristic)
+    fhaApproved: p.fhaApproved
+  });
+}
+// LoopNet cross-reference for EVERY listing — free deep-links ($0, no scrape). Mirrors scripts/sources/loopnet.js.
+const LN_TYPE={'Multifamily':'multifamily','Office':'office','Retail':'retail','Industrial':'industrial','Mixed-use':'mixed-use','Mixed Use':'mixed-use','Land':'land'};
+function loopnetLink(p){
+  const sl=s=>String(s||'').toLowerCase().trim().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
+  const city=p.city?`${sl(p.city)}-ca`:'los-angeles-ca';
+  const ptype=LN_TYPE[p.type]||'';
+  // exact: site-search resolves the specific LoopNet listing for this address if it exists.
+  const exact=`https://www.google.com/search?q=${encodeURIComponent(`site:loopnet.com "${[p.address,p.city].filter(Boolean).join(' ')}"`)}`;
+  const compsQ=`site:loopnet.com ${ptype||'commercial'} for sale ${p.city||'Los Angeles'} CA`;
+  const comps=`https://www.google.com/search?q=${encodeURIComponent(compsQ)}`;
+  return `<a href="${exact}" target="_blank" rel="noopener noreferrer" title="Find this exact property on LoopNet (CoStar) — free search, opens in a new tab">LoopNet ↗</a> <a href="${comps}" target="_blank" rel="noopener noreferrer" class="small" title="Browse comparable ${p.type||'commercial'} for-sale on LoopNet in ${p.city||'LA'}" style="opacity:.7">(comps)</a>`;
+}
+// Rich filter state
+const F = { types:new Set(), firms:new Set(), cityQuery:'', priceMin:null, priceMax:null,
+            capMin:0, unitsMin:null, unitsMax:null, yearMin:null, yearMax:null, status:'all', afford:false, unwarrantable:false, nonqm:false, shortlist:false, hasPhone:false, hasEmail:false };
+
+// ── SEARCH ALL DATA (Steve list-build rule 7) ──────────────────────────────────
+// Free-text search across EVERY field on a listing — not just the visible/defined columns,
+// nested objects included (finance/qwen/deal…) — via JSON.stringify. Space = AND.
+// A per-row haystack is memoized on `p.__idxHay` and invalidated when the row identity changes.
+let idxQuery='';
+function idxHay(p){
+  if(p.__idxHay!=null) return p.__idxHay;
+  let s;
+  try{ s=JSON.stringify(p, (k,v)=> k==='__idxHay'?undefined:v); }
+  catch(e){ s=Object.keys(p).map(k=>{ const v=p[k]; return v==null?'':(typeof v==='object'?'':String(v)); }).join(' '); }
+  // fold in the CRM-saved contact (agent name/phone/email) which lives outside the row object
+  const c=window.CONTACTS&&window.CONTACTS[p.id]; if(c) s+=' '+JSON.stringify(c);
+  s=s.toLowerCase();
+  try{ Object.defineProperty(p,'__idxHay',{value:s,enumerable:false,configurable:true,writable:true}); }catch(_){ p.__idxHay=s; }
+  return s;
+}
+function idxSearchPass(p){
+  if(!idxQuery) return true;
+  const hay=idxHay(p);
+  return idxQuery.split(/\s+/).filter(Boolean).every(t=>hay.includes(t));
+}
+
+function recompute(){
+  const A = SCEN[scenario];
+  DATA.ranked.forEach(p=>{
+    try{ delete p.__idxHay; }catch(_){}   // finance/deal recomputed → refresh the search haystack
+    p.finance = CREFin.finance(p, A);
+    const fsc = CREFin.financeScore(p, p.finance);
+    p.financeScore = fsc.score; p.financeConf = fsc.conf;
+    p.composite = CREFin.composite(p, p.finance, fsc.score, p.qwenScore);
+    p.deal = dealScore(p);   // {score, parts, confidence, coverage} — cached for badge + sort + tooltip
+  });
+  DATA.ranked.sort((a,b)=>b.composite-a.composite);
+  DATA.ranked.forEach((p,i)=>p.rank=i+1);
+}
+
+function firmLabel(p){
+  if(p.firm) return p.firm;
+  const d=(p.source||'').replace(/^https?:\/\/(www\.)?/,'').split('/')[0]||'';
+  const map={'crexi.com':'Crexi','loopnet.com':'LoopNet','compass.com':'Compass','remax.com':'RE/MAX',
+    'coldwellbankerhomes.com':'Coldwell Banker','marcusmillichap.com':'Marcus & Millichap',
+    'lee-associates.com':'Lee & Associates','colliers.com':'Colliers','cbre.com':'CBRE','jll.com':'JLL'};
+  if(map[d]) return map[d];
+  return d ? d.split('.')[0].replace(/^./,c=>c.toUpperCase()) : 'Unknown';
+}
+
+// ── Agent-contact CRM (front-end) ──────────────────────────────────────────────────
+// CONTACTS[listingId] = {name,phone,email,firm,contacted_at,letters:[]}. Loaded once at
+// boot from /api/contacts; every save re-writes the record here so the grid re-renders
+// without a reload. Sending a letter is NOT done here — letters are SAVED only.
+window.CONTACTS = window.CONTACTS || {};
+fetch('/api/contacts').then(r=>r.json()).then(j=>{ window.CONTACTS=j.contacts||{}; if(window.DATA_READY) render(); }).catch(()=>{});
+
+// ── Real phone reveal (TK-10687) ── each listing carries a broker/agent NAME but no phone; the real
+// numbers live in the usre DRE registry. resolveContacts() batches the visible page's agents to the
+// /api/broker-contacts proxy, which returns a REAL city-scoped contact (see serve.js). National
+// toll-free numbers arrive flagged phone_national and are SUPPRESSED below (Steve: "not a national
+// number — closest to the listing"); the firm website link stands in so the local branch is reachable.
+window.BROKER_RESOLVED = window.BROKER_RESOLVED || {};
+const _TOLLFREE=new Set(['800','833','844','855','866','877','888','822','880','887','889']);
+// CRCP is LA-County-only, so the "closest to the listing" phone (Steve's rule) must be a CALIFORNIA
+// number. Every CA area code — a firm phone outside this set (e.g. a Compass 212 NY HQ line) is a
+// national/HQ leak and is SUPPRESSED (the firm website stands in) exactly like a toll-free number.
+const _CA_AREA=new Set(['209','213','279','310','323','341','350','408','415','424','442','510','530','559','562','619','626','628','650','657','661','669','707','714','747','760','805','818','820','831','858','909','916','925','935','949','951']);
+const _area=ph=>{ const d=String(ph||'').replace(/\D/g,'').replace(/^1(?=\d{10}$)/,''); return d.slice(0,3); };
+const isNat=ph=>{ const a=_area(ph); return _TOLLFREE.has(a) || (a.length===3 && !_CA_AREA.has(a)); };
+const _resolveDone=new Set();
+async function resolveContacts(rows){
+  const items=[];
+  for(const p of (rows||[])){ if(p&&p.id!=null&&!_resolveDone.has(p.id)){ _resolveDone.add(p.id);
+    const a=agentInfo(p); if(a.name) items.push({id:p.id,name:a.name,firm:a.firm||'',city:p.city||''}); } }
+  if(!items.length) return;
+  try{ const j=await postJSON('/api/broker-contacts',{items});
+    if(j&&j.contacts&&Object.keys(j.contacts).length){ Object.assign(window.BROKER_RESOLVED,j.contacts); paintPage(); } }catch(_){}
+}
+
+// ── Firm Instagram (global, link-free) ── each brokerage firm's last 3 IG posts,
+// rendered INLINE under the agent-contact card (no outside links — Business
+// Discovery, thumbnails cached same-origin). Loads once at boot, re-renders on ready.
+window.FIRM_IG = window.FIRM_IG || {}; window.FIRM_IG_IDX = window.FIRM_IG_IDX || {};
+Promise.all([
+  fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
+  fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}}))
+]).then(([cache,map])=>{ window.FIRM_IG=cache||{}; const idx={};
+  for(const [h,al] of Object.entries((map.handles)||{})) for(const a of al) idx[a]=h; window.FIRM_IG_IDX=idx;
+  if(window.DATA_READY) render(); }).catch(()=>{});
+window.firmHandleOf=function(firm){ const n=String(firm||'').toLowerCase().replace(/[^a-z0-9]/g,''); if(!n) return null;
+  if(window.FIRM_IG_IDX[n]) return window.FIRM_IG_IDX[n];
+  for(const [a,h] of Object.entries(window.FIRM_IG_IDX)){ if(n===a||(a.length>=5&&n.includes(a))||(a.length>=3&&n.startsWith(a))) return h; } return null; };
+window.firmIgInline=function(firm){ const h=window.firmHandleOf(firm); if(!h) return '';
+  const c=window.FIRM_IG[h]; if(!c||!c.posts||!c.posts.length) return '';
+  const e=s=>String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]));
+  const items=c.posts.slice(0,3).map(p=>{ const cap=e((p.caption||'').replace(/\s+/g,' ').slice(0,90))||'(no caption)';
+    const when=p.timestamp?String(p.timestamp).slice(0,10):''; const vid=p.media_type==='VIDEO'?'▶ ':'';
+    const thumb=p.image?'<img class="figt" loading="lazy" src="'+e(p.image)+'" alt="">':'<span class="figt"></span>';
+    return '<div class="figli figli-static">'+thumb+'<span class="figb"><span class="figc">'+vid+cap+'</span><span class="figm">'+(when?when+' · ':'')+'♥ '+(p.likes||0)+' · 💬 '+(p.comments||0)+'</span></span></div>'; }).join('');
+  return '<div class="figwrap"><div class="fighd">📷 @'+e(h)+' · firm’s latest 3</div>'+items+'</div>'; };
+// Best-known contact info for a listing: the saved CRM record wins, else what the scrape gave us.
+function agentInfo(p){
+  const c=window.CONTACTS[p.id]||{};
+  const rv=window.BROKER_RESOLVED[p.id]||{};                       // real contact resolved from usre (TK-10687)
+  const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
+  const dName=p.broker_name||p.broker_agent||(Array.isArray(p.broker_agents)&&p.broker_agents[0])||(bh&&bh.current_broker&&(bh.current_broker.name||bh.current_broker.firm))||'';
+  // Phone precedence: hand-saved CRM > scraped row > resolved usre line — but a NATIONAL/toll-free
+  // number is never shown as the phone (Steve: "not a national number"); the firm website stands in.
+  const rvPhone=(rv.phone && !rv.phone_national && !isNat(rv.phone))?rv.phone:'';   // isNat now also drops non-CA area codes (out-of-region HQ lines)
+  return { name:c.name||dName||'', phone:c.phone||p.broker_phone||rvPhone||'', email:c.email||p.broker_email||rv.email||'',
+           firm:c.firm||(rv.firm)||firmLabel(p),
+           phoneTier:rv.phone_tier||null, phoneCity:rv.phone_city||null, phoneNational:!!rv.phone_national,
+           firmWebsite:rv.firm_website||'', firmPhone:(rv.firm_phone && !isNat(rv.firm_phone))?rv.firm_phone:'', firmAddress:rv.firm_address||'',
+           contacted_at:c.contacted_at||null, notes:c.notes||'', letters:(c.letters||[]) };
+}
+// Broker (listing broker of record) phone — the enriched current-broker line, distinct from the
+// agent/contact phone in agentInfo (which folds in the scraped row's broker_phone). Steve 2026-08-25:
+// "add broker and agent phone numbers to the fields."
+function brokerPhoneOf(p){
+  const a=agentInfo(p);
+  const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
+  const chist=(bh&&bh.current_broker&&bh.current_broker.phone)||'';
+  // Prefer a DISTINCT firm/broker-of-record line (resolved usre firm_phone or broker-history), then the
+  // raw row, then — if the agent's own resolved line IS a firm/branch line — use that (it's the broker
+  // office number). Keeps "Broker phone" the firm-of-record line, distinct from the agent-direct phone.
+  return a.firmPhone || chist || p.broker_phone || (a.phoneTier==='firm' ? a.phone : '') || '';
+}
+function gsearch(q){ return 'https://www.google.com/search?q='+encodeURIComponent(q); } // retained; no longer linked out
+function safeUrl(u){ u=String(u==null?'':u); return /^https?:\/\//i.test(u)?u:'#'; }
+// "↗ View property" — a link OUT to the real listing on whichever service it's on (Crexi / LoopNet /
+// the broker's own site). Uses p.source, the actual listing URL. Steve 2026-08-18: "need to see the property."
+function sourceLink(p){ const u=p.source; if(!u||!/^https?:\/\//i.test(u)) return '';
+  const host=u.replace(/^https?:\/\/(www\.)?/,'').split('/')[0];
+  const map={'crexi.com':'Crexi','loopnet.com':'LoopNet','compass.com':'Compass','remax.com':'RE/MAX','lyonstahl.com':'Lyon Stahl','coldwellbankerhomes.com':'Coldwell Banker'};
+  const name=map[host]||host.replace(/\.(com|net|org)$/,'').replace(/^./,c=>c.toUpperCase());
+  return `<a class="srclink" href="${safeUrl(u)}" target="_blank" rel="noopener noreferrer" title="See this property on ${esc(name)}">↗ View on ${esc(name)}</a>`;
+}
+// Aggregator doctrine: never surface a clickable href to crexi/redfin/zillow/costar/loopnet/realtor/myelisting.
+// Mirror of mls.html brokerListing() — resolve canonical click-through from listing data.
+const _AGG=['crexi','redfin','zillow','costar','loopnet','realtor','myelisting'];
+const _host=u=>{try{return new URL(u).hostname.replace(/^www\./,'');}catch{return null;}};
+const _isAgg=u=>{const h=_host(u);return !!(h&&_AGG.some(a=>h.includes(a)));};
+// Refined listing-link doctrine (2026-07-31):
+// 1. Non-agg source → firm-direct listing link.
+// 2. Agg/absent source + non-agg broker_url → honest "firm site" label (NOT "the listing").
+// 3. Always append a Google ground-truth finder (never crexi/aggregator).
+// Returns array of HTML strings to push() into the find[] block.
+function listingLinkBits(p, firm){
+  const bits=[];
+  const listingQ='"'+(p.address||'')+'" '+(p.city||'')+' '+(firm&&firm!=='Unknown'?firm:'')+' commercial listing';
+  if(p.firm_direct_url&&!_isAgg(p.firm_direct_url)){   // harvested broker's-own-page deep link — highest confidence
+    bits.push(`<a class="findlink" href="${safeUrl(p.firm_direct_url)}" target="_blank" rel="noopener noreferrer">↗ View firm listing</a>`);
+  } else if(p.source&&!_isAgg(p.source)){
+    bits.push(`<a class="findlink" href="${safeUrl(p.source)}" target="_blank" rel="noopener noreferrer">↗ View firm listing</a>`);
+  } else {
+    if(p.broker_url&&!_isAgg(p.broker_url)){
+      bits.push(`<a class="findlink" href="${safeUrl(p.broker_url)}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm&&firm!=='Unknown'?firm:'Broker')} site</a>`);
+    }
+    bits.push(`<a class="findlink" href="https://www.google.com/search?q=${encodeURIComponent(listingQ)}" target="_blank" rel="noopener noreferrer">🔎 Find this listing</a>`);
+  }
+  return bits;
+}
+// A listing with NO contact of any kind (no agent name, phone, email, or firm site) is an off-market /
+// POCKET listing — the broker deliberately isn't published. Show that honestly instead of "no agent on
+// file" (which reads like missing data). Steve 2026-08-19: "show the word pocket listing if appropriate."
+function isPocket(a){ return !a.name && !a.phone && !a.email && !a.firmWebsite && !a.firm; }
+const pocketBadge = `<span class="pocketbadge" title="Off-market / pocket listing — the broker isn't publicly listed for this property">🔒 Pocket listing</span>`;
+// Firm cell — clicking filters the grid to that firm IN-APP (no external link).
+function firmCell(p){ const n=firmLabel(p); if(!n||n==='Unknown') return esc(n||'—');
+  return `<span class="flink" data-firmfilter="${esc(n).replace(/"/g,'&quot;')}" title="Show only ${esc(n)} listings">${esc(n)}</span>`; }
+// Agent name — plain in-app text (no external search).
+function agentCell(name){ return name?esc(name):'—'; }
+// Inline phone/email. The phone is a REAL tel: link (Steve 2026-08-18 "make phone icon actually call
+// out") so clicking 📞 or the number places the call; email is a mailto:. Copy buttons stay for both.
+const telHref=ph=>'tel:'+String(ph||'').replace(/[^0-9+]/g,'');
+function contactBits(a){
+  const tier=a.phone&&a.phoneTier==='firm'?`<span class="tierb" title="Firm branch line${a.phoneCity?' — '+esc(a.phoneCity)+' office':''}">firm</span>`:'';
+  const tel=a.phone?`<span class="ain"><a href="${telHref(a.phone)}" title="Call ${esc(a.phone)}">📞 ${esc(a.phone)}</a> ${tier} <button class="copyb" data-copyval="${esc(a.phone)}" title="Copy phone">⧉</button></span>`:'';
+  const eml=a.email?`<span class="ain"><a href="mailto:${esc(a.email)}" title="Email ${esc(a.email)}">✉ ${esc(a.email)}</a> <button class="copyb" data-copyval="${esc(a.email)}" title="Copy email">⧉</button></span>`:'';
+  // Firm website — "open listing firm website" (and the way to reach the LOCAL branch when we only
+  // have a national/missing number). Opens in a new tab; noopener for safety.
+  const w=a.firmWebsite?String(a.firmWebsite).replace(/^https?:\/\//,'').replace(/\/$/,''):'';
+  const web=w?`<span class="ain">🔗 <a href="${esc(a.firmWebsite.match(/^https?:\/\//)?a.firmWebsite:'https://'+a.firmWebsite)}" target="_blank" rel="noopener noreferrer" title="Open the listing firm’s website — find the local office">${esc(w)}</a></span>`:'';
+  const inner=[tel,eml,web].filter(Boolean).join(' ');
+  return inner?`<div class="ainrow">${inner}</div>`:'';
+}
+// The Agent · contact column cell: name + copyable phone/email + Open/Mark buttons + chips.
+function agentContactCell(p){
+  const a=agentInfo(p);
+  const nm=a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a>`:(isPocket(a)?pocketBadge:`<span class="amut">no agent on file</span>`);
+  const contacted=a.contacted_at?`<span class="achip done" title="Marked contacted ${esc(a.contacted_at)}">✓ ${fmtWhen(a.contacted_at)}</span>`:'';
+  const lc=a.letters.length?`<span class="achip" title="${a.letters.length} saved letter(s)">✎ ${a.letters.length}</span>`:'';
+  const nt=a.notes?`<span class="achip" title="Has notes">🗒 notes</span>`:'';
+  // Steve 2026-08-18 "phone on front page. no OPEN": the contact info (phone/email/firm website) shows
+  // DIRECTLY in the cell — no "Open" button gating it. Only a compact Contacted mark remains (click the
+  // ROW to open the full agent window if needed; the row click handler already does that).
+  const btns=`<span class="acts">`
+    +`<button class="abtn markbtn" data-mark="${p.id}" title="${a.contacted_at?'Update the contacted date':'Mark contacted (saves the date)'}">${a.contacted_at?'↻':'✓'} Contacted</button>`
+    +`</span>`;
+  return `<div class="agentcell">${nm}${contactBits(a)}<div class="chiprow">${contacted}${lc}${nt}</div>${btns}</div>`;
+}
+function fmtWhen(iso){ try{ return new Date(iso).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}); }catch(_){ return String(iso||'').slice(0,10); } }
+
+// Deal-Score badge + plain-text breakdown tooltip (explains WHY the score is what it is).
+function dealBadge(p){
+  const d = p.deal; if(!d || d.score==null) return `<span class="b" title="Not enough signal to score this deal (no cap rate, rent estimate, or DSCR available).">⬡ Deal —</span>`;
+  const col = DealScore.scoreColor(d.score);
+  const lines = d.parts.map(pt=>`• ${pt.label}: ${Math.round(pt.score)}/100 (${pt.conf}, weight ${Math.round(pt.weight)})`).join('\n');
+  const tip = `Deal Score ${d.score}/100 — weighted blend of the signals below, re-normalized over what's present (missing signals drop out, never NaN).\nConfidence: ${d.confidence}${d.coverage!=null?` (core coverage ${Math.round(d.coverage*100)}%)`:''}.\n\n${lines}\n\nA screen, not an appraisal. Verify NOI + units in DD.`;
+  return `<span class="b deal-badge" style="color:${col};border-color:${col}" title="${tip.replace(/"/g,'&quot;')}">⬡ Deal ${d.score}${d.confidence==='low'||d.confidence==='thin'?' ?':''}</span>`;
+}
+function card(p){
+  const f=p.finance, uc=/Under Contract|Pending/i.test(p.status);
+  // firm badge is now a DRILL atom → filters the grid to this firm (reuses data-firmfilter handler)
+  const _fn=firmLabel(p);
+  const firmB = (_fn && _fn!=='Unknown')
+    ? `<span class="b drill" data-firmfilter="${esc(_fn).replace(/"/g,'&quot;')}" data-filter="firm" data-v="${esc(_fn).replace(/"/g,'&quot;')}" title="Filter to ${esc(_fn)} listings">🏢 ${esc(_fn)}</span>`
+    : `<span class="b" title="Listing source / brokerage firm">🏢 ${esc(_fn||'—')}</span>`;
+  const aff = f.affordable ? `<span class="b ok">✓ $${(f.cashNeeded/1000).toFixed(0)}k in budget</span>`
+                           : `<span class="b bad">✗ needs $${(f.cashNeeded/1000).toFixed(0)}k</span>`;
+  const rec = p.qwen && p.qwen.recommendation ? `<span class="b rec-${p.qwen.recommendation}">${p.qwen.recommendation}</span>` : '';
+  const statusB = uc ? `<span class="b warn">${p.status}</span>` : (/OUT OF BUDGET/i.test(p.status)?`<span class="b bad">over budget</span>`:`<span class="b ok">Active</span>`);
+  const capB = p.cap_rate!=null
+     ? `<span class="b">${p.cap_rate}% cap${p.cap_rate_projected?` →${p.cap_rate_projected}%*`:''}</span>`
+     : `<span class="b warn">cap n/d</span>`;
+  const verB = p.verified===true ? `<span class="b ok" title="In-place NOI confirmed against MLS">✓ cap verified</span>`
+     : (p.cap_rate!=null ? `<span class="b" title="Broker-stated cap, not independently verified">broker cap</span>`
+     : `<span class="b warn" title="No in-place income disclosed">⚠ no income data</span>`);
+  // Broker-history enrichment — keyed by listing id then by address fallback
+  const bh = window.BROKER_HIST[p.id] || window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
+  const brokerFlagBadge = bh && bh.broker_flag
+    ? `<span class="brokerflag" title="${esc(bh.flag_reason||'')}">⚑ Prior broker on file</span>`
+    : '';
+  function txHistHTML(bh){
+    if(!bh || !bh.tx_history || !bh.tx_history.length) return '';
+    const rows = bh.tx_history.map(t=>{
+      const price = t.price ? `<span class="tx-price">$${Number(t.price).toLocaleString()}</span>` : '';
+      const src = t.tier==='A' ? ' <span style="color:var(--acc);font-size:9px">[A]</span>' : ' <span style="color:var(--mut);font-size:9px">[B]</span>';
+      return `<div class="tx-row"><span class="tx-date">${esc(String(t.date||'').slice(0,10))}</span><span class="tx-ev">${esc(t.event||'')}${src}</span>${price}</div>`;
+    }).join('');
+    // Also show the last deed recording from assessor if we have AIN
+    const ain = bh.ain ? `<div style="font-size:10px;color:var(--mut);margin-top:2px">AIN ${bh.ain} · built ${bh.assessor_year_built||'—'} · ${bh.assessor_units||'?'} units (assessor)</div>` : '';
+    return `<div class="txhist">${rows}${ain}</div>`;
+  }
+  function brokerLineHTML(bh){
+    if(!bh || !bh.current_broker) return '';
+    const b = bh.current_broker;
+    if(!b.name && !b.firm) return '';
+    const nameStr = b.name ? `<strong>${esc(b.name)}</strong>` : '';
+    const firmStr = b.firm ? esc(b.firm) : '';
+    const dreStr  = b.dre ? (function(){const L=String(b.dre).trim();return /^\d{8}$/.test(L)?` · DRE# <a href="https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=${encodeURIComponent(L)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record ↗">${esc(L)} ↗</a>`:` · DRE ${esc(L)}`;})() : '';
+    const phoneStr = b.phone ? ` · <a href="tel:${esc(String(b.phone).replace(/[^0-9+]/g,''))}">${esc(b.phone)}</a>` : '';
+    const mlsStr  = b.mls  ? ` · MLS ${esc(b.mls)}` : '';
+    const parts = [nameStr, firmStr].filter(Boolean).join(' · ');
+    const tierBadge = b.tier==='A' ? ' <span style="color:var(--acc);font-size:9px">[A]</span>' : '';
+    return `<div class="brokerline">🧑‍💼 ${parts}${dreStr}${phoneStr}${mlsStr}${tierBadge}</div>`;
+  }
+  return `<div class="card ${uc?'uc':''}" id="card-${p.id}">
+    <div class="top">
+      <div><div class="rank">#${p.rank} · score</div><div class="addr">${esc(p.address||'')}</div><div class="city">${p.city?`<span class="drill" data-cityfilter="${esc(p.city).replace(/"/g,'&quot;')}" data-filter="city" data-v="${esc(p.city).replace(/"/g,'&quot;')}" title="Filter to ${esc(p.city)}">${esc(p.city)}</span>`:''}, CA ${esc(p.zip||'')}</div></div>
+      <div style="display:flex;align-items:flex-start;gap:8px">
+        <button class="copybtn" data-copy="${p.id}" aria-label="Copy listing summary" title="Copy summary to clipboard">📋</button>
+        <button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist this listing">★</button>
+        ${vis('score')?`<div class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' · ai '+p.qwenScore:''}</small></div>`:''}
+      </div>
+    </div>
+    <div class="badges">${vis('deal')?dealBadge(p):''}${vis('firm')?firmB:''}${vis('type')?(p.type?`<span class="b type drill" data-typefilter="${esc(p.type).replace(/"/g,'&quot;')}" data-filter="type" data-v="${esc(p.type).replace(/"/g,'&quot;')}" title="Filter to ${esc(p.type)}">${esc(p.type)}</span>`:`<span class="b type">—</span>`):''}${vis('units')?`<span class="b">${p.units} unit${p.units>1?'s':''}</span>`:''}${vis('price')?`<span class="b">${fmt(p.price)}</span>`:''}${vis('ppu')&&ppu(p)&&!p.isCondo?`<span class="b" title="Price per unit">$${ppu(p).toLocaleString()}/unit</span>`:''}${vis('pricePerSf')&&ppsf(p)?`<span class="b" title="Price per building SF">$${ppsf(p).toLocaleString()}/sf</span>`:''}${vis('cap')?capB:''}${vis('verified')?verB:''}${vis('yield')&&f.grossYield?`<span class="b ok" title="Annual gross rent ÷ price">${f.grossYield}% rent yld</span>`:''}${vis('grm')&&grm(p)!=null?`<span class="b" title="Gross rent multiplier — price ÷ annual gross rent (lower = cheaper vs rent)">GRM ${grm(p)}</span>`:''}${vis('yearBuilt')&&p.year_built?`<span class="b" title="Year built">🏗 ${p.year_built}</span>`:''}${vis('dom')&&daysOnMarket(p)!=null?`<span class="b" title="Days since listed">${daysOnMarket(p)}d on mkt</span>`:''}${vis('beds')&&p.beds!=null?`<span class="b">${p.beds} bd</span>`:''}${vis('baths')&&p.baths!=null?`<span class="b">${p.baths} ba</span>`:''}${vis('sqft')&&p.sqft?`<span class="b">${(+p.sqft).toLocaleString()} sf</span>`:''}${vis('hoa')&&p.hoa?`<span class="b" title="HOA dues">$${(+p.hoa).toLocaleString()}/mo</span>`:''}${vis('warr')&&p.warrantable_status?`<span class="b ${p.warrantable_status==='fha_approved'?'ok':'warn'}" title="Condo warrantability (FHA-approval proxy)">${p.warrantable_status==='fha_approved'?'✓ warrantable':'⚑ '+p.warrantable_status}</span>`:''}${vis('broker')&&p.broker_name?`<span class="b" title="Listing broker">🧑‍💼 ${esc(p.broker_name)}</span>`:''}${vis('status')?statusB:''} ${vis('afford')?aff:''} ${vis('rec')?rec:''}</div>
+    ${vis('brokerflag')?brokerFlagBadge:''}
+    ${vis('statusline')?statusLine(p):''}
+    ${vis('txhist')?txHistHTML(bh):''}
+    ${vis('brokerline')?brokerLineHTML(bh):''}
+    ${(vis('coc')||vis('dscr')||vis('noi')||vis('debt')||vis('cashNeeded')||vis('cashFlow'))?`<div class="metrics">
+      ${vis('coc')?`<div><span class="k">Cash-on-cash</span><span class="v ${cls(f.coc,6,3)}">${pct(f.coc)}</span></div>`:''}
+      ${vis('dscr')?`<div><span class="k">DSCR</span><span class="v ${cls(f.dscr,1.25,1.0)}">${f.dscr??'—'}</span></div>`:''}
+      ${vis('noi')?`<div><span class="k">Est. NOI</span><span class="v">${fmt(f.noi)}</span></div>`:''}
+      ${vis('debt')?`<div><span class="k">${scenario==='cash'?'Debt svc':'Debt svc/yr'}</span><span class="v">${fmt(f.annualDebtService)}</span></div>`:''}
+      ${vis('cashNeeded')?`<div><span class="k">Cash needed</span><span class="v ${f.affordable?'pos':'neg'}">${fmt(f.cashNeeded)}</span></div>`:''}
+      ${vis('cashFlow')?`<div><span class="k">Cash flow/yr</span><span class="v ${cls(f.cashFlow,1,0)}">${fmt(f.cashFlow)}</span></div>`:''}
+    </div>`:''}
+    ${vis('thesis')&&p.qwen?.thesis?`<div class="thesis">${esc(p.qwen.thesis)}</div>`:''}
+    ${vis('risks')&&p.qwen?.risks?.length?`<div><span class="small">Risks</span><ul class="lst">${p.qwen.risks.map(r=>`<li>${esc(r)}</li>`).join('')}</ul></div>`:''}
+    ${vis('upside')&&p.upside_note?`<div class="small">💡 ${esc(p.upside_note)}</div>`:''}
+    ${vis('demo')?demoLine(p):''}
+    ${vis('rent')?rentLine(p):''}
+    ${anyAssessorVisible()?`<div class="assessline small" data-addr="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}">📋 assessor…</div>`:''}
+    ${cardAgentBlock(p)}
+  </div>`;
+}
+// Per-listing agent-contact block for the CARD view — every card (any asset type) gets the same
+// contact modal + mark-contacted as the list rows. Compact: name/phone/email + chips + buttons.
+function cardAgentBlock(p){
+  const a=agentInfo(p), id=p.id;
+  const nm=a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a>`:(isPocket(a)?pocketBadge:`<span class="amut">no agent on file</span>`);
+  const chips=`${a.contacted_at?`<span class="achip done" title="Contacted ${esc(a.contacted_at)}">✓ ${fmtWhen(a.contacted_at)}</span>`:''}${a.letters.length?`<span class="achip" title="${a.letters.length} saved letter(s)">✎ ${a.letters.length}</span>`:''}${a.notes?`<span class="achip" title="Has notes">🗒 notes</span>`:''}`;
+  return `<div class="cardagent" id="cardagent-${id}">`
+    +`<div class="ca-top">🧑‍💼 ${nm} ${chips}</div>`
+    +contactBits(a)
+    +window.firmIgInline(a.firm)
+    +`<div class="acts"><button class="abtn contactbtn" data-contact="${id}" title="Open the inline agent window — notes, letters, draft">✎ Open</button>`
+    +`<button class="abtn markbtn" data-mark="${id}" title="${a.contacted_at?'Update contacted date':'Mark contacted (saves the date)'}">${a.contacted_at?'↻':'✓'} Contacted</button></div>`
+    +`</div>`;
+}
+
+// ---- Non-QM target set: unwarrantable condos + small multifamily (1–9u) + mixed-use ----
+function isNonQM(p){
+  const t=(p.type||'').toLowerCase();
+  if(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved') return true;
+  if(/multi/.test(t) && p.units>=1 && p.units<=9) return true;
+  if(/mixed/.test(t)) return true;
+  return false;
+}
+// ---- lazy assessor enrichment (LA County parcel roll via /api/property-history, free) ----
+function anyAssessorVisible(){ return FIELDS.some(f=>f.assessor && vis(f.k)); }
+window.ASSESS = window.ASSESS || {};
+let assessObs = null;
+function assessLine(d){
+  const a=d&&(d.assessor||d.assessorArchive); const bits=[];
+  if(vis('assessed')){ const v=a&&(a.roll_total_value||a.total_value); if(v) bits.push('💰 assessed $'+(+v).toLocaleString()); }
+  if(vis('landimp')&&a){ const l=a.roll_land_value, im=a.roll_imp_value; if(l||im) bits.push('land $'+(+(l||0)).toLocaleString()+' / imp $'+(+(im||0)).toLocaleString()); }
+  if(vis('usecode')&&a){ const u=a.use_desc2||a.use_desc1||a.use_description; if(u) bits.push('🏷 '+u); }
+  if(vis('priorsale')&&d&&d.sales&&d.sales.length){ const s=d.sales[0]; bits.push('last sold $'+(+(s.sold_price||0)).toLocaleString()+' · '+String(s.sold_date||'').slice(0,10)); }
+  return bits.length ? ('📋 '+bits.join(' · ')) : '<span class="na">📋 no assessor record on file</span>';
+}
+function enrichAssessor(){
+  if(assessObs){ assessObs.disconnect(); assessObs=null; }
+  if(!anyAssessorVisible()) return;
+  const els=[...document.querySelectorAll('.assessline')];
+  const queue=[]; let active=0; const MAX=6;
+  function pump(){ while(active<MAX && queue.length){ fetchOne(queue.shift()); } }
+  function fetchOne(el){
+    const addr=el.dataset.addr, city=el.dataset.city, key=(addr+'|'+city).toLowerCase();
+    if(window.ASSESS[key]!==undefined){ el.innerHTML=assessLine(window.ASSESS[key]); pump(); return; }
+    active++;
+    fetch('/api/property-history?address='+encodeURIComponent(addr)+'&city='+encodeURIComponent(city||''))
+      .then(r=>r.json()).then(d=>{ window.ASSESS[key]=d; el.innerHTML=assessLine(d); })
+      .catch(()=>{ el.innerHTML='<span class="na">📋 assessor lookup failed</span>'; })
+      .finally(()=>{ active--; pump(); });
+  }
+  // As cards approach the viewport, queue them for enrichment (concurrency-capped).
+  assessObs=new IntersectionObserver((ents,obs)=>{ ents.forEach(en=>{ if(en.isIntersecting){ obs.unobserve(en.target); queue.push(en.target); pump(); } }); },{rootMargin:'400px'});
+  els.forEach(el=>assessObs.observe(el));
+  // Eager first batch so the top of the grid always resolves (no dependence on a scroll event).
+  els.slice(0,30).forEach(el=>{ assessObs.unobserve(el); queue.push(el); }); pump();
+}
+
+// ---- the rich filter pass ----
+function applyFilters(list){
+  return list.filter(p=>{
+    if(!idxSearchPass(p)) return false;
+    if(F.types.size && !F.types.has(p.type)) return false;
+    if(F.firms.size && !F.firms.has(firmLabel(p))) return false;
+    if(F.cityQuery){ const hay=((p.city||'')+' '+(p.address||'')).toLowerCase(); if(!hay.includes(F.cityQuery)) return false; }
+    if(F.priceMin!=null && p.price<F.priceMin) return false;
+    if(F.priceMax!=null && p.price>F.priceMax) return false;
+    if(F.capMin>0 && !(p.cap_rate!=null && p.cap_rate>=F.capMin)) return false;
+    if(F.unitsMin!=null && !(p.units>=F.unitsMin)) return false;
+    if(F.unitsMax!=null && !(p.units!=null && p.units<=F.unitsMax)) return false;
+    if(F.yearMin!=null && !(p.year_built && p.year_built>=F.yearMin)) return false;
+    if(F.yearMax!=null && !(p.year_built && p.year_built<=F.yearMax)) return false;
+    if(F.status==='active' && /Under Contract|Pending|OUT OF BUDGET/i.test(p.status)) return false;
+    if(F.afford && !p.finance.affordable) return false;
+    if(F.unwarrantable && !(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved')) return false;
+    if(F.nonqm && !isNonQM(p)) return false;
+    if(F.hasPhone||F.hasEmail){ const ci=agentInfo(p); if(F.hasPhone && !ci.phone) return false; if(F.hasEmail && !ci.email) return false; }
+    if(F.shortlist && !SHORT.has(p.id)) return false;
+    return true;
+  });
+}
+// Match everything EXCEPT one facet dimension — powers live facet counts (Amazon-style narrowing).
+function passExcept(p, skip){
+  if(!idxSearchPass(p)) return false;
+  if(skip!=='type' && F.types.size && !F.types.has(p.type)) return false;
+  if(skip!=='firm' && F.firms.size && !F.firms.has(firmLabel(p))) return false;
+  if(skip!=='city' && F.cityQuery){ const hay=((p.city||'')+' '+(p.address||'')).toLowerCase(); if(!hay.includes(F.cityQuery)) return false; }
+  if(F.priceMin!=null && p.price<F.priceMin) return false;
+  if(F.priceMax!=null && p.price>F.priceMax) return false;
+  if(F.capMin>0 && !(p.cap_rate!=null && p.cap_rate>=F.capMin)) return false;
+  if(F.unitsMin!=null && !(p.units>=F.unitsMin)) return false;
+  if(F.unitsMax!=null && !(p.units!=null && p.units<=F.unitsMax)) return false;
+  if(F.yearMin!=null && !(p.year_built && p.year_built>=F.yearMin)) return false;
+  if(F.yearMax!=null && !(p.year_built && p.year_built<=F.yearMax)) return false;
+  if(F.status==='active' && /Under Contract|Pending|OUT OF BUDGET/i.test(p.status)) return false;
+  if(F.afford && !p.finance.affordable) return false;
+  if(F.unwarrantable && !(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved')) return false;
+  if(F.nonqm && !isNonQM(p)) return false;
+  if(F.hasPhone||F.hasEmail){ const ci=agentInfo(p); if(F.hasPhone && !ci.phone) return false; if(F.hasEmail && !ci.email) return false; }
+  if(F.shortlist && !SHORT.has(p.id)) return false;
+  return true;
+}
+function updateFacetCounts(){
+  const tc={}, fc={}, cc={};
+  DATA.ranked.forEach(p=>{
+    if(passExcept(p,'type')) tc[p.type]=(tc[p.type]||0)+1;
+    if(passExcept(p,'firm')){ const fl=firmLabel(p); fc[fl]=(fc[fl]||0)+1; }
+    if(p.city && passExcept(p,'city')) cc[p.city]=(cc[p.city]||0)+1;
+  });
+  const paint=(sel,attr,map)=>$$(sel).forEach(c=>{ const key=c.dataset[attr]; if(key==null) return; const ct=c.querySelector('.ct'); const n=map[key]||0; if(ct) ct.textContent=n; c.classList.toggle('zero', n===0 && !c.classList.contains('active')); });
+  paint('#fType .chip','t',tc); paint('#fFirm .chip','fm',fc); paint('#fCityChips .chip','cy',cc);
+}
+function activeFilterCount(){
+  let n=0;
+  if(F.types.size) n++; if(F.firms.size) n++; if(F.cityQuery) n++;
+  if(F.priceMin!=null||F.priceMax!=null) n++; if(F.capMin>0) n++;
+  if(F.unitsMin!=null) n++; if(F.unitsMax!=null) n++; if(F.yearMin!=null) n++; if(F.yearMax!=null) n++;
+  if(F.status!=='all') n++; if(F.afford) n++; if(F.unwarrantable) n++; if(F.nonqm) n++; if(F.shortlist) n++; if(F.hasPhone) n++; if(F.hasEmail) n++;
+  return n;
+}
+
+const SORTS={ deal:(a,b)=>dealNum(b)-dealNum(a), composite:(a,b)=>b.composite-a.composite, coc:(a,b)=>num(b.finance.coc)-num(a.finance.coc),
+  yield:(a,b)=>num(b.finance.grossYield)-num(a.finance.grossYield),
+  cap:(a,b)=>num(b.cap_rate)-num(a.cap_rate), dscr:(a,b)=>num(b.finance.dscr)-num(a.finance.dscr),
+  priceAsc:(a,b)=>a.price-b.price, priceDesc:(a,b)=>b.price-a.price,
+  cashNeeded:(a,b)=>a.finance.cashNeeded-b.finance.cashNeeded, unitsDesc:(a,b)=>num(b.units)-num(a.units),
+  rentRoi:(a,b)=>rentRoiScore(b)-rentRoiScore(a) };
+const num=v=>v==null?-1e9:v;
+const dealNum=p=>(p.deal && p.deal.score!=null) ? p.deal.score : -1e9;  // unscored sink to the bottom
+
+// ---- active-filter summary bar (removable chips + reset) ----
+function renderActiveBar(){
+  const bar=$('#activebar'); if(!bar) return; const chips=[];
+  F.types.forEach(t=>chips.push(['type:'+t, t]));
+  F.firms.forEach(f=>chips.push(['firm:'+f, '🏢 '+f]));
+  if(F.cityQuery) chips.push(['city', 'city ~ “'+F.cityQuery+'”']);
+  if(F.priceMin!=null) chips.push(['priceMin', '≥ $'+(+F.priceMin).toLocaleString()]);
+  if(F.priceMax!=null) chips.push(['priceMax', '≤ $'+(+F.priceMax).toLocaleString()]);
+  if(F.capMin>0) chips.push(['capMin', 'cap ≥ '+F.capMin+'%']);
+  if(F.unitsMin!=null) chips.push(['unitsMin', '≥ '+F.unitsMin+' units']);
+  if(F.unitsMax!=null) chips.push(['unitsMax', '≤ '+F.unitsMax+' units']);
+  if(F.yearMin!=null) chips.push(['yearMin', 'built ≥ '+F.yearMin]);
+  if(F.yearMax!=null) chips.push(['yearMax', 'built ≤ '+F.yearMax]);
+  if(F.status!=='all') chips.push(['status', 'active only']);
+  if(F.afford) chips.push(['afford', 'in budget']);
+  if(F.unwarrantable) chips.push(['unwarrantable', '⚑ unwarrantable']);
+  if(F.nonqm) chips.push(['nonqm', '◆ non-QM']);
+  if(F.hasPhone) chips.push(['hasPhone', '☎ has phone']);
+  if(F.hasEmail) chips.push(['hasEmail', '✉ has email']);
+  if(F.shortlist) chips.push(['shortlist', '⭐ shortlist']);
+  bar.innerHTML = chips.length
+    ? '<span class="lbl">Filtering</span>'+chips.map(([k,l])=>`<span class="afchip" data-clear="${k.replace(/"/g,'&quot;')}">${esc(l)} <span class="xx">✕</span></span>`).join('')+'<button class="afreset" id="afreset">Reset all ✕</button>'
+    : '';
+}
+function clearFilter(spec){
+  if(spec.startsWith('type:')){ F.types.delete(spec.slice(5)); if(!F.types.has('Condo')) F.unwarrantable=false; }
+  else if(spec.startsWith('firm:')){ F.firms.delete(spec.slice(5)); }
+  else if(spec==='city'){ F.cityQuery=''; if($('#fCity')) $('#fCity').value=''; }
+  else if(spec==='priceMin'){ F.priceMin=null; if($('#fPriceMin')) $('#fPriceMin').value=''; }
+  else if(spec==='priceMax'){ F.priceMax=null; if($('#fPriceMax')) $('#fPriceMax').value=''; }
+  else if(spec==='capMin'){ F.capMin=0; if($('#fCap')){ $('#fCap').value=0; $('#capVal').textContent='any'; } }
+  else if(spec==='unitsMin'){ F.unitsMin=null; if($('#fUnits')) $('#fUnits').value=''; }
+  else if(spec==='unitsMax'){ F.unitsMax=null; if($('#fUnitsMax')) $('#fUnitsMax').value=''; }
+  else if(spec==='yearMin'){ F.yearMin=null; if($('#fYear')) $('#fYear').value=''; }
+  else if(spec==='yearMax'){ F.yearMax=null; if($('#fYearMax')) $('#fYearMax').value=''; }
+  else if(spec==='status'){ F.status='all'; }
+  else if(spec==='afford'){ F.afford=false; if($('#fAfford')) $('#fAfford').checked=false; }
+  else if(spec==='unwarrantable'){ F.unwarrantable=false; }
+  else if(spec==='nonqm'){ F.nonqm=false; }
+  else if(spec==='hasPhone'){ F.hasPhone=false; if($('#fHasPhone')) $('#fHasPhone').checked=false; }
+  else if(spec==='hasEmail'){ F.hasEmail=false; if($('#fHasEmail')) $('#fHasEmail').checked=false; }
+  else if(spec==='shortlist'){ F.shortlist=false; }
+  syncChips(); render();
+}
+// ---- shareable URL state (every filter/sort/view/scenario ↔ query string) ----
+function stateToURL(){
+  const p=new URLSearchParams();
+  if(F.types.size) p.set('t',[...F.types].join(','));
+  if(F.firms.size) p.set('fm',[...F.firms].join('~'));
+  if(F.cityQuery) p.set('city',F.cityQuery);
+  if(F.priceMin!=null) p.set('pmin',F.priceMin);
+  if(F.priceMax!=null) p.set('pmax',F.priceMax);
+  if(F.capMin>0) p.set('cap',F.capMin);
+  if(F.unitsMin!=null) p.set('u',F.unitsMin);
+  if(F.unitsMax!=null) p.set('ux',F.unitsMax);
+  if(F.yearMin!=null) p.set('yr',F.yearMin);
+  if(F.yearMax!=null) p.set('yrx',F.yearMax);
+  if(F.status!=='all') p.set('st',F.status);
+  if(F.afford) p.set('aff','1');
+  if(F.unwarrantable) p.set('unw','1');
+  if(F.nonqm) p.set('nq','1');
+  if(F.hasPhone) p.set('hp','1');
+  if(F.hasEmail) p.set('he','1');
+  if(scenario!=='25') p.set('sc',scenario);
+  const so=$('#sort')&&$('#sort').value; if(so && so!=='deal') p.set('sort',so);
+  if(colSort) p.set('cs',colSort.key+':'+(colSort.dir>0?'a':'d'));
+  if(heat!=='none') p.set('ht',heat);
+  if(view!=='grid') p.set('v',view);
+  const qs=p.toString();
+  // NOTE: the app declares a local `history` (chat log) that shadows window.history — must qualify.
+  try{ window.history.replaceState(null,'',location.pathname+(qs?('?'+qs):'')); }catch(e){}
+}
+// Snapshot the ORIGINAL query at first script-eval — an early render() can call stateToURL()
+// and rewrite location.search (it drops ?v= whenever the current view isn't grid) BEFORE
+// urlToState() runs, clobbering a shared ?v=/?t= link. Read shared state from this frozen copy.
+const INITIAL_SEARCH = location.search;
+function urlToState(){
+  const p=new URLSearchParams(INITIAL_SEARCH); let any=false;
+  if(p.get('t')){ F.types=new Set(p.get('t').split(',').filter(Boolean)); any=true; }
+  if(p.get('fm')){ F.firms=new Set(p.get('fm').split('~').filter(Boolean)); any=true; }
+  if(p.get('city')){ F.cityQuery=p.get('city'); if($('#fCity'))$('#fCity').value=F.cityQuery; any=true; }
+  if(p.get('pmin')){ F.priceMin=+p.get('pmin'); if($('#fPriceMin'))$('#fPriceMin').value=F.priceMin; any=true; }
+  if(p.get('pmax')){ F.priceMax=+p.get('pmax'); if($('#fPriceMax'))$('#fPriceMax').value=F.priceMax; any=true; }
+  if(p.get('cap')){ F.capMin=+p.get('cap'); if($('#fCap')){$('#fCap').value=F.capMin; $('#capVal').textContent=F.capMin+'%+';} any=true; }
+  if(p.get('u')){ F.unitsMin=+p.get('u'); if($('#fUnits'))$('#fUnits').value=F.unitsMin; any=true; }
+  if(p.get('ux')){ F.unitsMax=+p.get('ux'); if($('#fUnitsMax'))$('#fUnitsMax').value=F.unitsMax; any=true; }
+  if(p.get('yr')){ F.yearMin=+p.get('yr'); if($('#fYear'))$('#fYear').value=F.yearMin; any=true; }
+  if(p.get('yrx')){ F.yearMax=+p.get('yrx'); if($('#fYearMax'))$('#fYearMax').value=F.yearMax; any=true; }
+  if(p.get('st')){ F.status=p.get('st'); any=true; }
+  if(p.get('aff')==='1'){ F.afford=true; if($('#fAfford'))$('#fAfford').checked=true; any=true; }
+  if(p.get('unw')==='1'){ F.unwarrantable=true; any=true; }
+  if(p.get('nq')==='1'){ F.nonqm=true; any=true; }
+  if(p.get('hp')==='1'){ F.hasPhone=true; if($('#fHasPhone'))$('#fHasPhone').checked=true; any=true; }
+  if(p.get('he')==='1'){ F.hasEmail=true; if($('#fHasEmail'))$('#fHasEmail').checked=true; any=true; }
+  if(p.get('sc')){ scenario=p.get('sc'); }
+  if(p.get('sort')&&$('#sort')){ $('#sort').value=p.get('sort'); if($('#sortTop'))$('#sortTop').value=p.get('sort'); }
+  if(p.get('v')){ view=p.get('v'); }
+  if(p.get('cs')){ const [k,d]=p.get('cs').split(':'); colSort={key:k,dir:d==='a'?1:-1}; }
+  if(p.get('ht')){ heat=p.get('ht'); }
+  return any;
+}
+function csvEsc(v){ v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"'; }
+function exportCSV(){
+  const cols=listCols();
+  const rows=sortRows(applyFilters(DATA.ranked.slice()));
+  const head=['Address','City','ZIP'].concat(cols.map(c=>c.label));
+  const lines=[head.map(csvEsc).join(',')];
+  rows.forEach(p=>{ const row=[p.address,p.city,p.zip].concat(cols.map(c=>fieldVal(p,c.k))); lines.push(row.map(csvEsc).join(',')); });
+  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([lines.join('\n')],{type:'text/csv'})); a.download='cre-explorer-'+rows.length+'.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000);
+}
+function copyShareLink(){ const t=$('#linkbtn'); try{ navigator.clipboard.writeText(location.href); if(t){ const o=t.textContent; t.textContent='✓ Copied'; setTimeout(()=>t.textContent=o,1400); } }catch(e){ if(t) t.textContent='⌘C to copy'; } }
+// ---- side-by-side compare of shortlisted listings ----
+const CMP=[
+  {l:'Type', f:p=>esc(p.type||'—')},
+  {l:'Price', f:p=>fmt(p.price)},
+  {l:'$/unit', f:p=>{const v=ppu(p);return v?'$'+v.toLocaleString():'—';}},
+  {l:'$/SF', f:p=>{const v=ppsf(p);return v?'$'+v.toLocaleString():'—';}},
+  {l:'Units', f:p=>p.units??'—'},
+  {l:'Cap rate', f:p=>p.cap_rate!=null?p.cap_rate+'%':'—'},
+  {l:'DSCR', f:p=>p.finance&&p.finance.dscr!=null?p.finance.dscr:'—'},
+  {l:'Cash-on-cash', f:p=>p.finance&&p.finance.coc!=null?p.finance.coc+'%':'—'},
+  {l:'Cash needed', f:p=>p.finance?fmt(p.finance.cashNeeded):'—'},
+  {l:'Cash flow/yr', f:p=>p.finance?fmt(p.finance.cashFlow):'—'},
+  {l:'Est. rent', f:p=>{const e=rentEstimate(p);return e?'$'+e.std.toLocaleString()+'/mo':'—';}},
+  {l:'Beds / Baths', f:p=>(p.beds!=null||p.baths!=null)?((p.beds??'—')+' / '+(p.baths??'—')):'—'},
+  {l:'SqFt', f:p=>p.sqft?(+p.sqft).toLocaleString():'—'},
+  {l:'Year built', f:p=>p.year_built||'—'},
+  {l:'Warrantability', f:p=>esc(p.warrantable_status||'—')},
+  {l:'Deal score', f:p=>p.deal&&p.deal.score!=null?p.deal.score:'—'},
+  {l:'Invest score', f:p=>p.composite??'—'},
+];
+function openCompare(){
+  const ov=$('#cmpov'), body=$('#cmpbody');
+  const items=DATA.ranked.filter(p=>SHORT.has(p.id));
+  if(items.length<2){ body.innerHTML='<div class="cmphint">Star at least 2 listings (the ★ on any card) to compare them side by side.</div>'; ov.classList.add('on'); return; }
+  const head='<tr><th class="cmpk" scope="col"></th>'+items.map(p=>`<th scope="col">${esc(p.address)}<div class="cmpsub">${esc(p.city)}</div></th>`).join('')+'</tr>';
+  const rows=CMP.map(m=>`<tr><td class="cmpk">${m.l}</td>`+items.map(p=>`<td>${m.f(p)}</td>`).join('')+'</tr>').join('');
+  body.innerHTML=`<div class="cmptools no-print"><button id="cmpcsv">⬇ CSV</button><button id="cmpprint">🖨 Print</button></div><table class="cmptbl"><thead>${head}</thead><tbody>${rows}</tbody></table>`;
+  ov.classList.add('on');
+}
+function decodeEnt(s){ return String(s).replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&#39;/g,"'"); }
+function exportCompareCSV(){
+  const items=DATA.ranked.filter(p=>SHORT.has(p.id)); if(items.length<2) return;
+  const cols=['Address','City'].concat(CMP.map(m=>m.l));
+  const lines=[cols.map(csvEsc).join(',')];
+  items.forEach(p=>{ const row=[p.address,p.city].concat(CMP.map(m=>decodeEnt(m.f(p)))); lines.push(row.map(csvEsc).join(',')); });
+  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([lines.join('\n')],{type:'text/csv'})); a.download='cre-compare-'+items.length+'.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000);
+}
+function printCompare(){ document.body.classList.add('printing-compare'); window.print(); }
+// ---- paginated DOM (perf): render in pages, "Show more" appends ----
+let _rows=[], _shown=0;
+const PAGE=()=> view==='grid'?60:200;
+function moreBtn(){ return _shown<_rows.length ? `<div class="showmore-wrap"><button class="showmore" id="showmore">Show ${Math.min(PAGE(),_rows.length-_shown)} more · showing ${_shown} of ${_rows.length}</button></div>` : ''; }
+// Infinite scroll: auto-trigger the "show more" grow when it scrolls near view (reuses the
+// existing #showmore click handler → _shown+=PAGE → paintPage). Viewport-rooted so it works
+// whether the page or an inner pane scrolls. Re-pointed at the fresh button after each repaint.
+let _moreIO=null, _moreBusy=false;
+function wireAutoMore(){
+  const btn=document.getElementById('showmore'); if(!btn) return;
+  if(typeof IntersectionObserver==='undefined') return;
+  if(!_moreIO) _moreIO=new IntersectionObserver(es=>{ if(_moreBusy) return; if(es.some(e=>e.isIntersecting)){ const b=document.getElementById('showmore'); if(b){ _moreBusy=true; b.click(); setTimeout(()=>{_moreBusy=false;},60); } } },{rootMargin:'500px 0px'});
+  _moreIO.disconnect(); _moreIO.observe(btn);
+}
+function paintPage(){
+  const g=$('#grid'); const r=_rows; const slice=r.slice(0,_shown);
+  if(!r.length){ g.className='grid'; g.innerHTML=`<div class="empty">No properties match these filters.<br><span class="small">Loosen a filter or hit “Reset all”.</span></div>`; return; }
+  if(view==='table'){                                    // full columnar spreadsheet (sortable + drag-reorder + resizable cols)
+    g.className='listhost'; g.innerHTML=renderTableGrouped(slice)+moreBtn();
+    const csig=listCols().map(c=>c.k).join(',');
+    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
+  }
+  else if(view==='kv'){                                  // key→value block per record (deep inspection)
+    g.className='kvhost'; g.innerHTML=renderKVGrouped(slice)+moreBtn();
+  }
+  else if(view==='list'){                                // compact one-row-per-record: identity + inline summary chips
+    g.className='compacthost'; g.innerHTML=renderCompactGrouped(slice)+moreBtn();
+  }
+  else { g.className='grid'; g.innerHTML=groupWrap(slice,card,'grid')+moreBtn(); enrichAssessor(); }
+  applyHeat(slice);
+  resolveContacts(slice);   // fetch REAL phones for the visible agents, then repaint (TK-10687)
+  wireAutoMore();           // convert the show-more button into infinite scroll
+}
+// ===== nav-agent inline view renderers: List (compact) / KV / Table + group-by-any-field =====
+// The rail sort already ordered rows; sortRows() also front-sorts by the group key when groupBy is set,
+// so groups arrive contiguous here — we just detect a key change and emit a header band.
+function groupKeyLabel(p){ if(!groupBy) return ''; let v=fieldVal(p,groupBy); if(v==null||v==='') return '(none)'; return String(v); }
+// Generic grouping wrapper for non-table views: emits a heading band before each group.
+function groupWrap(rows, renderOne, kind){
+  if(!groupBy){ return rows.map(renderOne).join(''); }
+  let out='', last=null;
+  rows.forEach(p=>{ const gk=groupKeyLabel(p); if(gk!==last){ last=gk;
+    out+=`<div class="grouphead grouphead-${kind}">${esc(fieldLabel(groupBy))}: <b>${esc(gk)}</b></div>`; }
+    out+=renderOne(p); });
+  return out;
+}
+function fieldLabel(k){ if(k==='addr') return 'Property'; const f=FIELDS.find(x=>x.k===k); return f?f.label:k; }
+// ---- compact List: address identity + inline summary chips from the visible columns ----
+function compactChips(p){
+  const cols=listCols().slice(0,7);   // a handful of the visible fields as inline chips (keeps the row light)
+  return cols.map(c=>`<span class="cchip"><span class="cck">${esc(c.label)}</span> ${colCell(c.k,p)}</span>`).join('');
+}
+function renderCompactRow(p){
+  return `<div class="crow" data-id="${p.id}">`
+    +`<button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button>`
+    +`<div class="cid"><span class="cla">${esc(p.address)}</span><span class="clc">${esc(p.city)}, CA ${p.zip||''}</span></div>`
+    +`<div class="cfields">${compactChips(p)}</div></div>`;
+}
+function renderCompactGrouped(rows){ return `<div class="compactlist">${groupWrap(rows,renderCompactRow,'compact')}</div>`; }
+// ---- KV: each record a labeled key→value block, every visible field vertical ----
+function renderKVCard(p){
+  const cols=listCols();
+  const rowsHtml=[`<div class="kvrow"><span class="kvk">Property</span><span class="kvv">${esc(p.address)}, ${esc(p.city)} CA ${p.zip||''}</span></div>`]
+    .concat(cols.map(c=>`<div class="kvrow"><span class="kvk">${esc(c.label)}</span><span class="kvv">${colCell(c.k,p)}</span></div>`)).join('');
+  return `<div class="kvcard" data-id="${p.id}">`
+    +`<div class="kvhead"><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="Shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button><span class="kvaddr">${esc(p.address)}</span></div>`
+    +rowsHtml+`</div>`;
+}
+function renderKVGrouped(rows){ return `<div class="kvgrid">${groupWrap(rows,renderKVCard,'kv')}</div>`; }
+// ---- Table: reuse the full renderList() spreadsheet, injecting group header rows when grouped ----
+function renderTableGrouped(rows){
+  if(!groupBy) return renderList(rows);
+  // Build the grouped body DIRECTLY (no regex-splice of renderList's output) so col-resize sees the
+  // same thead/table shell and we never rebuild a body just to discard it.
+  const ncols=listCols().length+1;
+  let bodyRows='', last=null;
+  rows.forEach(p=>{ const gk=groupKeyLabel(p);
+    if(gk!==last){ last=gk; bodyRows+=`<tr class="grouprow"><td class="groupcell" colspan="${ncols}">${esc(fieldLabel(groupBy))}: <b>${esc(gk)}</b></td></tr>`; }
+    bodyRows+=renderListBodyRow(p); });
+  return renderListShell(bodyRows);
+}
+// The table shell (listwrap + thead), body injected. Shared by renderList() and the grouped table so
+// the <thead>/sortable/drag headers are identical in both paths (col-resize signature stays stable).
+function renderListShell(bodyHtml){
+  const cols=listCols();
+  const arrow=k=>colSort&&colSort.key===k?(colSort.dir>0?' asc':' desc'):'';
+  const head='<th scope="col" class="stick sortable'+arrow('addr')+'" data-sortk="addr" title="Sort by address">Property</th>'+cols.map(c=>`<th scope="col" class="sortable dragcol${arrow(c.k)}" draggable="true" data-sortk="${c.k}" data-k="${c.k}" title="Drag to reorder · click to sort by ${esc(c.label)}">${esc(c.label)}</th>`).join('');
+  return `<div class="listwrap"><table class="listtbl" data-no-sort><thead><tr>${head}</tr></thead><tbody>${bodyHtml}</tbody></table></div>`;
+}
+// One <tr> body row, factored out of renderList so the grouped table can interleave group headers.
+function renderListBodyRow(p){
+  const cols=listCols();
+  return `<tr class="lrow" data-id="${p.id}">`
+    +`<td class="stick"><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button> <span class="la">${esc(p.address)}</span><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div>${sourceLink(p)?`<div class="lsrc">${sourceLink(p)}</div>`:''}</td>`
+    +cols.map(c=>`<td>${colCell(c.k,p)}</td>`).join('')+`</tr>`;
+}
+// ---- saved searches (name + restore the full shareable-URL state) ----
+const SAVED=(function(){ try{ return JSON.parse(localStorage.getItem('cre_saved')||'[]'); }catch(e){ return []; } })();
+function saveSaved(){ try{ localStorage.setItem('cre_saved', JSON.stringify(SAVED)); }catch(e){} }
+function buildSavedList(){ const el=$('#savedList'); if(!el) return;
+  el.innerHTML = SAVED.length
+    ? SAVED.map((s,i)=>`<div class="savedrow"><button class="savedname" data-qs="${(s.qs||'').replace(/"/g,'&quot;')}" title="Restore this saved view">📁 ${esc(s.name)}</button><button class="saveddel" data-del="${i}" aria-label="Delete saved search">✕</button></div>`).join('')
+    : '<div class="savedempty">None yet — filter, then “Save current view”.</div>';
+}
+// ---- per-listing clipboard summary (honest fields only) ----
+function cardSummary(p){ const f=p.finance||{}; const L=[];
+  L.push(`${p.address}, ${p.city} CA ${p.zip||''}`.trim());
+  const l2=[p.type, p.units?`${p.units} unit${p.units>1?'s':''}`:null, p.price?fmt(p.price):null].filter(Boolean).join(' · '); if(l2) L.push(l2);
+  const m=[]; if(p.cap_rate!=null) m.push('Cap '+p.cap_rate+'%'); if(ppu(p)&&!p.isCondo) m.push('$'+ppu(p).toLocaleString()+'/unit'); if(ppsf(p)) m.push('$'+ppsf(p).toLocaleString()+'/SF'); if(m.length) L.push(m.join(' · '));
+  const fin=[]; if(f.dscr!=null) fin.push('DSCR '+f.dscr); if(f.coc!=null) fin.push('CoC '+f.coc+'%'); if(f.cashNeeded!=null) fin.push('Cash needed '+fmt(f.cashNeeded)); if(f.cashFlow!=null) fin.push('Cash flow/yr '+fmt(f.cashFlow)); if(fin.length) L.push(fin.join(' · '));
+  const r=rentEstimate(p); const ex=[]; if(r) ex.push('Est. rent $'+r.std.toLocaleString()+'/mo'); if(p.deal&&p.deal.score!=null) ex.push('Deal score '+p.deal.score+'/100'); if(p.warrantable_status) ex.push('Warrantability: '+p.warrantable_status); if(ex.length) L.push(ex.join(' · '));
+  if(p.source) L.push(p.source);
+  return L.join('\n');
+}
+function copySummary(id, btn){ const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
+  const done=()=>{ if(btn){ btn.textContent='✓'; setTimeout(()=>btn.textContent='📋',1200); } };
+  try{ navigator.clipboard.writeText(cardSummary(p)).then(done).catch(done); }catch(e){ done(); }
+}
+function applyQuick(k){
+  if(k==='condo'){ F.types=new Set(['Condo']); }
+  else if(k==='mf'){ F.types=new Set(['Multifamily']); }
+  else if(k==='nonqm'){ F.nonqm=true; }
+  else if(k==='unw'){ F.unwarrantable=true; F.types=new Set(['Condo']); }
+  else if(k==='afford'){ F.afford=true; if($('#fAfford')) $('#fAfford').checked=true; }
+  else if(k==='short'){ F.shortlist=true; }
+  syncChips(); render();
+}
+function median(a){ if(!a.length) return null; const s=a.slice().sort((x,y)=>x-y); const m=Math.floor(s.length/2); return s.length%2?s[m]:(s[m-1]+s[m])/2; }
+function renderStats(r){
+  const sb=$('#statsbar'); if(!sb) return;
+  if(!r||!r.length){ sb.innerHTML=''; return; }
+  const prices=r.map(p=>p.price).filter(v=>v>0);
+  const ppus=r.map(p=>ppu(p)).filter(v=>v>0);
+  const ppsfs=r.map(p=>ppsf(p)).filter(v=>v>0);
+  const caps=r.map(p=>p.cap_rate).filter(v=>v!=null);
+  const aff=r.filter(p=>p.finance&&p.finance.affordable).length;
+  const rents=r.map(p=>{ const e=rentEstimate(p); return e?e.std:null; }).filter(v=>v);
+  const pill=(l,v)=>v==null?'':`<span class="statpill"><span class="sl">${l}</span> <b>${v}</b></span>`;
+  sb.innerHTML =
+    pill('Median price', prices.length?'$'+Math.round(median(prices)).toLocaleString():null)
+    +pill('Median $/unit', ppus.length?'$'+Math.round(median(ppus)).toLocaleString():null)
+    +pill('Median $/SF', ppsfs.length?'$'+Math.round(median(ppsfs)).toLocaleString():null)
+    +pill('Avg cap', caps.length?(caps.reduce((a,b)=>a+(+b),0)/caps.length).toFixed(2)+'% ('+caps.length+' disc.)':null)
+    +pill('Median est. rent', rents.length?'$'+Math.round(median(rents)).toLocaleString()+'/mo':null)
+    +pill('In budget', aff+' of '+r.length);
+}
+function render(){
+  const g=$('#grid'); const fc=activeFilterCount();
+  $('#fcount').textContent=fc; $('#fcount').classList.toggle('on',fc>0);
+  { const sb=$('#shortbtn'); if(sb){ sb.textContent='⭐ Shortlist ('+SHORT.size+')'; sb.classList.toggle('shorton',F.shortlist); } }
+  renderActiveBar(); stateToURL(); updateFacetCounts();
+  // Wait-for-selection: on a clean load (no filter chosen) don't dump 2,134 cards — prompt for a pick.
+  // A free-text search counts as a selection, so typing in the search box shows results immediately.
+  if(fc===0 && !F.cityQuery && !idxQuery){
+    g.className='grid';
+    g.innerHTML=`<div class="empty">👈 Expand a filter tab on the left — or start with one of these:
+      <div class="quickstart">
+        <button class="qchip" data-quick="condo">🏢 Condos</button>
+        <button class="qchip" data-quick="mf">🏠 Multifamily</button>
+        <button class="qchip" data-quick="nonqm">◆ Non-QM</button>
+        <button class="qchip" data-quick="unw">⚑ Unwarrantable</button>
+        <button class="qchip" data-quick="afford">✓ In budget</button>
+        ${SHORT.size?`<button class="qchip" data-quick="short">⭐ My shortlist (${SHORT.size})</button>`:''}
+      </div>
+      <span class="small">${(DATA.ranked.length).toLocaleString()} listings loaded · ${DATA.meta.market} — results appear instantly; your active filters show as removable chips up top.</span></div>`;
+    $('#count').innerHTML=`<b>${(DATA.ranked.length).toLocaleString()}</b> loaded · choose a filter to view`;
+    $('#statsbar').innerHTML='';
+    saveState(); return;
+  }
+  let r = applyFilters(DATA.ranked.slice());
+  sortRows(r);
+  renderStats(r);
+  _rows=r; _shown=Math.min(PAGE(), r.length);   // paginate the DOM; stats/count/facets still use the full set
+  paintPage();
+  $('#count').innerHTML = `<b>${r.length}</b> of ${DATA.ranked.length} properties${fc?` · ${fc} filter${fc>1?'s':''} active`:''}`;
+  $('#foot').textContent = `${scenario==='cash'?'all-cash':SCEN[scenario].downPct+'% down @ '+SCEN[scenario].ratePct+'%'} · * →X% = projected cap after value-add (not in-place). Cap rates broker-stated — verify NOI in DD.`;
+  saveState();
+}
+
+// ---- build the filter UI from the data ----
+function buildFacets(){
+  const tCount={}, fCount={}, cCount={};
+  DATA.ranked.forEach(p=>{ tCount[p.type]=(tCount[p.type]||0)+1; const fl=firmLabel(p); fCount[fl]=(fCount[fl]||0)+1; if(p.city) cCount[p.city]=(cCount[p.city]||0)+1; });
+  const chipHTML=(obj,key)=>Object.entries(obj).sort((a,b)=>b[1]-a[1])
+    .map(([k,c])=>`<span class="chip" data-${key}="${k.replace(/"/g,'&quot;')}">${k}<span class="ct">${c}</span></span>`).join('');
+  // Condos are merged into the grid as a real 'Condo' asset type (chip comes from the data below).
+  // Prepend one special in-grid filter: ⚑ Unwarrantable = Condo AND not FHA-approved (non-QM target).
+  const nonqmChip='<span class="chip" data-warr="nonqm" style="border-color:rgba(188,140,255,.55);color:var(--purp,#bc8cff)" title="Non-QM target set: unwarrantable condos + small multifamily (1–9 units) + mixed-use">◆ Non-QM</span>';
+  const unwChip='<span class="chip" data-warr="unwarrantable" style="border-color:rgba(248,81,73,.55);color:var(--bad)" title="Non-FHA-approved condos — filters the grid to non-QM targets">⚑ Unwarrantable</span>';
+  $('#fType').innerHTML=nonqmChip+unwChip+chipHTML(tCount,'t');
+  $('#fFirm').innerHTML=chipHTML(fCount,'fm');
+  // top 14 cities as quick chips; the search box covers the rest
+  const topCities=Object.entries(cCount).sort((a,b)=>b[1]-a[1]).slice(0,14);
+  $('#fCityChips').innerHTML=topCities.map(([k,c])=>`<span class="chip" data-cy="${k.replace(/"/g,'&quot;')}">${k}<span class="ct">${c}</span></span>`).join('');
+  // sort options mirrored into the top bar
+  $('#sortTop').innerHTML=$('#sort').innerHTML;
+}
+
+// ---- chat context ----
+function getContext(){
+  const lines=[];
+  lines.push(`MARKET: ${DATA.meta.market}. Budget: $400,000 down. Active scenario: ${scenario==='cash'?'ALL CASH':SCEN[scenario].downPct+'% down, '+SCEN[scenario].ratePct+'%/'+SCEN[scenario].amortYears+'yr'}.`);
+  lines.push(`Active filters: types=[${[...F.types]}], firms=[${[...F.firms]}], city~"${F.cityQuery}", price ${F.priceMin||'any'}-${F.priceMax||'any'}, capMin=${F.capMin}, unitsMin=${F.unitsMin||'any'}, unitsMax=${F.unitsMax||'any'}, builtAfter=${F.yearMin||'any'}, builtBefore=${F.yearMax||'any'}, status=${F.status}, inBudgetOnly=${F.afford}, sort=${$('#sort').value}.`);
+  lines.push('RANKED PROPERTIES (current scenario):');
+  applyFilters(DATA.ranked).slice(0,120).forEach(p=>{ const f=p.finance;
+    lines.push(`#${p.rank} id=${p.id} | ${p.address}, ${p.city} | ${p.type} | $${p.price.toLocaleString()} | ${p.units}u | cap ${p.cap_rate!=null?p.cap_rate+'%':'n/d'}${p.verified===true?' VERIFIED':''} | firm ${firmLabel(p)} | CoC ${f.coc!=null?f.coc+'%':'n/a'} | DSCR ${f.dscr??'n/a'} | cash needed $${f.cashNeeded.toLocaleString()} ${f.affordable?'(in budget)':'(OVER)'} | composite ${p.composite} | status ${p.status}`);
+  });
+  if(DATA.comps && DATA.comps.submarket_cap_benchmarks){
+    lines.push('\nSUBMARKET CAP BENCHMARKS:');
+    Object.entries(DATA.comps.submarket_cap_benchmarks).forEach(([k,v])=>lines.push(`- ${k}: ${v}`));
+  }
+  const lc = Object.entries(window.liveComps||{}).filter(([,v])=>v&&v.length);
+  if(lc.length){ lines.push('\nLIVE COMPS THIS SESSION:'); lc.forEach(([id,arr])=>{ const p=DATA.ranked.find(x=>x.id===id); lines.push(`for ${p?p.address:id}:`); arr.forEach(c=>lines.push(`  - ${c.title} | ${[c.price,c.cap,c.units,c.sf].filter(Boolean).join(' · ')}`)); }); }
+  return lines.join('\n');
+}
+
+// ---- chat-issued actions (extended for the rich filters) ----
+const TYPE_ALIASES={multifamily:'Multifamily',retail:'Retail',office:'Office',industrial:'Industrial','mixed':'Mixed-use','mixed-use':'Mixed-use',land:'Land',hospitality:'Hospitality'};
+function applyAction(a){
+  const done=[];
+  if(a.scenario && SCEN[String(a.scenario)]){ setScenario(String(a.scenario)); done.push('scenario→'+a.scenario); }
+  if(a.sort && SORTS[a.sort]){ $('#sort').value=a.sort; $('#sortTop').value=a.sort; done.push('sort→'+a.sort); }
+  const setType=v=>{ const key=TYPE_ALIASES[String(v).toLowerCase()]; if(key){ F.types=new Set([key]); done.push('type→'+key); } };
+  if(a.type) setType(a.type);
+  if(a.filter && a.filter!=='all' && a.filter!=='active' && a.filter!=='affordable') setType(a.filter);
+  if(a.filter==='affordable'){ F.afford=true; done.push('in-budget only'); }
+  if(a.filter==='active'){ F.status='active'; done.push('active only'); }
+  if(a.filter==='all'){ F.types.clear(); F.afford=false; F.status='all'; done.push('cleared'); }
+  if(a.firm){ F.firms=new Set([a.firm]); done.push('firm→'+a.firm); }
+  if(a.city){ F.cityQuery=String(a.city).toLowerCase(); $('#fCity').value=a.city; done.push('city→'+a.city); }
+  if(a.maxPrice!=null && !isNaN(+a.maxPrice)){ F.priceMax=+a.maxPrice; $('#fPriceMax').value=+a.maxPrice; done.push('max $'+(+a.maxPrice).toLocaleString()); }
+  if(a.minPrice!=null && !isNaN(+a.minPrice)){ F.priceMin=+a.minPrice; $('#fPriceMin').value=+a.minPrice; done.push('min $'+(+a.minPrice).toLocaleString()); }
+  if(a.capMin!=null && !isNaN(+a.capMin)){ F.capMin=+a.capMin; $('#fCap').value=+a.capMin; $('#capVal').textContent=F.capMin?F.capMin+'%+':'any'; done.push('cap ≥'+a.capMin+'%'); }
+  if(a.unitsMin!=null && !isNaN(+a.unitsMin)){ F.unitsMin=+a.unitsMin; $('#fUnits').value=+a.unitsMin; done.push('units ≥'+a.unitsMin); }
+  if(a.unitsMax!=null && !isNaN(+a.unitsMax)){ F.unitsMax=+a.unitsMax; $('#fUnitsMax').value=+a.unitsMax; done.push('units ≤'+a.unitsMax); }
+  if(a.yearMin!=null && !isNaN(+a.yearMin)){ F.yearMin=+a.yearMin; $('#fYear').value=+a.yearMin; done.push('built ≥'+a.yearMin); }
+  if(a.yearMax!=null && !isNaN(+a.yearMax)){ F.yearMax=+a.yearMax; $('#fYearMax').value=+a.yearMax; done.push('built ≤'+a.yearMax); }
+  syncChips(); render();
+  if(a.highlightId){ const el=$('#card-'+a.highlightId); if(el){ el.scrollIntoView({behavior:'smooth',block:'center'}); el.classList.add('flash'); setTimeout(()=>el.classList.remove('flash'),2200); done.push('→'+a.highlightId); } }
+  return done;
+}
+
+function setScenario(s){ scenario=s; localStorage.setItem('cre_scenario',s);
+  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===s)); recompute(); }
+
+function syncChips(){
+  $$('#fType .chip').forEach(c=>{ if(c.dataset.warr==='unwarrantable') c.classList.toggle('active',F.unwarrantable); else if(c.dataset.warr==='nonqm') c.classList.toggle('active',F.nonqm); else c.classList.toggle('active',F.types.has(c.dataset.t)); });
+  $$('#fFirm .chip').forEach(c=>c.classList.toggle('active',F.firms.has(c.dataset.fm)));
+  $$('#fCityChips .chip').forEach(c=>c.classList.toggle('active',F.cityQuery===c.dataset.cy.toLowerCase()));
+  $$('#fStatus .chip').forEach(c=>c.classList.toggle('active',F.status===c.dataset.st));
+}
+
+// ---- Top-of-page natural-language chat bar (client-side; drives F via applyAction) ----
+const CHAT_TYPES=[['multifamily','Multifamily'],['multi-family','Multifamily'],['multi family','Multifamily'],['apartment','Multifamily'],['office','Office'],['industrial','Industrial'],['retail','Retail'],['mixed-use','Mixed-use'],['mixed use','Mixed-use'],['net lease','Retail'],['net-lease','Retail'],['nnn','Retail'],['land','Land'],['hotel','Hospitality'],['hospitality','Hospitality']];
+function chatMoney(s){ s=String(s).toLowerCase().replace(/[,\s$]/g,''); let n=parseFloat(s); if(isNaN(n))return null; if(/m/.test(s))n*=1e6; else if(/k/.test(s))n*=1e3; return Math.round(n); }
+function chatCities(){ try{ return [...new Set((DATA||[]).map(r=>(r.city||'').trim()).filter(Boolean))].sort((a,b)=>b.length-a.length); }catch(e){ return []; } }
+function chatClear(){
+  F.types.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.unwarrantable=F.nonqm=false;
+  ['#fCity','#fPriceMin','#fPriceMax','#fUnits','#fUnitsMax','#fYear','#fYearMax'].forEach(x=>{const e=$(x); if(e)e.value='';});
+  const c=$('#fCap'); if(c){ c.value=0; const v=$('#capVal'); if(v)v.textContent='any'; }
+  syncChips(); render();
+}
+function applyChat(raw){
+  const msg=$('#chatmsg'); const s=' '+String(raw||'').toLowerCase().replace(/\s+/g,' ').trim()+' ';
+  if(!s.trim())return;
+  if(/^\s*(clear|reset|all|everything|start over|show all)\s*$/.test(s)){ chatClear(); if(msg)msg.textContent='Cleared'; return; }
+  // fresh search — clear chat-controlled fields + their drawer inputs first
+  F.types.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.unwarrantable=F.nonqm=false;
+  ['#fCity','#fPriceMin','#fPriceMax','#fUnits','#fUnitsMax','#fYear','#fYearMax'].forEach(x=>{const e=$(x); if(e)e.value='';});
+  const a={}; const got=[]; let m;
+  // warrantability / condo
+  if(/unwarrantable|non-?warrantable|not warrantable/.test(s)){ F.unwarrantable=true; got.push('unwarrantable condos'); }
+  if(/non-?qm\b|non qm/.test(s)){ F.nonqm=true; got.push('non-QM'); }
+  if(/\bcondos?\b/.test(s) && !F.unwarrantable){ F.types.add('Condo'); got.push('Condo'); }
+  // financing scenario (the seg now lives in the left drawer)
+  if(/\ball\s*cash\b/.test(s)){ a.scenario='cash'; got.push('all cash'); }
+  else if(/\b30\s*%?\s*down\b/.test(s)){ a.scenario='30'; got.push('30% down'); }
+  else if(/\b25\s*%?\s*down\b/.test(s)){ a.scenario='25'; got.push('25% down'); }
+  // units
+  const WORDU={duplex:2,triplex:3,fourplex:4,fiveplex:5};
+  for(const w in WORDU){ if(s.includes(' '+w)){ a.unitsMin=a.unitsMax=WORDU[w]; got.push(WORDU[w]+' units'); } }
+  if(m=s.match(/(\d+)\s*(?:-|to)\s*(\d+)\s*unit/)){ a.unitsMin=+m[1]; a.unitsMax=+m[2]; got.push(m[1]+'–'+m[2]+' units'); }
+  else if(m=s.match(/(?:under|below|less than|up to|max(?:imum)?|<)\s*(\d+)\s*unit/)){ a.unitsMax=+m[1]; got.push('≤'+m[1]+' units'); }
+  else if(m=s.match(/(?:over|above|more than|at least|min(?:imum)?|\+|>)\s*(\d+)\s*unit/)){ a.unitsMin=+m[1]; got.push(m[1]+'+ units'); }
+  else if(m=s.match(/(\d+)\s*unit/)){ a.unitsMin=a.unitsMax=+m[1]; got.push(m[1]+' units'); }
+  // type (set on F directly so Condo/multi don't get clobbered by applyAction's single-type set)
+  const types=[]; for(const [k,v] of CHAT_TYPES){ if(s.includes(' '+k) && !types.includes(v)) types.push(v); }
+  types.forEach(v=>F.types.add(v)); if(types.length) got.push(types.join('+'));
+  // city (known city names from the data; longest-first to avoid substring collisions)
+  for(const c of chatCities()){ const cl=c.toLowerCase(); if(s.includes(' '+cl+' ') || s.includes(' in '+cl)){ a.city=c; got.push(c); break; } }
+  // price
+  if((m=s.match(/\$?\s*([\d.,]+\s*[mk]?)\s*(?:-|to)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b/.test(m[0])){ a.minPrice=chatMoney(m[1]); a.maxPrice=chatMoney(m[2]); got.push('$'+m[1].trim()+'–$'+m[2].trim()); }
+  else if((m=s.match(/(?:under|below|less than|up to|<)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b|\d{4,}/.test(m[0])){ a.maxPrice=chatMoney(m[1]); got.push('< $'+m[1].trim()); }
+  else if((m=s.match(/(?:over|above|more than|at least|>)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b|\d{4,}/.test(m[0])){ a.minPrice=chatMoney(m[1]); got.push('> $'+m[1].trim()); }
+  // cap rate
+  if(m=s.match(/(\d+(?:\.\d+)?)\s*%?\s*cap/) || (m=s.match(/cap\s*(?:rate)?\s*(?:over|above|min|>|of)?\s*(\d+(?:\.\d+)?)/))){ a.capMin=+m[1]; got.push(m[1]+'%+ cap'); }
+  // year
+  if(m=s.match(/(?:built\s*after|newer than|after|since)\s*(\d{4})/)){ a.yearMin=+m[1]; got.push('after '+m[1]); }
+  if(m=s.match(/(?:built\s*before|older than|before)\s*(\d{4})/)){ a.yearMax=+m[1]; got.push('before '+m[1]); }
+  applyAction(a);   // maps a.* → F + drawer inputs, then syncChips()+render()
+  if(msg) msg.textContent = got.length ? ('Showing: '+got.join(' · ')) : ('Couldn’t parse — try “4 units in Los Angeles”');
+}
+(function(){ const q=$('#chatq'), go=$('#chatgo'), cl=$('#chatclear'), ex=$('#chatex');
+  if(q) q.addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); applyChat(q.value); } });
+  if(go) go.addEventListener('click',()=>applyChat(q?q.value:''));
+  if(cl) cl.addEventListener('click',()=>{ if(q)q.value=''; chatClear(); const m=$('#chatmsg'); if(m)m.textContent='Cleared'; });
+  if(ex) ex.addEventListener('click',e=>{ const c=e.target.closest('.chatchip'); if(!c)return; if(q)q.value=c.dataset.q; applyChat(c.dataset.q); });
+})();
+
+// ---- persistence ----
+function saveState(){ try{ localStorage.setItem('cre_filters', JSON.stringify({
+  types:[...F.types],firms:[...F.firms],cityQuery:F.cityQuery,priceMin:F.priceMin,priceMax:F.priceMax,
+  capMin:F.capMin,unitsMin:F.unitsMin,unitsMax:F.unitsMax,yearMin:F.yearMin,yearMax:F.yearMax,status:F.status,afford:F.afford,hasPhone:F.hasPhone,hasEmail:F.hasEmail,sort:$('#sort').value })); }catch(e){} }
+function loadState(){ try{ const s=JSON.parse(localStorage.getItem('cre_filters')||'{}');
+  if(s.types) F.types=new Set(s.types); if(s.firms) F.firms=new Set(s.firms);
+  F.cityQuery=s.cityQuery||''; F.priceMin=s.priceMin??null; F.priceMax=s.priceMax??null;
+  F.capMin=s.capMin||0; F.unitsMin=s.unitsMin??null; F.unitsMax=s.unitsMax??null; F.yearMin=s.yearMin??null; F.yearMax=s.yearMax??null; F.status=s.status||'all'; F.afford=!!s.afford; F.hasPhone=!!s.hasPhone; F.hasEmail=!!s.hasEmail;
+  if(s.cityQuery) $('#fCity').value=s.cityQuery; if(s.priceMin) $('#fPriceMin').value=s.priceMin; if(s.priceMax) $('#fPriceMax').value=s.priceMax;
+  if(s.capMin){ $('#fCap').value=s.capMin; $('#capVal').textContent=s.capMin+'%+'; } if(s.unitsMin) $('#fUnits').value=s.unitsMin; if(s.unitsMax) $('#fUnitsMax').value=s.unitsMax; if(s.yearMin) $('#fYear').value=s.yearMin; if(s.yearMax) $('#fYearMax').value=s.yearMax;
+  $('#fAfford').checked=F.afford; if($('#fHasPhone'))$('#fHasPhone').checked=F.hasPhone; if($('#fHasEmail'))$('#fHasEmail').checked=F.hasEmail; if(s.sort){ $('#sort').value=s.sort; }
+}catch(e){} }
+function resetAll(){ F.types.clear(); F.firms.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.status='all'; F.afford=false; F.unwarrantable=false; F.nonqm=false; F.hasPhone=false; F.hasEmail=false;
+  ['fCity','fPriceMin','fPriceMax','fUnits','fUnitsMax','fYear','fYearMax'].forEach(id=>$('#'+id).value=''); $('#fCap').value=0; $('#capVal').textContent='any'; $('#fAfford').checked=false; if($('#fHasPhone'))$('#fHasPhone').checked=false; if($('#fHasEmail'))$('#fHasEmail').checked=false;
+  syncChips(); render(); }
+
+// ---- chat ----
+function addMsg(role, text, actions){
+  const d=document.createElement('div'); d.className='msg '+role;
+  d.innerHTML = (text||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/```action[\s\S]*?```/g,'').trim()
+    .replace(/\*\*(.+?)\*\*/g,'<b>$1</b>').replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\n/g,'<br>');
+  if(actions&&actions.length) d.innerHTML += `<div class="actiontag">⚡ view updated: ${actions.join(', ')}</div>`;
+  $('#log').appendChild(d); $('#log').scrollTop=$('#log').scrollHeight; return d;
+}
+function parseAction(reply){ const m=reply.match(/```action\s*([\s\S]*?)```/); if(!m) return null; try{ return JSON.parse(m[1].trim()); }catch{ return null; } }
+async function ask(text){
+  addMsg('u', text); history.push({role:'user',content:text});
+  $('#send').disabled=true; const thinking=addMsg('sys','…thinking');
+  try{
+    const r = await fetch('/api/claude-chat',{ method:'POST', headers:{'Content-Type':'application/json'},
+      body: JSON.stringify({ messages: history, context: getContext() }) });
+    const j = await r.json(); thinking.remove();
+    if(j.error){ addMsg('sys','⚠ '+j.error); }
+    else { const act=parseAction(j.reply); const applied=act?applyAction(act):null; addMsg('a', j.reply, applied); history.push({role:'assistant',content:j.reply}); }
+  }catch(e){ thinking.remove(); addMsg('sys','⚠ '+e.message); }
+  $('#send').disabled=false; $('#q').focus();
+}
+
+// ---- property history + live comps ----
+window.liveComps = {};
+async function fetchHistory(btn){
+  const id=btn.dataset.id, out=$('#hist-'+id); if(!out) return;
+  if(out.dataset.loaded){ out.style.display = out.style.display==='none'?'block':'none'; return; }
+  out.innerHTML='<span class="small">…checking WhoLivedThere archive (free)…</span>';
+  try{
+    const q=new URLSearchParams({address:btn.dataset.address||'',city:btn.dataset.city||'',zip:btn.dataset.zip||''});
+    const j=await (await fetch('/api/history?'+q)).json();
+    out.dataset.loaded='1';
+    if(j.available===false){ out.innerHTML='<span class="b warn">⚠ archive offline</span>'; return; }
+    if(!j.matched){ out.innerHTML='<span class="small">📜 No record in the WhoLivedThere archive for this address yet.</span>'; return; }
+    const P=j.parcel, money=v=>v?'$'+Number(v).toLocaleString():'—';
+    let h='<div class="small" style="margin-top:6px;border-top:1px solid #2a2a2a;padding-top:6px">📜 <b>WhoLivedThere archive</b>';
+    if(P){ h+=`<div class="badges" style="margin:4px 0">${P.year_built?`<span class="b">built ${P.year_built}</span>`:''}${P.use_type?`<span class="b type">${P.use_type}</span>`:''}${P.units?`<span class="b">${P.units} unit${P.units>1?'s':''}</span>`:''}${P.total_value?`<span class="b ok" title="LA County assessor total assessed value">assessed ${money(P.total_value)}</span>`:''}${P.land_value?`<span class="b">land ${money(P.land_value)}</span>`:''}${P.improvement_value?`<span class="b">improv ${money(P.improvement_value)}</span>`:''}</div><div class="small">${P.use_description||''} · APN ${P.apn||'—'} · ${P.situs_address||''}</div>`; }
+    if(j.permits&&j.permits.length){ h+=`<div class="small" style="margin-top:4px">🔨 Permits (${j.permits.length}): `+j.permits.slice(0,5).map(pm=>`${pm.permit_type||'permit'}${pm.valuation?' $'+Number(pm.valuation).toLocaleString():''}${pm.issue_date?' ('+String(pm.issue_date).slice(0,10)+')':''}`).join(' · ')+'</div>'; }
+    if(j.filming&&j.filming.length){ h+=`<div class="small" style="margin-top:4px">🎬 Filmed here: `+j.filming.slice(0,5).map(f=>`${f.title||'production'}${f.year?' ('+f.year+')':''}`).join(' · ')+'</div>'; }
+    if(j.events&&j.events.length){ h+=`<div class="small" style="margin-top:4px">📌 ${j.events.slice(0,4).map(ev=>ev.summary||ev.kind).join(' · ')}</div>`; }
+    out.innerHTML=h+'</div>';
+  }catch(e){ out.innerHTML='<span class="b warn">⚠ '+e.message+'</span>'; }
+}
+async function fetchLiveComps(id){
+  const p = DATA.ranked.find(x=>x.id===id); if(!p) return;
+  const out = $('#lc-'+id); if(!out) return;
+  out.innerHTML = '<span class="small">…pulling fresh comps (~$0.03)…</span>';
+  try{
+    const r = await fetch('/api/comps',{method:'POST',headers:{'Content-Type':'application/json'},
+      body:JSON.stringify({address:p.address,city:p.city,type:p.type})});
+    const j = await r.json();
+    if(j.error && !(j.comps&&j.comps.length)){ out.innerHTML = `<span class="b warn">⚠ ${j.error}</span>`; return; }
+    window.liveComps[id] = j.comps||[];
+    const none = (!j.comps||!j.comps.length)?' — none scraped (source may have blocked)':'';
+    out.innerHTML = `<div class="small">Live comps via ${j.source||'web'} (cost ${j.cost})${none}:</div>` +
+      (j.comps||[]).map(c=>`<div class="lc-item"><span>${esc(c.title||c.url||'comp')}</span>${c.url?` <button class="copyb" data-copyval="${esc(c.url)}" title="Copy comp URL">⧉</button>`:''} — ${esc([c.price,c.cap,c.units,c.sf].filter(Boolean).join(' · '))}</div>`).join('');
+  }catch(e){ out.innerHTML = `<span class="b bad">⚠ ${e.message}</span>`; }
+}
+
+// ---- drawer open/close ----
+function openDrawer(){ $('#drawer').classList.add('open'); $('#scrim').classList.add('on'); }
+function closeDrawer(){ $('#drawer').classList.remove('open'); $('#scrim').classList.remove('on'); }
+
+// ── Inline list-row expansion + Agent CRM actions ──────────────────────────────────
+function postJSON(url,body){ return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body||{})}).then(r=>r.json()); }
+function toast(msg){ let t=document.getElementById('crmToast'); if(!t){ t=document.createElement('div'); t.id='crmToast'; t.className='crmToast'; document.body.appendChild(t); } t.textContent=msg; t.classList.add('on'); clearTimeout(t._t); t._t=setTimeout(()=>t.classList.remove('on'),1900); }
+// In-place refresh after any CRM save — repaints EVERY visible surface for this listing (list
+// agent cell + open list-row panel + card block) WITHOUT a full render() (which would collapse
+// expanded rows). Works for every asset type in either view.
+function refreshContactUI(id){
+  const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
+  try{ delete p.__idxHay; }catch(_){}   // CRM changed → let the search haystack rebuild
+
+  const row=[...document.querySelectorAll('.listtbl tr.lrow')].find(r=>r.dataset.id===id);
+  if(row){ const cols=listCols(); const ai=cols.findIndex(c=>c.k==='agent');
+    if(ai>=0 && row.children[ai+1]) row.children[ai+1].innerHTML=colCell('agent',p);
+    const exp=row.nextElementSibling;
+    if(exp&&exp.classList.contains('lexp')&&exp.classList.contains('open')){
+      const body=exp.querySelector('.lexpbody'); if(body){ body.innerHTML=expandBody(p); body.dataset.loaded='1'; }
+    }
+  }
+  const cb=document.getElementById('cardagent-'+id); if(cb) cb.outerHTML=cardAgentBlock(p);
+}
+async function markContacted(id){
+  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/contacted',{}); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('✓ marked contacted '+fmtWhen(j.contact.contacted_at)); } else toast('⚠ '+(j.error||'failed')); }
+  catch(e){ toast('⚠ '+e.message); }
+}
+async function saveAgentInfo(id){
+  const scope=document.getElementById('composeM')&&document.getElementById('composeM').classList.contains('on')?document.getElementById('composeM'):document;
+  const inputs=[...scope.querySelectorAll('.crmf')].filter(i=>i.dataset.id===id);
+  const body={}; inputs.forEach(i=>body[i.dataset.info]=i.value.trim());
+  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/info',body); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); toast('💾 contact info saved'); } else toast('⚠ '+(j.error||'failed')); }
+  catch(e){ toast('⚠ '+e.message); }
+}
+async function deleteLetter(id,idx){
+  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/letter/'+encodeURIComponent(idx)+'/delete',{}); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('letter deleted'); } }
+  catch(e){ toast('⚠ '+e.message); }
+}
+function agentDraft(p,a){ return `Hi ${a.name||'there'},\n\nI'm interested in ${p.address||'your listing'}${p.city?' in '+p.city:''}${p.price?' (listed '+fmt(p.price)+')':''}. Could you share the setup — current rents / in-place NOI, any financing in place, and whether an offer near list would be entertained?\n\nThanks`; }
+// The full per-listing contact modal body (editable info + mark-contacted + compose/save + letter log).
+// The single per-listing "Agent window" — ONE modal with tabs: Agent · Letters · Find · Comps · History.
+function contactModalBody(p){
+  const a=agentInfo(p), id=p.id, firm=a.firm;
+  const tel=a.phone?`<a class="ain" href="tel:${esc(a.phone).replace(/[^0-9+]/g,'')}">📞 ${esc(a.phone)}</a>`:'';
+  const eml=a.email?`<a class="ain" href="mailto:${esc(a.email)}">✉ ${esc(a.email)}</a>`:'';
+  const letters=a.letters.length?`<div class="crm-letters"><div class="deeplbl">Saved letters (${a.letters.length}) — saved, not sent</div>`
+    +a.letters.map((l,i)=>`<div class="lett"><div class="letth"><b>${esc(l.subject||'(no subject)')}</b>${l.draft_id?`<span class="achip done" title="Gmail draft created${l.to?' to '+esc(l.to):''} — review & send in Gmail">📤 drafted</span>`:''}<span class="letts">${fmtWhen(l.ts)}</span><button class="letdel" data-letdel="${id}" data-idx="${l.id||i}" title="Delete this saved letter">✕</button></div><div class="lettb">${esc(l.body||'')}</div></div>`).join('')+`</div>`:'<div class="tabmut">No saved letters yet.</div>';
+  const find=[];
+  if(firm&&firm!=='Unknown'){
+    find.push(`<a class="findlink" href="${gsearch('"'+firm+'" commercial real estate Los Angeles')}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm)} — Google</a>`);
+    find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/companies/?keywords=${encodeURIComponent(firm)}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm)} — LinkedIn</a>`);
+    find.push(`<button class="findlink" data-firmfilter="${esc(firm).replace(/"/g,'&quot;')}" title="Show only ${esc(firm)} listings in the grid">▦ More from ${esc(firm)} (in-app)</button>`);
+  }
+  if(a.name){
+    find.push(`<a class="findlink" href="${gsearch('"'+a.name+'"'+(firm&&firm!=='Unknown'?' "'+firm+'"':'')+' real estate agent')}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — Google</a>`);
+    find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/people/?keywords=${encodeURIComponent(a.name+(firm&&firm!=='Unknown'?' '+firm:''))}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — LinkedIn</a>`);
+    find.push(`<a class="findlink" href="${gsearch('"'+a.name+'" DRE license California real estate')}" target="_blank" rel="noopener noreferrer">🪪 ${esc(a.name)} — DRE license</a>`);
+  }
+  listingLinkBits(p,firm).forEach(b=>find.push(b));
+  find.push(`<span class="findlink findloop">${loopnetLink(p)}</span>`);
+  const findHtml=find.length?`<div class="findgrid">${find.join('')}</div>`:'<div class="tabmut">Add an agent/firm in the Agent tab to enable lookups.</div>';
+  return `<div class="composeCard tabwin">
+    <div class="composeH">🧑‍💼 <b>${esc(p.address||'listing')}</b>${p.city?', '+esc(p.city):''}<span class="winmeta">${a.name?esc(a.name):'no agent yet'}${a.contacted_at?' · ✓ '+fmtWhen(a.contacted_at):''}</span><button class="composeX" title="Close">✕</button></div>
+    <div class="tabbar">
+      <button class="tabt active" data-tab="agent">Agent</button>
+      <button class="tabt" data-tab="letters">Letters${a.letters.length?' ('+a.letters.length+')':''}</button>
+      <button class="tabt" data-tab="find">Find</button>
+      <button class="tabt" data-tab="comps">Comps</button>
+      <button class="tabt" data-tab="history">History</button>
+    </div>
+    <div class="tabpane active" data-pane="agent">
+      <div class="cm-info">
+        <input class="crmf" data-info="name" data-id="${id}" placeholder="Agent name" value="${esc(a.name||'')}">
+        <input class="crmf" data-info="phone" data-id="${id}" placeholder="Phone" value="${esc(a.phone||'')}">
+        <input class="crmf" data-info="email" data-id="${id}" placeholder="Email" value="${esc(a.email||'')}">
+      </div>
+      ${(tel||eml)?`<div class="ainrow">${tel} ${eml}</div>`:''}
+      <div class="cm-notes"><div class="deeplbl">Notes</div><textarea class="crmf" data-info="notes" data-id="${id}" rows="4" placeholder="Notes on this agent / listing">${esc(a.notes||'')}</textarea></div>
+      <div class="cm-row">
+        <button class="abtn saveinfobtn" data-saveinfo="${id}">💾 Save info + notes</button>
+        <button class="abtn markbtn" data-mark="${id}">${a.contacted_at?'↻ Update contacted date':'✓ Mark contacted'}</button>
+        ${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}
+      </div>
+    </div>
+    <div class="tabpane" data-pane="letters">
+      <div class="composeGate">⚑ <b>Save letter</b> logs it. <b>Send via George</b> creates a Gmail <b>DRAFT</b> — NOT sent; you review &amp; hit send in Gmail.</div>
+      <input id="composeSubj" placeholder="Subject" value="${esc('Interest in '+(p.address||'your listing')+(p.city?', '+p.city:''))}">
+      <textarea id="composeBody" rows="7">${esc(agentDraft(p,a))}</textarea>
+      <div class="composeActs">
+        <button id="composeSave" class="primary" data-id="${id}">💾 Save letter</button>
+        <button id="composeSend" class="gated" data-id="${id}" title="Create a Gmail DRAFT to the agent (fill Email in the Agent tab) — not sent">📤 Send via George</button>
+        <button id="composeCopy">📋 Copy</button>
+      </div>
+      ${letters}
+    </div>
+    <div class="tabpane" data-pane="find">${findHtml}</div>
+    <div class="tabpane" data-pane="comps">
+      <button class="lcbtn" data-id="${id}" title="Pull fresh comps via a cloud browser (~$0.03)">🔄 Pull live comps (~$0.03)</button>
+      <div class="lcout" id="lc-${id}"></div>
+    </div>
+    <div class="tabpane" data-pane="history">
+      <button class="histbtn" data-id="${id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (free)">📜 Load property history</button>
+      <div class="histout" id="hist-${id}"></div>
+    </div>
+  </div>`;
+}
+// One modal, opens for EVERY listing from any surface. Its own click delegation (it lives on
+// document.body, outside #grid) handles save-info / mark / save-letter / delete-letter.
+function openContact(id){
+  const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
+  let m=document.getElementById('composeM');
+  if(!m){ m=document.createElement('div'); m.id='composeM'; m.className='composeM'; document.body.appendChild(m); }
+  m.dataset.id=id; m.innerHTML=contactModalBody(p); m.classList.add('on');
+  m.onclick=async(e)=>{
+    if(e.target===m || e.target.closest('.composeX,.composeX2')){ closeCompose(); return; }
+    const tb=e.target.closest('.tabt'); if(tb){ activateTab(m,tb.dataset.tab); return; }
+    const cv=e.target.closest('[data-copyval]'); if(cv){ navigator.clipboard.writeText(cv.dataset.copyval).then(()=>toast('📋 copied')).catch(()=>{}); return; }
+    const ff=e.target.closest('[data-firmfilter]'); if(ff){ closeCompose(); F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
+    const lc=e.target.closest('.lcbtn'); if(lc){ fetchLiveComps(lc.dataset.id); return; }
+    const hi=e.target.closest('.histbtn'); if(hi){ fetchHistory(hi); return; }
+    if(e.target.closest('#composeCopy')){ navigator.clipboard.writeText(m.querySelector('#composeBody').value); toast('📋 copied'); return; }
+    if(e.target.closest('#composeSave')){
+      const subject=m.querySelector('#composeSubj').value, body=m.querySelector('#composeBody').value;
+      try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/letter',{subject,body,channel:'letter'});
+        if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('💾 letter saved to contact log'); }
+        else toast('⚠ '+(j.error||'save failed')); }catch(err){ toast('⚠ '+err.message); }
+      return;
+    }
+    if(e.target.closest('#composeSend')){
+      const to=((m.querySelector('.crmf[data-info=email]')||{}).value||'').trim();
+      if(!to){ toast('add the agent email first'); return; }
+      const subject=m.querySelector('#composeSubj').value, body=m.querySelector('#composeBody').value;
+      if(!confirm('Create a Gmail DRAFT to '+to+' via George?\n\nIt will NOT be sent — it lands in your Gmail drafts (steve@designerwallcoverings.com) for you to review and send.')) return;
+      try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/send-draft',{to,subject,body});
+        if(j.ok){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('📤 Gmail draft created — review & send in Gmail'); }
+        else if(j.error==='george_not_configured'){ toast('⚠ George drafting not enabled on this box'); alert('George drafting isn’t enabled on this server yet — the letter is not drafted.\n\nTo turn it on, GEORGE_AUTH must be set in the CRCP env (one-time). The letter is unaffected; use 💾 Save letter to keep it.'); }
+        else toast('⚠ '+(j.detail||j.error||'draft failed')); }
+      catch(err){ toast('⚠ '+err.message); }
+      return;
+    }
+    if(e.target.closest('[data-saveinfo]')){ saveAgentInfo(id); return; }
+    if(e.target.closest('[data-mark]')){ markContacted(id); return; }
+    const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(id, ld.dataset.idx); return; }
+  };
+}
+// Switch the active tab within the agent window.
+function activateTab(m,tab){
+  m.querySelectorAll('.tabt').forEach(t=>t.classList.toggle('active',t.dataset.tab===tab));
+  m.querySelectorAll('.tabpane').forEach(pn=>pn.classList.toggle('active',pn.dataset.pane===tab));
+}
+// Repaint the open modal in place (after mark/letter changes) — preserves open state AND active tab.
+function repaintModal(id){ const m=document.getElementById('composeM'); const p=DATA.ranked.find(x=>x.id===id);
+  if(m && m.classList.contains('on') && m.dataset.id===id && p){
+    const cur=(m.querySelector('.tabt.active')||{}).dataset; const tab=cur?cur.tab:'agent';
+    m.innerHTML=contactModalBody(p); activateTab(m,tab||'agent');
+  } }
+function openCompose(id){ openContact(id); }   // back-compat alias
+function closeCompose(){ const m=document.getElementById('composeM'); if(m) m.classList.remove('on'); }
+
+// ---- Broker-history pilot index ----
+// Keyed by both listing id (e.g. crx2477882) and lowercase address.
+// Loaded once at boot; cards read from this map at render time.
+window.BROKER_HIST = {};   // id → enrichment object
+window.BROKER_HIST_ADDR = {};  // lc-address → enrichment object
+fetch('/api/broker-history').then(r=>r.json()).then(j=>{
+  (j.properties||[]).forEach(p=>{
+    if(p.id) window.BROKER_HIST[p.id]=p;
+    if(p.address) window.BROKER_HIST_ADDR[p.address.toLowerCase().trim()]=p;
+  });
+  // Re-render if DATA is already loaded (race: demographics loads fast too)
+  if(window.DATA_READY) render();
+}).catch(()=>{}); // graceful — broker pilot data is additive, not load-blocking
+
+// ---- boot ----
+fetch('data/comps.json').then(r=>r.json()).then(c=>{ if(DATA) DATA.comps=c; else window.__comps=c; }).catch(()=>{});
+fetch('data/demographics.json').then(r=>r.json()).then(d=>{ window.DEMO=d.byCity||{}; if(DATA) render(); }).catch(()=>{});
+// Map a condo (separate /api/condos schema) into the explorer's property shape so it flows through
+// finance, scoring, filters, sort, and the card/list renderers as a first-class 'Condo' asset type.
+function mapCondo(c){
+  return { id:c.id, address:c.address, city:c.city, zip:c.zip, type:'Condo',
+    price:+c.price||0, units:1, cap_rate:null, year_built:c.year_built||null,
+    status:'Active', source:c.source, firm:c.firm_name||null, verified:false,
+    isCondo:true, warrantable_status:c.warrantable_status,
+    beds:c.beds, baths:c.baths, sqft:c.sqft, hoa:c.hoa, broker_name:c.broker_name,
+    project_name:c.project_name, upside_note:null, qwen:null };
+}
+function loadRanked(_retry){ return fetch('data/ranked.json').then(r=>{ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); }).catch(e=>{ if(_retry>0){ return new Promise(res=>setTimeout(res,1500)).then(()=>loadRanked(_retry-1)); } throw e; }); } loadRanked(1).then(async d=>{
+  DATA=d; if(window.__comps) DATA.comps=window.__comps;
+  // Merge condo inventory in-grid (best-effort; explorer still works if the API is down).
+  let condoN=0; try{ const cd=await (await fetch('/api/condos')).json(); const cs=(cd.condos||[]).map(mapCondo); condoN=cs.length; DATA.ranked=DATA.ranked.concat(cs); }catch(e){}
+  $('#sub').innerHTML = `${d.ranked.length} listings${condoN?` · ${condoN} condos`:''} · <b>${d.meta.market}</b>${d.generated?' · sourced '+d.generated:''}`;
+
+  scenario = localStorage.getItem('cre_scenario')||'25';
+  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===scenario));
+  buildFacets();
+  loadState();
+  urlToState();  // a shared ?query overrides saved localStorage state and restores the exact view
+  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===scenario));
+
+  // drawer wiring
+  $('#burger').onclick=openDrawer; $('#dclose').onclick=closeDrawer; $('#scrim').onclick=closeDrawer;
+  $('#reset').onclick=resetAll;
+  // collapsible filter tabs — click a section header to expand/collapse; all collapsed on load
+  $('#drawer').addEventListener('click',e=>{ const h=e.target.closest('h4'); if(h && h.parentElement.classList.contains('fsec')) h.parentElement.classList.toggle('collapsed'); });
+  // Fields section starts EXPANDED and first — all other fsec sections collapse on load.
+  $$('#drawer .fsec').forEach(s=>{ if(s.id!=='fieldsSec') s.classList.add('collapsed'); });
+  // active-filter summary bar: remove one filter or reset all
+  $('#activebar').addEventListener('click',e=>{ if(e.target.closest('#afreset')){ resetAll(); return; } const c=e.target.closest('.afchip'); if(c) clearFilter(c.dataset.clear); });
+  // saved searches
+  buildSavedList();
+  $('#savedList').addEventListener('click',e=>{ const del=e.target.closest('[data-del]'); if(del){ SAVED.splice(+del.dataset.del,1); saveSaved(); buildSavedList(); return; } const n=e.target.closest('.savedname'); if(n){ location.href=location.pathname+(n.dataset.qs||''); } });
+  $('#savebtn').onclick=()=>{ const name=(prompt('Name this saved search:')||'').trim(); if(!name) return; SAVED.push({name, qs:location.search||''}); saveSaved(); buildSavedList(); };
+  $('#linkbtn').onclick=copyShareLink;
+  $('#csvbtn').onclick=exportCSV;
+  // search-all-fields box (Steve list-build rule 7) — filters across EVERY field, space = AND
+  { const si=$('#idxSearch'); if(si) si.oninput=()=>{ idxQuery=si.value.trim().toLowerCase(); if($('#railSearch'))$('#railSearch').value=si.value; render(); }; }
+  // Reset the PRIMARY List table layout back to the tight default: column order, resize widths,
+  // and visibility. Clears idxColOrder + col-resize's cr:<pathname> widths + resets VIS to defaults.
+  { const rb=$('#idxReset'); if(rb) rb.onclick=()=>{
+      COLORDER=[]; saveColOrder();
+      try{ Object.keys(localStorage).forEach(k=>{ if(k.indexOf('cr:'+location.pathname+':')===0) localStorage.removeItem(k); }); }catch(e){}
+      FIELDS.forEach(f=>VIS[f.k]=!!f.def); saveVis();
+      lastColSig=''; buildFieldToggles(); render();
+    }; }
+  // facet chip handlers (multi-select toggles)
+  $('#fType').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return;
+    if(c.dataset.warr==='nonqm'){ F.nonqm=!F.nonqm; syncChips(); render(); return; }
+    if(c.dataset.warr==='unwarrantable'){ F.unwarrantable=!F.unwarrantable; if(F.unwarrantable) F.types=new Set(['Condo']); syncChips(); render(); return; }
+    F.types.has(c.dataset.t)?F.types.delete(c.dataset.t):F.types.add(c.dataset.t);
+    if(!F.types.has('Condo')) F.unwarrantable=false;
+    syncChips(); render(); };
+  $('#fFirm').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; F.firms.has(c.dataset.fm)?F.firms.delete(c.dataset.fm):F.firms.add(c.dataset.fm); syncChips(); render(); };
+  $('#fCityChips').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; const cy=c.dataset.cy.toLowerCase(); F.cityQuery=(F.cityQuery===cy)?'':cy; $('#fCity').value=(F.cityQuery?c.dataset.cy:''); syncChips(); render(); };
+  $('#fStatus').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; F.status=c.dataset.st; syncChips(); render(); };
+  $('#fCity').oninput=()=>{ F.cityQuery=$('#fCity').value.trim().toLowerCase(); syncChips(); render(); };
+  $('#fPriceMin').oninput=()=>{ F.priceMin=$('#fPriceMin').value?+$('#fPriceMin').value:null; render(); };
+  $('#fPriceMax').oninput=()=>{ F.priceMax=$('#fPriceMax').value?+$('#fPriceMax').value:null; render(); };
+  $('#fCap').oninput=()=>{ F.capMin=+$('#fCap').value; $('#capVal').textContent=F.capMin?F.capMin+'%+':'any'; render(); };
+  $('#fUnits').oninput=()=>{ F.unitsMin=$('#fUnits').value?+$('#fUnits').value:null; render(); };
+  $('#fUnitsMax').oninput=()=>{ F.unitsMax=$('#fUnitsMax').value?+$('#fUnitsMax').value:null; render(); };
+  $('#fYear').oninput=()=>{ F.yearMin=$('#fYear').value?+$('#fYear').value:null; render(); };
+  $('#fYearMax').oninput=()=>{ F.yearMax=$('#fYearMax').value?+$('#fYearMax').value:null; render(); };
+  $('#fAfford').onchange=()=>{ F.afford=$('#fAfford').checked; render(); };
+  $('#fHasPhone').onchange=()=>{ F.hasPhone=$('#fHasPhone').checked; render(); };
+  $('#fHasEmail').onchange=()=>{ F.hasEmail=$('#fHasEmail').checked; render(); };
+  $('#scenario').onclick=e=>{ if(!e.target.dataset.s) return; setScenario(e.target.dataset.s); render(); };
+
+  // sort (drawer + top bar mirror each other)
+  $('#sort').onchange=()=>{ colSort=null; $('#sortTop').value=$('#sort').value; render(); };
+  $('#sortTop').onchange=()=>{ colSort=null; $('#sort').value=$('#sortTop').value; render(); };
+  $('#sortTop').value=$('#sort').value;
+  // density
+  const dv=String(Math.min(10,Math.max(1,+(localStorage.getItem('cre_density')||'3')||3))); $('#density').value=dv; $('#densVal').textContent=dv; document.documentElement.style.setProperty('--cols',dv); localStorage.setItem('cre_density',dv);
+  $('#density').oninput=()=>{ document.documentElement.style.setProperty('--cols',$('#density').value); $('#densVal').textContent=$('#density').value; localStorage.setItem('cre_density',$('#density').value); if($('#railDensity')){$('#railDensity').value=$('#density').value; $('#railDensVal').textContent=$('#density').value;} };
+
+  // ===== right-hand control rail (nav-agent skill surface) =====
+  wireCtrlRail(dv);
+
+  // field show/hide toggles + list/grid view
+  buildFieldToggles();
+  $('#fFields').onchange=e=>{ const cb=e.target.closest('input[data-fk]'); if(!cb) return; VIS[cb.dataset.fk]=cb.checked; saveVis(); buildFieldToggles(); render(); };
+  { const rf=$('#railFields'); if(rf) rf.onchange=e=>{ const cb=e.target.closest('input[data-fk]'); if(!cb) return; VIS[cb.dataset.fk]=cb.checked; saveVis(); buildFieldToggles(); render(); }; }
+  $('#fAll').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=true); saveVis(); buildFieldToggles(); render(); };
+  $('#fNone').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=false); saveVis(); buildFieldToggles(); render(); };
+  $('#fDef').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=!!f.def); saveVis(); buildFieldToggles(); render(); };
+  // ── panel drag-to-reorder columns (Fix 3) ──
+  // Dragging the ⠿ handle on a .ftog row reorders COLORDER then rebuilds toggles + re-renders list.
+  let pDragKey=null;
+  $('#fFields').addEventListener('dragstart',e=>{ const row=e.target.closest('.ftog'); if(!row) return;
+    if(!e.target.closest('.fdrag')){ e.preventDefault(); return; }  // only ⠿ handle starts a move
+    pDragKey=row.dataset.fk; e.dataTransfer.effectAllowed='move'; try{e.dataTransfer.setData('text/plain',pDragKey);}catch(_){}
+    row.classList.add('fdragging'); });
+  $('#fFields').addEventListener('dragover',e=>{ if(!pDragKey) return; const row=e.target.closest('.ftog'); if(!row||row.dataset.fk===pDragKey) return;
+    e.preventDefault(); e.dataTransfer.dropEffect='move';
+    const r=row.getBoundingClientRect(), after=(e.clientY-r.top)>r.height/2;
+    $('#fFields').querySelectorAll('.ftog').forEach(x=>x.classList.remove('fdropT','fdropB'));
+    row.classList.toggle('fdropB',after); row.classList.toggle('fdropT',!after); });
+  $('#fFields').addEventListener('dragleave',e=>{ const row=e.target.closest('.ftog'); if(row) row.classList.remove('fdropT','fdropB'); });
+  $('#fFields').addEventListener('drop',e=>{ if(!pDragKey) return; const row=e.target.closest('.ftog'); if(!row){ pDragKey=null; return; } e.preventDefault();
+    const tgt=row.dataset.fk;
+    if(tgt!==pDragKey){
+      const r=row.getBoundingClientRect(), after=(e.clientY-r.top)>r.height/2;
+      syncColOrder(); const from=COLORDER.indexOf(pDragKey);
+      if(from>=0){ COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,pDragKey);
+        saveColOrder(); lastColSig=''; buildFieldToggles(); render(); } }
+    pDragKey=null; });
+  $('#fFields').addEventListener('dragend',()=>{ $('#fFields').querySelectorAll('.fdragging,.fdropT,.fdropB').forEach(x=>x.classList.remove('fdragging','fdropT','fdropB')); pDragKey=null; });
+  $$('#viewseg button').forEach(b=>b.classList.toggle('active',b.dataset.view===view));
+  $('#viewseg').onclick=e=>{ const b=e.target.closest('button[data-view]'); if(!b) return; view=b.dataset.view; localStorage.setItem('cre_view',view); $$('#viewseg button').forEach(x=>x.classList.toggle('active',x.dataset.view===view)); render(); };
+
+  // chat
+  const EX=['Show only multifamily under $1M, sorted by cash-on-cash','Which deals actually cash-flow at 30% down?','Office and industrial in Pasadena','Take me to the Reseda mixed-use','Best in-budget deals'];
+  $('#examples').innerHTML = EX.map(x=>`<button class="ex">${x}</button>`).join('');
+  $('#examples').onclick=e=>{ if(e.target.classList.contains('ex')){ $('#q').value=e.target.textContent; $('#q').focus(); } };
+  $('#chatform').onsubmit=e=>{ e.preventDefault(); const t=$('#q').value.trim(); if(!t) return; $('#q').value=''; ask(t); };
+  $('#grid').onclick=e=>{ const cp=e.target.closest('[data-copy]'); if(cp){ copySummary(cp.dataset.copy, cp); return; } const qk=e.target.closest('[data-quick]'); if(qk){ applyQuick(qk.dataset.quick); return; } const more=e.target.closest('#showmore'); if(more){ _shown=Math.min(_shown+PAGE(), _rows.length); paintPage(); return; } const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); const sb=$('#shortbtn'); if(sb) sb.textContent='⭐ Shortlist ('+SHORT.size+')'; if(F.shortlist) render(); return; } const th=e.target.closest('.listtbl th[data-sortk]'); if(th){ const k=th.dataset.sortk; if(colSort&&colSort.key===k) colSort.dir*=-1; else colSort={key:k,dir:['type','firm','warr','broker','status','zip','addr','rec','phone','email','firmweb'].includes(k)?1:-1}; render(); return; } const b=e.target.closest('.lcbtn'); if(b){ fetchLiveComps(b.dataset.id); return; } const h=e.target.closest('.histbtn'); if(h){ fetchHistory(h); return; }
+    const cv=e.target.closest('[data-copyval]'); if(cv){ navigator.clipboard.writeText(cv.dataset.copyval).then(()=>toast('📋 copied')).catch(()=>toast('⚠ copy failed')); return; }
+    const ff=e.target.closest('[data-firmfilter]'); if(ff){ if(e.metaKey||e.ctrlKey||e.shiftKey) F.firms.add(ff.dataset.firmfilter); else F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
+    // drill atoms: card asset-type + city → filter the grid to that value (URL updates via render→stateToURL)
+    const tf=e.target.closest('[data-typefilter]'); if(tf){ if(e.metaKey||e.ctrlKey||e.shiftKey) F.types.add(tf.dataset.typefilter); else F.types=new Set([tf.dataset.typefilter]); syncChips(); render(); return; }
+    const cf=e.target.closest('[data-cityfilter]'); if(cf){ const cy=cf.dataset.cityfilter.toLowerCase(); F.cityQuery=(F.cityQuery===cy)?'':cy; if($('#fCity'))$('#fCity').value=(F.cityQuery?cf.dataset.cityfilter:''); syncChips(); render(); return; }
+    const ct=e.target.closest('[data-contact]'); if(ct){ openCompose(ct.dataset.contact); return; }
+    const mk=e.target.closest('[data-mark]'); if(mk){ markContacted(mk.dataset.mark); return; }
+    const si=e.target.closest('[data-saveinfo]'); if(si){ saveAgentInfo(si.dataset.saveinfo); return; }
+    const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(ld.dataset.letdel, ld.dataset.idx); return; }
+    // Row-click NO LONGER opens the agent pop-out (Steve 2026-08-18: "do not allow this to pop out, or
+    // move round. fix in place"). Contact info renders directly in the row; explicit links/buttons still work.
+  };
+  // ── drag-to-reorder PRIMARY List columns (Steve list-build rule 2) ──
+  // Delegated on #grid since the list table is re-rendered each render(). Property (addr) is a
+  // fixed anchor (no data-k) so it never participates as a drag source or drop target.
+  let idxDragKey=null;
+  $('#grid').addEventListener('dragstart',e=>{ const th=e.target.closest('.listtbl th.dragcol'); if(!th||!th.dataset.k) return;
+    if(e.target.closest && e.target.closest('.cr-handle')){ e.preventDefault(); return; }  // that grab is a resize, not a move
+    idxDragKey=th.dataset.k; e.dataTransfer.effectAllowed='move'; try{ e.dataTransfer.setData('text/plain',idxDragKey); }catch(_){}
+    th.classList.add('dragging'); });
+  $('#grid').addEventListener('dragover',e=>{ if(!idxDragKey) return; const th=e.target.closest('.listtbl th.dragcol'); if(!th||!th.dataset.k||th.dataset.k===idxDragKey) return;
+    e.preventDefault(); e.dataTransfer.dropEffect='move';
+    const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
+    th.classList.toggle('dropR',after); th.classList.toggle('dropL',!after); });
+  $('#grid').addEventListener('dragleave',e=>{ const th=e.target.closest('.listtbl th.dragcol'); if(th) th.classList.remove('dropL','dropR'); });
+  $('#grid').addEventListener('drop',e=>{ if(!idxDragKey) return; const th=e.target.closest('.listtbl th.dragcol'); if(!th||!th.dataset.k){ idxDragKey=null; return; }
+    e.preventDefault(); const tgt=th.dataset.k;
+    if(tgt!==idxDragKey){ const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
+      syncColOrder(); const from=COLORDER.indexOf(idxDragKey);
+      if(from>=0){ COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,idxDragKey);
+        saveColOrder(); lastColSig=''; render(); } }
+    idxDragKey=null; });
+  $('#grid').addEventListener('dragend',()=>{ document.querySelectorAll('.listtbl th.dragging,.listtbl th.dropL,.listtbl th.dropR').forEach(x=>x.classList.remove('dragging','dropL','dropR')); idxDragKey=null; });
+  $('#shortbtn').onclick=()=>{ F.shortlist=!F.shortlist; render(); };
+  $('#cmpbtn').onclick=openCompare;
+  $('#heat').value=heat; $('#heat').onchange=()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat',heat); render(); };
+  $('#cmpov').addEventListener('click',e=>{ if(e.target.id==='cmpov') e.currentTarget.classList.remove('on'); });
+  $('#cmpmodal').addEventListener('click',e=>{ if(e.target.closest('#cmpcsv')) exportCompareCSV(); else if(e.target.closest('#cmpprint')) printCompare(); });
+  window.addEventListener('afterprint',()=>document.body.classList.remove('printing-compare'));
+  // a11y: Escape closes any open overlay (compare / broker / property-history modals)
+  document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ ['cmpov','bkov'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('on'); }); } });
+
+  window.DATA_READY = true;
+  recompute(); syncChips(); render();
+}).catch(e=>{ $('#sub').textContent='Listings are still loading — refresh in a moment if this persists.'; console.warn('ranked.json load failed:',e); });
+</script>
+  <script src="/col-resize.js" defer></script>
+  <script src="/global-search.js" defer></script>
+<script src="/corner-nav.js" defer></script>
+<script>
+// Day/Night toggle — theme already applied before first paint by the head snippet.
+// crcp-theme key; Day (light) is the default. Only an explicit click persists.
+(function(){
+  var root=document.documentElement;
+  function label(t){
+    var b=document.getElementById('crcpThemeToggle'); if(!b) return;
+    b.innerHTML=(t==='light'?'☀️':'🌙')+' <span>'+(t==='light'?'Day':'Night')+'</span>';
+  }
+  function applyTheme(t){ root.setAttribute('data-theme',t); label(t); }
+  function mount(){
+    if(document.getElementById('crcpThemeToggle')){ label(root.getAttribute('data-theme')||'light'); return; }
+    var btn=document.createElement('button');
+    btn.id='crcpThemeToggle'; btn.type='button';
+    btn.title='Toggle day / night'; btn.setAttribute('aria-label','Toggle day / night theme');
+    btn.addEventListener('click',function(){
+      var nt=root.getAttribute('data-theme')==='light'?'dark':'light';
+      applyTheme(nt);
+      try{ localStorage.setItem('crcp-theme',nt); }catch(e){}
+    });
+    var host=document.querySelector('header .nav')||document.querySelector('header');
+    if(host){ host.appendChild(btn); } else { btn.style.cssText+=';position:fixed;top:12px;right:14px;z-index:99999'; document.body.appendChild(btn); }
+    label(root.getAttribute('data-theme')||'light');
+  }
+  if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',mount); else mount();
+})();
+</script>
+  <!-- nav-agent REMOVED (Steve 2026-08-18: "no more pop out … fix into the ui"): its floating
+       movable/removable panel overlay (.na-restorebar, position:fixed) popped out over the grid.
+       The native Fields show/hide/reorder panel (#fFields) is independent and stays. -->
+  <!-- <script src="/nav-agent/nav-agent.js" defer></script> -->
+
+  <!-- ── Controls-rail resize bar (Steve 2026-08-20): drag the #ctrlrail (Controls) panel wider ──
+       A fixed vertical grab-bar pinned to the rail's left edge via right:var(--railW), so it auto-
+       tracks the width and never scrolls with the rail content. Drag = resize, dbl-click = reset,
+       width persisted to localStorage. Hidden with the rail below 1100px. -->
+  <style>
+    .railrsz{ position:fixed; right:var(--railW,272px); top:57px; bottom:0; width:10px; margin-right:-5px; cursor:ew-resize; z-index:25; }
+    .railrsz::before{ content:''; position:absolute; left:4px; top:0; bottom:0; width:2px; background:transparent; transition:background .15s; }
+    .railrsz:hover::before, .railrsz.rsz-active::before{ background:var(--blue,#3b7dff); }
+    body.rsz-dragging{ cursor:ew-resize; user-select:none; }
+    @media (max-width:1100px){ .railrsz{ display:none; } }
+  </style>
+  <script>
+  (function(){
+    var rail=document.getElementById('ctrlrail'); if(!rail) return;
+    var KEY='crcp_railW';
+    try{ var s=parseInt(localStorage.getItem(KEY),10); if(s>=200&&s<=760) document.documentElement.style.setProperty('--railW', s+'px'); }catch(_){}
+    var h=document.createElement('div'); h.className='railrsz'; h.title='Drag to resize the Controls panel · double-click to reset'; document.body.appendChild(h);
+    var dragging=false;
+    function onMove(e){ if(!dragging) return; var w=Math.max(200, Math.min(760, window.innerWidth - e.clientX)); document.documentElement.style.setProperty('--railW', w+'px'); }
+    function onUp(){ if(!dragging) return; dragging=false; document.body.classList.remove('rsz-dragging'); h.classList.remove('rsz-active');
+      document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp);
+      try{ localStorage.setItem(KEY, Math.round(rail.getBoundingClientRect().width)); }catch(_){} }
+    h.addEventListener('mousedown',function(e){ e.preventDefault(); dragging=true; document.body.classList.add('rsz-dragging'); h.classList.add('rsz-active');
+      document.addEventListener('mousemove',onMove); document.addEventListener('mouseup',onUp); });
+    h.addEventListener('dblclick',function(){ document.documentElement.style.removeProperty('--railW'); try{ localStorage.removeItem(KEY); }catch(_){} });
+  })();
+  </script>
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
+<script src="/user-badge.js" defer></script><div id="__geo" style="position:fixed;bottom:0;left:0;z-index:2147483647;font:10px monospace;background:#000;color:#0f0;padding:2px">m</div><script>window.addEventListener("load",function(){setTimeout(function(){document.documentElement.style.removeProperty("--cnav-rsv-l");document.documentElement.style.removeProperty("--cnav-rsv-r");setTimeout(function(){var burger=document.querySelector(".cnav-burger.left"),h1=document.querySelector(".htitle h1");var bb=burger.getBoundingClientRect(),hh=h1.getBoundingClientRect();var padL=getComputedStyle(document.querySelector("header")).paddingLeft;document.getElementById("__geo").textContent="FALLBACK-ONLY padL="+padL+" burgerRight="+Math.round(bb.right)+" titleX="+Math.round(hh.left)+" GAP="+Math.round(hh.left-bb.right)+" OVERLAP="+(bb.right>hh.left);},200);},1800);});</script></body>
+</html>
diff --git a/front-page-audit/screens/crcp/repro/real-index-probe.html b/front-page-audit/screens/crcp/repro/real-index-probe.html
new file mode 100644
index 0000000..22ee6ff
--- /dev/null
+++ b/front-page-audit/screens/crcp/repro/real-index-probe.html
@@ -0,0 +1,2548 @@
+<!doctype html>
+<html lang="en">
+<head>
+<script>/* credential-safe fetch (2026-08-18): resolve EVERY relative fetch (with or without a leading slash) against a credential-free base so creds embedded in the page URL (http://user:pass@host/...) don't poison document.baseURI and throw "Request cannot be constructed from a URL that includes credentials". */(function(){var of=window.fetch,O=location.origin;window.fetch=function(u,o){try{if(typeof u==='string'){if(/^[a-z][a-z0-9+.-]*:/i.test(u)){var x=new URL(u);if(x.username||x.password){x.username='';x.password='';u=x.href;}}else{u=new URL(u,O+location.pathname+location.search).href;}}else if(u&&typeof u.url==='string'){var y=new URL(u.url,O+location.pathname+location.search);if(y.username||y.password){y.username='';y.password='';u=new Request(y.href,u);}}}catch(e){}return of.call(this,u,o);};try{if(location.href.indexOf('@')>=0){history.replaceState(null,'',location.pathname+location.search+location.hash);}}catch(e){}})();</script>
+<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='dark'?'dark':'light');}catch(e){document.documentElement.setAttribute('data-theme','light');}})();</script>
+<!-- Self-heal: nav-agent grid controls can persist a "removed"/"collapsed" state that hides the
+     whole grid with no obvious way back. Clear it before nav-agent runs so a grid can never stay hidden. -->
+<script>(function(){try{for(var i=localStorage.length-1;i>=0;i--){var k=localStorage.key(i);if(k&&k.indexOf('navagent:')===0){var v=localStorage.getItem(k);if(v&&v.indexOf('"removed":true')>=0){localStorage.removeItem(k);}}}}catch(e){}})();</script>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230e1116'/%3E%3Ctext x='8' y='12' font-size='11' text-anchor='middle' fill='%233fb950'%3E%24%3C/text%3E%3C/svg%3E">
+<title>LA County CRE — Investment Explorer</title>
+<style>
+  :root { --cols:3; --drawerW:316px; }
+  * { box-sizing:border-box; }
+  :focus-visible { outline:2px solid var(--blue); outline-offset:2px; border-radius:4px; }
+  @media (prefers-reduced-motion: reduce){ *,*::before,*::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; scroll-behavior:auto !important; } }
+  body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }
+
+  /* ---- top bar ---- */
+  header { display:flex; align-items:center; gap:14px; padding:12px 20px; border-bottom:1px solid var(--line); position:sticky; top:0; z-index:30; background:var(--headbg); backdrop-filter:blur(8px); }
+  .burger { display:inline-flex; align-items:center; gap:8px; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:9px 13px; font-size:15px; cursor:pointer; font-weight:600; }
+  .burger:hover { border-color:var(--blue); }
+  .fcount { display:none; background:var(--blue); color:#04122b; font-size:11px; font-weight:700; border-radius:20px; padding:1px 7px; }
+  .fcount.on { display:inline-block; }
+  .htitle h1 { margin:0; font-size:18px; letter-spacing:.2px; }
+  .htitle .sub { color:var(--mut); font-size:12px; }
+  header .spacer { margin-left:auto; }
+  /* nav-link cluster; on wide screens it sits inline right-aligned, on tablets/phones it becomes a swipeable strip (see @media 1024) */
+  .topnav { display:flex; align-items:center; flex-wrap:nowrap; }
+  .seg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .seg button { background:var(--card); color:var(--ink); border:0; border-right:1px solid var(--line); padding:8px 11px; font-size:12px; cursor:pointer; }
+  .seg button:last-child { border-right:0; }
+  .seg button.active { background:#1f6feb; color:#fff; }
+  #drawer .seg { display:flex; width:100%; }
+  #drawer .seg button { flex:1; text-align:center; padding:9px 6px; }
+
+  /* ---- left drawer ---- */
+  #scrim { position:fixed; inset:0; background:rgba(0,0,0,.5); opacity:0; pointer-events:none; transition:opacity .2s; z-index:38; }
+  #scrim.on { opacity:1; pointer-events:auto; }
+  #drawer { position:fixed; top:0; left:0; height:100vh; width:var(--drawerW); background:var(--bg2); border-right:1px solid var(--line); transform:translateX(-100%); transition:transform .22s ease; z-index:40; overflow-y:auto; padding:0 0 40px; }
+  #drawer.open { transform:translateX(0); box-shadow:6px 0 30px rgba(0,0,0,.4); }
+  .dhead { display:flex; align-items:center; justify-content:space-between; padding:16px 18px 10px; position:sticky; top:0; background:var(--bg2); border-bottom:1px solid var(--line); z-index:2; }
+  .dhead b { font-size:15px; } .dhead .x { background:none; border:0; color:var(--mut); font-size:18px; cursor:pointer; }
+  .fsec { padding:14px 18px; border-bottom:1px solid var(--line); }
+  .fsec h4 { margin:0 0 9px; font-size:11px; text-transform:uppercase; letter-spacing:.6px; color:var(--mut); }
+  .chips { display:flex; flex-wrap:wrap; gap:6px; }
+  .chip { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:18px; padding:5px 11px; font-size:12px; cursor:pointer; user-select:none; }
+  .chip:hover { color:var(--ink); }
+  .chip.active { border-color:var(--blue); color:var(--blue); background:rgba(88,166,255,.08); }
+  .chip .ct { color:var(--mut); font-size:10px; margin-left:4px; }
+  .chip.zero { opacity:.4; }
+  .row2 { display:flex; gap:8px; }
+  .fld { flex:1; }
+  .fld label { display:block; font-size:10px; color:var(--mut); margin-bottom:3px; }
+  input[type=text], input[type=number], select { width:100%; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:8px 10px; font-size:13px; }
+  input[type=range] { width:100%; accent-color:var(--blue); }
+  .rangeval { font-size:11px; color:var(--blue); float:right; }
+  .toggle { display:flex; align-items:center; gap:8px; font-size:13px; cursor:pointer; }
+  .resetbtn { width:calc(100% - 36px); margin:14px 18px 0; background:transparent; color:var(--bad); border:1px solid #7d2b28; border-radius:9px; padding:9px; font-size:13px; cursor:pointer; }
+
+  /* ---- results bar + grid ---- */
+  .bar { display:flex; flex-wrap:wrap; align-items:center; gap:14px; padding:11px 20px; border-bottom:1px solid var(--line); position:sticky; top:57px; background:var(--headbg); backdrop-filter:blur(6px); z-index:20; }
+  .bar .count { font-size:13px; color:var(--mut); } .bar .count b { color:var(--ink); }
+  .bar label { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.5px; }
+  .bar .right { margin-left:auto; display:flex; align-items:center; gap:12px; flex-wrap:wrap; justify-content:flex-end; min-width:0; }  /* wrap so the search/heat/sort controls can't overflow under the right-hand #ctrlrail when main is narrow */
+  .grid { display:grid; grid-template-columns:repeat(var(--cols),minmax(0,1fr)); gap:22px; padding:30px 32px 44px; }
+  @media (max-width:900px){ .grid{ grid-template-columns:repeat(2,minmax(0,1fr));} }
+  /* ---- iPad / tablet / narrow-desktop band (≤1024): keep header ONE fixed row so sticky offsets hold,
+         turn the nav-link cluster into a swipeable horizontal strip instead of clipping off-screen. ---- */
+  @media (max-width:1024px){
+    .grid{ grid-template-columns:repeat(2,minmax(0,1fr)); }  /* docked-drawer band: 2 cols so cards aren't crushed (iPad landscape) */
+    header{ flex-wrap:nowrap; gap:10px; }
+    header .spacer{ display:none; }                    /* nav no longer right-pushed; topnav flexes to fill */
+    .htitle{ min-width:0; flex:0 1 auto; }             /* let the title shrink instead of shoving nav off-screen */
+    .htitle h1{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+    .htitle .sub{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }  /* keep title block 2 lines → header stays ~57px so sticky bar/drawer offsets hold */
+    .topnav{ flex:1 1 auto; min-width:0; overflow-x:auto; overflow-y:hidden;
+             -webkit-overflow-scrolling:touch; scrollbar-width:none; }  /* iOS momentum-scroll, hidden scrollbar */
+    .topnav::-webkit-scrollbar{ display:none; }
+    .topnav > a, .topnav > .seg{ flex:0 0 auto; }      /* items keep intrinsic width; strip scrolls, nothing clips */
+    .topnav > a{ padding:8px 0; }                      /* taller touch target for finger taps */
+    html, body{ overflow-x:hidden; max-width:100%; }   /* no horizontal page scroll on iPad (html is the scroller) */
+    .bar{ max-width:100%; top:auto; }                   /* sort/density strip: no hairline overflow; un-stick so it can't tuck under the taller (burger+title) iPad header */
+  }
+  @media (max-width:620px){ .grid{ grid-template-columns:1fr;} }
+  .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:24px; display:flex; flex-direction:column; gap:10px; position:relative; transition:box-shadow .2s,border-color .2s; }
+  .card.uc { opacity:.62; }
+  .card.flash { border-color:var(--blue); box-shadow:0 0 0 3px rgba(88,166,255,.35); }
+  .top { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; }
+  .rank { font-size:12px; color:var(--mut); }
+  .score { font-size:30px; font-weight:700; line-height:1; }
+  .score small { font-size:11px; color:var(--mut); font-weight:400; display:block; text-align:right; }
+  .addr { font-weight:600; font-size:15px; }
+  .city { color:var(--mut); font-size:13px; }
+  .badges { display:flex; flex-wrap:wrap; gap:6px; }
+  .b { font-size:11px; padding:3px 8px; border-radius:20px; border:1px solid var(--line); color:var(--mut); }
+  .b.type { color:var(--blue); border-color:#284b7a; }
+  .b.deal-badge { font-weight:700; cursor:help; }
+  .b.ok { color:var(--acc); border-color:#1f6f37; }
+  .b.warn { color:var(--warn); border-color:#6b5320; }
+  .b.bad { color:var(--bad); border-color:#7d2b28; }
+  .b.rec-Buy { color:var(--acc); border-color:#1f6f37; font-weight:600; }
+  .b.rec-Watch { color:var(--warn); border-color:#6b5320; font-weight:600; }
+  .b.rec-Pass { color:var(--bad); border-color:#7d2b28; font-weight:600; }
+  .metrics { display:grid; grid-template-columns:1fr 1fr; gap:6px 14px; font-size:13px; border-top:1px solid var(--line); border-bottom:1px solid var(--line); padding:10px 0; }
+  .metrics div { display:flex; justify-content:space-between; }
+  .metrics .k { color:var(--mut); }
+  .metrics .v { font-variant-numeric:tabular-nums; font-weight:600; }
+  .pos { color:var(--acc); } .neg { color:var(--bad); } .na { color:var(--mut); }
+  .thesis { font-size:13px; color:var(--ink); }
+  .lst { font-size:12px; color:var(--mut); margin:2px 0 0; padding-left:16px; }
+  .src { font-size:12px; } .src a { color:var(--blue); text-decoration:none; }
+  .lcbtn,.histbtn { background:transparent; color:var(--warn); border:1px dashed #6b5320; border-radius:14px; padding:2px 8px; font-size:11px; cursor:pointer; }
+  .histbtn { color:var(--blue); border-color:#284b7a; }
+  .lcout,.histout { font-size:11px; color:var(--mut); margin-top:6px; }
+  .lcout a { color:var(--blue); text-decoration:none; }
+  .lcout .lc-item { padding:4px 0; border-top:1px solid var(--line); }
+  .small { font-size:11px; color:var(--mut); }
+  .statusline { font-size:12px; font-weight:600; font-variant-numeric:tabular-nums; padding:5px 9px; border-radius:8px; border:1px solid var(--line); background:rgba(255,255,255,.02); }
+  .statusline.ok { color:var(--acc); border-color:#1f6f37; }
+  .statusline.warn { color:var(--warn); border-color:#6b5320; }
+  .statusline.bad { color:var(--bad); border-color:#7d2b28; }
+  .statusline.mut { color:var(--mut); }
+  .statusline.none { color:var(--warn); border-color:#6b5320; border-style:dashed; }
+  .foot { color:var(--mut); font-size:12px; padding:0 20px 8px; }
+  .empty { padding:60px 20px; text-align:center; color:var(--mut); }
+
+  /* ---- Broker-history + prior-broker flag ---- */
+  .brokerflag { display:inline-flex; align-items:center; gap:5px; font-size:11px; padding:3px 9px; border-radius:20px; background:rgba(255,159,9,.10); color:#ff9f09; border:1px solid rgba(255,159,9,.35); font-weight:700; cursor:help; margin-top:2px; }
+  .txhist { font-size:11px; color:var(--mut); margin-top:4px; line-height:1.5; }
+  .txhist .tx-row { display:flex; gap:8px; align-items:baseline; padding:2px 0; border-bottom:1px dashed rgba(255,255,255,.05); }
+  .txhist .tx-row:last-child { border-bottom:0; }
+  .txhist .tx-date { color:var(--blue); font-variant-numeric:tabular-nums; min-width:72px; }
+  .txhist .tx-ev { flex:1; }
+  .txhist .tx-price { color:var(--acc); font-weight:600; font-variant-numeric:tabular-nums; }
+  .brokerline { font-size:11px; padding:4px 9px; background:rgba(88,166,255,.06); border-radius:7px; border:1px solid rgba(88,166,255,.15); color:#a5c8ff; margin-top:3px; }
+  .brokerline strong { color:var(--blue); }
+
+  /* ---- docked chat ---- */
+  #chatwrap { border-top:1px solid var(--line); background:var(--bg2); padding:16px 20px 40px; }
+  #chatwrap h2 { font-size:15px; margin:0 0 2px; }
+  #chatwrap .hint { color:var(--mut); font-size:12px; margin-bottom:10px; }
+  #log { display:flex; flex-direction:column; gap:10px; max-height:42vh; overflow-y:auto; padding:4px 2px; }
+  .msg { max-width:80%; padding:9px 13px; border-radius:12px; font-size:14px; line-height:1.45; white-space:pre-wrap; word-wrap:break-word; }
+  .msg.u { align-self:flex-end; background:#1f6feb; color:#fff; border-bottom-right-radius:3px; }
+  .msg.a { align-self:flex-start; background:var(--card); border:1px solid var(--line); border-bottom-left-radius:3px; }
+  .msg.a code { background:var(--bg); padding:1px 5px; border-radius:4px; }
+  .msg.sys { align-self:center; color:var(--mut); font-size:12px; background:transparent; }
+  .actiontag { display:inline-block; margin-top:6px; font-size:11px; color:var(--acc); border:1px solid #1f6f37; border-radius:6px; padding:2px 7px; }
+  #chatform { display:flex; gap:10px; margin-top:12px; }
+  #q { flex:1; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:11px 13px; font-size:14px; }
+  #send { background:#1f6feb; color:#fff; border:0; border-radius:10px; padding:0 20px; font-size:14px; font-weight:600; cursor:pointer; }
+  #send:disabled { opacity:.5; cursor:default; }
+  .examples { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
+  .ex { font-size:12px; color:var(--blue); border:1px dashed #284b7a; border-radius:16px; padding:4px 10px; cursor:pointer; background:transparent; }
+
+  /* ---- view toggle (grid/list) ---- */
+  .viewseg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .viewseg button { background:var(--card); color:var(--mut); border:0; border-right:1px solid var(--line); padding:7px 12px; font-size:12px; cursor:pointer; }
+  .viewseg button:last-child { border-right:0; }
+  .viewseg button.active { background:#1f6feb; color:#fff; }
+
+  /* ===== nav-agent inline views: compact List / KV / Table group headers ===== */
+  .compacthost, .kvhost { padding:20px 32px 44px; }
+  .compactlist { display:flex; flex-direction:column; gap:6px; }
+  .crow { display:flex; align-items:center; gap:12px; background:var(--card); border:1px solid var(--line); border-radius:9px; padding:8px 12px; }
+  .crow .starbtn { flex:0 0 auto; }
+  .crow .cid { flex:0 0 220px; min-width:180px; }
+  .crow .cla { display:block; font-weight:600; color:var(--ink); font-size:13.5px; }
+  .crow .clc { display:block; color:var(--mut); font-size:11px; }
+  .crow .cfields { flex:1; display:flex; flex-wrap:wrap; gap:6px 14px; align-items:center; min-width:0; }
+  .cchip { font-size:12px; color:var(--ink); white-space:nowrap; }
+  .cchip .cck { color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.3px; margin-right:3px; }
+  .kvgrid { display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:16px; }
+  .kvcard { background:var(--card); border:1px solid var(--line); border-radius:11px; padding:12px 14px; }
+  .kvhead { display:flex; align-items:center; gap:8px; margin-bottom:8px; padding-bottom:8px; border-bottom:1px solid var(--line); }
+  .kvaddr { font-weight:600; font-size:14px; color:var(--ink); }
+  .kvrow { display:flex; gap:10px; padding:3px 0; font-size:12.5px; border-bottom:1px solid rgba(127,127,127,.08); }
+  .kvrow:last-child { border-bottom:0; }
+  .kvk { flex:0 0 42%; color:var(--mut); text-transform:uppercase; font-size:10px; letter-spacing:.3px; padding-top:2px; }
+  .kvv { flex:1; color:var(--ink); min-width:0; word-break:break-word; }
+  .grouphead { padding:8px 12px; margin:10px 0 4px; background:rgba(88,166,255,.08); border-left:3px solid var(--blue); border-radius:4px; font-size:12px; color:var(--mut); text-transform:uppercase; letter-spacing:.4px; }
+  .grouphead b { color:var(--ink); }
+  .kvgrid .grouphead { grid-column:1/-1; }
+  tr.grouprow td.groupcell { background:rgba(88,166,255,.10); font-size:11px; text-transform:uppercase; letter-spacing:.4px; color:var(--mut); padding:7px 10px; }
+  tr.grouprow td.groupcell b { color:var(--ink); }
+
+  /* ===== right-hand control rail (in-flow, NOT floating) — nav-agent skill surface ===== */
+  #ctrlrail { flex:0 0 var(--railW,272px); width:var(--railW,272px); border-left:1px solid var(--line); background:var(--headbg); align-self:stretch; padding:12px 12px 40px; position:sticky; top:57px; max-height:calc(100vh - 57px); overflow-y:auto; }
+  #ctrlrail .railtitle { font-size:11px; text-transform:uppercase; letter-spacing:.6px; color:var(--mut); margin:2px 4px 12px; display:flex; align-items:center; gap:6px; }
+  .rpanel { border:1px solid var(--line); border-radius:9px; margin-bottom:8px; overflow:hidden; background:var(--card); }
+  .rpanel > .rphead { width:100%; display:flex; align-items:center; justify-content:space-between; gap:8px; background:transparent; color:var(--ink); border:0; padding:10px 12px; font-size:12.5px; font-weight:600; cursor:pointer; text-align:left; }
+  .rpanel > .rphead:hover { color:var(--blue); }
+  .rpanel > .rphead .caret { color:var(--mut); font-size:11px; transition:transform .15s; }
+  .rpanel.open > .rphead .caret { transform:rotate(90deg); color:var(--blue); }
+  .rpanel > .rpbody { display:none; padding:4px 12px 12px; }
+  .rpanel.open > .rpbody { display:block; }
+  .rpbody select, .rpbody input[type=text] { width:100%; background:var(--bg); color:var(--ink); border:1px solid var(--line); border-radius:7px; padding:7px 9px; font-size:12px; margin-top:6px; }
+  .rpbody .rlbl { font-size:11px; color:var(--mut); margin-top:8px; display:block; }
+  .rpbody .ftog { display:flex; width:100%; margin:0 0 4px; }
+  @media (max-width:1100px){ #ctrlrail { display:none; } }   /* on narrow screens the left drawer already carries these controls */
+  /* ---- field show/hide toggles: one CARD per field, click to toggle on/off ---- */
+  .fieldtoggles { display:flex; flex-wrap:wrap; gap:6px; max-height:360px; overflow-y:auto; padding:2px 4px 2px 0; }
+  .ftog { display:inline-flex; align-items:center; gap:5px; font-size:12px; color:var(--mut); cursor:pointer; padding:5px 9px;
+          border:1px solid var(--line); border-radius:9px; background:var(--card); user-select:none; transition:border-color .12s,color .12s,background .12s; }
+  .ftog input { display:none; }                                   /* card IS the toggle; checkbox drives state via :has() */
+  .ftog::before { content:'○'; font-size:11px; color:var(--mut); }
+  .ftog:hover { border-color:var(--blue); color:var(--ink); }
+  .ftog:has(input:checked) { border-color:var(--blue); color:var(--blue); background:rgba(88,166,255,.10); }
+  .ftog:has(input:checked)::before { content:'●'; color:var(--blue); }
+  .ftog .colbadge { color:var(--mut); font-size:10px; }
+  /* panel drag-to-reorder (Fix 3) */
+  .ftog .flbl { flex:1; }
+  .ftog .fdrag { cursor:grab; color:var(--mut); font-size:12px; opacity:.5; user-select:none; line-height:1; margin-right:2px; }
+  .ftog:hover .fdrag { opacity:1; }
+  .ftog.fdragging { opacity:.45; }
+  .ftog.fdropT { box-shadow:inset 0 3px 0 var(--gold); }
+  .ftog.fdropB { box-shadow:inset 0 -3px 0 var(--gold); }
+  .minibtn { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:7px; padding:4px 9px; font-size:11px; cursor:pointer; margin-right:5px; }
+  .minibtn:hover { border-color:var(--blue); color:var(--ink); }
+  /* ---- list view ---- */
+  .listhost { display:block; padding:20px 20px 30px; }
+  .listwrap { overflow:auto; border:1px solid var(--line); border-radius:12px; }
+  /* col-resize.js wraps the table in .cr-scroll (overflow:auto) — THAT is the th's nearest scroll
+     container, so bounding ITS height (not .listwrap's) is what makes the sticky header (thead
+     top:0) AND sticky first column (td.stick left:0) actually freeze while scrolling both axes.
+     --tbl-top = the sticky app-chrome height above the table (tune per layout). */
+  .cr-scroll { max-height:calc(100vh - var(--tbl-top, 178px)); }
+  /* max-content so columns size to their content and expand left/right (no artificial squish);
+     min-width:100% keeps it filling the pane when there are few columns; .listwrap scrolls-x. */
+  .listtbl { width:max-content; min-width:100%; border-collapse:collapse; font-size:12.5px; white-space:nowrap; }
+  .listtbl th, .listtbl td { text-align:right; padding:9px 13px; border-top:1px solid var(--line); }
+  .listtbl th { position:sticky; top:0; background:var(--bg2); color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; border-top:0; z-index:2; }
+  .listtbl td.stick, .listtbl th.stick { text-align:left; position:sticky; left:0; background:var(--card); z-index:1; }
+  .listtbl th.stick { background:var(--bg2); z-index:3; }  /* corner = header row + first col: highest */
+  .listtbl th.sortable { cursor:pointer; } .listtbl th.sortable:hover { color:var(--ink); }
+  /* sort arrow via CSS ::after (not literal text) so the header TEXT stays stable and col-resize's tableSig-keyed saved widths survive re-render (TK-10089) */
+  .listtbl th.asc::after { content:' ▲'; color:var(--gold); } .listtbl th.desc::after { content:' ▼'; color:var(--gold); }
+  /* draggable column reorder on the PRIMARY list table (Steve list-build rule) */
+  .listtbl th.dragcol { cursor:grab; }
+  .listtbl th.dragging { opacity:.45; }
+  .listtbl th.dropL { box-shadow:inset 3px 0 0 var(--blue); }
+  .listtbl th.dropR { box-shadow:inset -3px 0 0 var(--blue); }
+  .listtbl tr.lrow:hover td { background:rgba(88,166,255,.06); }
+  .listtbl tr.lrow:hover td.stick { background:var(--card); }
+  .listtbl .la { font-weight:600; color:var(--ink); text-decoration:none; }
+  .listtbl .la:hover { color:var(--blue); }
+  .listtbl .lc { color:var(--mut); font-size:11px; }
+  .listtbl .lsrc { margin-top:2px; }
+  .srclink { color:var(--blue); text-decoration:none; font-size:11px; font-weight:600; } .srclink:hover { text-decoration:underline; }
+  .listtbl .flink { color:var(--ink); text-decoration:none; border-bottom:1px dotted #3a4453; }
+  .listtbl .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  .listtbl tr.lrow { cursor:pointer; }
+  .listtbl .exptog { display:inline-block; width:11px; color:var(--mut); font-size:10px; transition:transform .15s; }
+  .listtbl tr.lrow.open td.stick { background:var(--card); }
+  .listtbl tr.lexp > td { padding:0; border-top:0; }
+  .listtbl tr.lexp .lexpbody { max-height:0; overflow:hidden; transition:max-height .25s ease; background:var(--bg); }
+  .listtbl tr.lexp.open .lexpbody { max-height:1200px; padding:12px 16px; border-top:1px solid var(--line); }
+  .lexpbody .src { text-align:left; white-space:normal; font-size:12px; }
+  .lexpbody .deep { text-align:left; white-space:normal; margin-top:8px; display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
+  .lexpbody .deeplbl { color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; margin-right:2px; }
+  .lexpbody .deep a, .lexpbody .deep button { font-size:11px; padding:3px 9px; border-radius:14px; border:1px solid var(--line); background:var(--card); color:var(--ink); text-decoration:none; cursor:pointer; }
+  .lexpbody .deep a:hover, .lexpbody .deep button:hover { border-color:var(--blue); color:var(--blue); }
+  .lexpbody .lcout, .lexpbody .histout { text-align:left; white-space:normal; }
+  /* Agent · contact column cell */
+  .listtbl td .agentcell { text-align:left; white-space:normal; min-width:170px; display:flex; flex-direction:column; gap:3px; }
+  .agentcell .amut { color:var(--mut); font-style:italic; font-size:11px; }
+  .agentcell .ainrow { display:flex; flex-wrap:wrap; gap:8px; }
+  .agentcell .ain { color:var(--mut); text-decoration:none; font-size:11px; }
+  .agentcell .ain:hover { color:var(--blue); }
+  .ain a { color:var(--blue); text-decoration:none; } .ain a:hover { text-decoration:underline; }
+  .tierb { font-size:9px; text-transform:uppercase; letter-spacing:.4px; color:var(--mut); border:1px solid var(--line); border-radius:4px; padding:0 4px; vertical-align:middle; }
+  .pocketbadge { font-size:11px; font-weight:600; color:var(--gold,#d29922); background:rgba(210,153,34,.12); border:1px solid rgba(210,153,34,.4); border-radius:5px; padding:1px 7px; white-space:nowrap; }
+  .agentcell .chiprow:empty { display:none; }
+  .agentcell .achip { display:inline-block; font-size:10px; color:var(--mut); background:var(--card); border:1px solid var(--line); border-radius:10px; padding:1px 6px; margin-right:4px; }
+  .agentcell .achip.done { color:#3fb950; border-color:#264f2e; }
+  .agentcell .acts { display:flex; flex-wrap:wrap; gap:5px; margin-top:2px; }
+  .abtn { font-size:10.5px; padding:2px 8px; border-radius:12px; border:1px solid var(--line); background:var(--card); color:var(--ink); cursor:pointer; }
+  .abtn:hover { border-color:var(--blue); color:var(--blue); }
+  .abtn.contactbtn { border-color:#2f5233; color:#8fdfa0; }
+  .abtn.markbtn { border-color:#284b7a; color:#8ec0ff; }
+  /* CRM block inside the inline panel */
+  .lexpbody .crm { margin-top:10px; border-top:1px solid var(--line); padding-top:10px; text-align:left; }
+  .crm-info { display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
+  .crm-info .crmf { background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12px; padding:5px 8px; min-width:150px; }
+  .crm-info .crmf:focus { outline:none; border-color:var(--blue); }
+  .crm-acts { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-top:8px; }
+  .crm-letters { margin-top:10px; }
+  .crm-letters .lett { border:1px solid var(--line); border-radius:8px; padding:7px 9px; margin-top:6px; background:var(--bg); }
+  .crm-letters .letth { display:flex; align-items:center; gap:8px; font-size:12px; }
+  .crm-letters .letts { color:var(--mut); font-size:10px; }
+  .crm-letters .letdel { margin-left:auto; background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:12px; }
+  .crm-letters .letdel:hover { color:var(--bad,#f85149); }
+  .crm-letters .lettb { white-space:pre-wrap; font-size:11.5px; color:var(--mut); margin-top:4px; }
+  /* Compose-letter modal */
+  .composeM { position:fixed; inset:0; background:rgba(0,0,0,.55); display:none; align-items:center; justify-content:center; z-index:9000; }
+  .composeM.on { display:flex; }
+  .composeCard { background:var(--card); border:1px solid var(--line); border-radius:14px; width:min(620px,92vw); max-height:88vh; overflow:auto; padding:16px; box-shadow:0 12px 40px rgba(0,0,0,.5); }
+  .composeH { display:flex; align-items:center; font-size:14px; margin-bottom:8px; }
+  .composeH .composeX { margin-left:auto; background:transparent; border:0; color:var(--mut); font-size:16px; cursor:pointer; }
+  .composeGate { font-size:11px; color:#e3b341; background:#241d0e; border:1px solid #5a4a1e; border-radius:8px; padding:6px 9px; margin-bottom:10px; }
+  .composeCard input, .composeCard textarea { width:100%; box-sizing:border-box; background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:13px; padding:8px 10px; margin-bottom:8px; font-family:inherit; }
+  .composeCard textarea { resize:vertical; line-height:1.5; }
+  .composeActs { display:flex; gap:8px; justify-content:flex-end; }
+  .composeActs button { font-size:12px; padding:6px 14px; border-radius:10px; border:1px solid var(--line); background:var(--card); color:var(--ink); cursor:pointer; }
+  .composeActs button.primary { border-color:#2f5233; color:#8fdfa0; }
+  .composeActs button.gated { border-color:#5a4a1e; color:#e3b341; }
+  .composeActs button.gated:hover:not([disabled]) { border-color:#e3b341; }
+  .composeActs button[disabled] { opacity:.4; cursor:not-allowed; }
+  /* Tabbed agent window */
+  .composeCard.tabwin { width:min(680px,94vw); }
+  .composeH .winmeta { color:var(--mut); font-size:11px; font-weight:400; margin-left:10px; }
+  .tabbar { display:flex; gap:2px; border-bottom:1px solid var(--line); margin:4px 0 12px; flex-wrap:wrap; }
+  .tabt { background:transparent; border:0; border-bottom:2px solid transparent; color:var(--mut); font-size:12.5px; padding:7px 12px; cursor:pointer; }
+  .tabt:hover { color:var(--ink); }
+  .tabt.active { color:var(--ink); border-bottom-color:var(--blue); font-weight:600; }
+  .tabpane { display:none; }
+  .tabpane.active { display:block; }
+  .tabmut { color:var(--mut); font-size:12px; font-style:italic; padding:8px 2px; }
+  .cm-row { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:10px; }
+  .tabpane .ainrow { display:flex; flex-wrap:wrap; gap:14px; margin:8px 0; }
+  .tabpane .ain { color:var(--mut); text-decoration:none; font-size:12px; }
+  .tabpane .ain:hover { color:var(--blue); }
+  .findgrid { display:flex; flex-direction:column; gap:7px; }
+  .findlink { display:inline-block; background:var(--card); border:1px solid var(--line); border-radius:8px; color:var(--ink); text-decoration:none; font-size:12.5px; padding:8px 11px; cursor:pointer; text-align:left; }
+  .findlink:hover { border-color:var(--blue); color:var(--blue); }
+  .findlink.findloop { padding:8px 11px; }
+  .findlink.findloop a { color:inherit; text-decoration:none; }
+  .crmToast { position:fixed; bottom:22px; left:50%; transform:translateX(-50%) translateY(12px); background:var(--bg2); border:1px solid var(--line); color:var(--ink); font-size:12.5px; padding:9px 16px; border-radius:20px; opacity:0; transition:opacity .2s,transform .2s; z-index:9500; pointer-events:none; }
+  .crmToast.on { opacity:1; transform:translateX(-50%) translateY(0); }
+  /* Card-view agent-contact block (every card, every asset type) */
+  .card .cardagent { margin-top:8px; border-top:1px solid var(--line); padding-top:8px; font-size:12px; }
+  .card .cardagent .ca-top { display:flex; flex-wrap:wrap; align-items:center; gap:6px; }
+  .card .cardagent .ainrow { display:flex; flex-wrap:wrap; gap:10px; margin-top:3px; }
+  .card .cardagent .ain { color:var(--mut); text-decoration:none; font-size:11px; }
+  .card .cardagent .ain:hover { color:var(--blue); }
+  .card .cardagent .acts { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
+  /* Full contact modal info row */
+  .composeCard .cm-info { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-bottom:10px; }
+  .composeCard .cm-info .crmf { background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12px; padding:6px 8px; min-width:140px; flex:1 1 140px; margin:0; }
+  .composeCard .cm-info .crmf:focus { outline:none; border-color:var(--blue); }
+  .composeH .flink { font-size:11px; font-weight:400; margin-left:8px; }
+  /* Inline (no-outside-link) agent bits */
+  .aname { font-weight:600; color:var(--ink); }
+  a.aname { text-decoration:none; cursor:pointer; } a.aname:hover { color:var(--blue); text-decoration:underline; }
+  .flink { cursor:pointer; }
+  .listtbl td .flink { border-bottom:1px dotted #3a4453; }
+  .listtbl td .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  /* ── drill atom: every displayed data point filters to its deeper view ── */
+  .drill { cursor:pointer; border-bottom:1px dotted transparent; }
+  .drill:hover { border-bottom-color:currentColor; filter:brightness(1.15); }
+  .badges .b.drill:hover { border-bottom-color:currentColor; }
+  .city .drill { color:inherit; }
+  .city .drill:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  .copyb { background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:11px; padding:0 3px; border-radius:4px; }
+  .copyb:hover { color:var(--blue); }
+  .crm-note, .lexpbody .crm-note { margin-top:6px; font-size:12px; color:var(--mut); white-space:pre-wrap; background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:6px 9px; text-align:left; }
+  .composeCard .cm-notes { margin-bottom:10px; }
+  .composeCard .cm-notes .deeplbl { margin-bottom:4px; }
+  .composeCard .cm-notes textarea { width:100%; box-sizing:border-box; background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12.5px; padding:7px 9px; font-family:inherit; resize:vertical; line-height:1.5; }
+  .composeCard .cm-notes textarea:focus { outline:none; border-color:var(--blue); }
+  /* ---- Amazon-style docked left filter rail (desktop) ---- */
+  .shell { display:flex; align-items:flex-start; }
+  main { flex:1 1 auto; min-width:0; }
+  @media (min-width:901px){
+    #burger { display:none; }
+    #scrim { display:none !important; }
+    #drawer { position:sticky; top:57px; height:calc(100vh - 57px); transform:none; flex:0 0 var(--drawerW); box-shadow:none; }
+    #drawer .dhead .x { display:none; }
+  }
+  /* collapsible filter tabs */
+  .fsec > h4 { cursor:pointer; display:flex; align-items:center; justify-content:space-between; user-select:none; }
+  .fsec > h4::after { content:'▾'; color:var(--mut); font-size:11px; transition:transform .15s; margin-left:8px; }
+  .fsec.collapsed > h4::after { transform:rotate(-90deg); }
+  .fsec.collapsed > *:not(h4) { display:none !important; }
+  .assessline { border-top:1px dashed rgba(255,255,255,.06); padding-top:6px; margin-top:2px; }
+  /* active-filter summary bar */
+  .activebar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:10px 20px; border-bottom:1px solid var(--line); background:rgba(88,166,255,.03); }
+  .activebar:empty { display:none; }
+  .activebar .lbl { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.5px; }
+  .afchip { display:inline-flex; align-items:center; gap:6px; background:rgba(88,166,255,.10); border:1px solid rgba(88,166,255,.4); color:var(--blue); border-radius:16px; padding:4px 10px; font-size:12px; cursor:pointer; }
+  .afchip:hover { background:rgba(88,166,255,.2); } .afchip .xx { opacity:.65; font-weight:700; }
+  .afreset { background:transparent; border:1px solid #7d2b28; color:var(--bad); border-radius:14px; padding:4px 11px; font-size:12px; cursor:pointer; margin-left:auto; }
+  .afreset:hover { background:rgba(248,81,73,.1); }
+  /* shortlist stars */
+  .starbtn { background:none; border:0; color:var(--mut); font-size:18px; line-height:1; cursor:pointer; padding:0; }
+  .starbtn:hover, .starbtn.on { color:var(--warn); }
+  .copybtn { background:none; border:0; color:var(--mut); font-size:14px; line-height:1; cursor:pointer; padding:0; }
+  .copybtn:hover { color:var(--blue); }
+  .listtbl .starbtn { font-size:14px; margin-right:5px; vertical-align:middle; }
+  #shortbtn.shorton { background:rgba(210,153,34,.14); border-color:var(--warn); color:var(--warn); }
+  /* aggregate stats strip */
+  .statsbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:9px 20px; border-bottom:1px solid var(--line); }
+  .statsbar:empty { display:none; }
+  .statpill { font-size:12px; color:var(--ink); background:var(--card); border:1px solid var(--line); border-radius:8px; padding:4px 10px; }
+  .statpill .sl { color:var(--mut); } .statpill b { font-variant-numeric:tabular-nums; }
+  .heatlegend { display:none; align-items:center; gap:5px; font-size:11px; color:var(--mut); }
+  .heatlegend.on { display:inline-flex; }
+  .heatlegend .bar { width:56px; height:8px; border-radius:4px; background:linear-gradient(90deg,hsl(0,70%,45%),hsl(60,70%,45%),hsl(120,70%,45%)); }
+  .showmore-wrap { grid-column:1/-1; text-align:center; padding:20px 0 6px; }
+  .showmore { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:10px; padding:9px 20px; font-size:13px; cursor:pointer; }
+  .showmore:hover { border-color:var(--blue); }
+  .quickstart { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin:16px 0; }
+  .qchip { background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:18px; padding:7px 14px; font-size:13px; cursor:pointer; }
+  .qchip:hover { border-color:var(--blue); color:var(--blue); }
+  .savedrow { display:flex; gap:6px; align-items:center; margin-bottom:5px; }
+  .savedname { flex:1; text-align:left; background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:8px; padding:6px 9px; font-size:12px; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+  .savedname:hover { border-color:var(--blue); color:var(--blue); }
+  .saveddel { background:none; border:0; color:var(--mut); cursor:pointer; font-size:13px; }
+  .saveddel:hover { color:var(--bad); }
+  #savebtn { width:100%; background:transparent; border:1px dashed #284b7a; color:var(--blue); border-radius:8px; padding:7px; font-size:12px; cursor:pointer; margin-top:4px; }
+  .savedempty { color:var(--mut); font-size:11px; }
+  /* compare overlay */
+  #cmpov { display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:60; align-items:flex-start; justify-content:center; padding:40px 14px; overflow:auto; }
+  #cmpov.on { display:flex; }
+  #cmpmodal { width:min(1100px,97vw); background:var(--card); border:1px solid var(--line); border-radius:15px; padding:20px 22px; position:relative; }
+  .cmpx { position:absolute; top:12px; right:14px; background:none; border:0; color:var(--mut); font-size:22px; cursor:pointer; }
+  .cmptbl { width:100%; border-collapse:collapse; font-size:13px; }
+  .cmptbl th, .cmptbl td { padding:8px 11px; border-top:1px solid var(--line); text-align:right; white-space:nowrap; }
+  .cmptbl thead th { position:sticky; top:0; background:var(--card); text-align:right; vertical-align:bottom; color:var(--ink); }
+  .cmptbl .cmpk, .cmptbl thead th.cmpk { text-align:left; color:var(--mut); position:sticky; left:0; background:var(--card); }
+  .cmptbl thead th .cmpsub { color:var(--mut); font-size:11px; font-weight:400; }
+  .cmphint { color:var(--mut); padding:24px 4px; }
+  .cmptools { display:flex; gap:8px; justify-content:flex-end; margin-bottom:10px; }
+  .cmptools button { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:8px; padding:6px 12px; font-size:12px; cursor:pointer; }
+  .cmptools button:hover { border-color:var(--blue); }
+  @media print {
+    body.printing-compare * { visibility:hidden; }
+    body.printing-compare #cmpov, body.printing-compare #cmpov * { visibility:visible; }
+    body.printing-compare #cmpov { position:absolute; inset:0; background:#fff; display:block; padding:0; overflow:visible; }
+    body.printing-compare #cmpmodal { width:100%; box-shadow:none; border:0; background:#fff; color:#000; }
+    body.printing-compare #cmpmodal h3 { color:#000; }
+    body.printing-compare .cmpx, body.printing-compare .no-print { display:none !important; }
+    body.printing-compare .cmptbl th, body.printing-compare .cmptbl td { color:#000; border-color:#999; }
+    body.printing-compare .cmptbl thead th, body.printing-compare .cmptbl .cmpk { background:#fff; color:#000; }
+    body.printing-compare .cmpsub { color:#444; }
+  }
+</style>
+<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 removed (no floating pop-out) -->
+  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+<script src="/crcp-cities.js"></script><script src="/crcp-scope.js"></script></head>
+<body>
+<header>
+  <button class="burger" id="burger" aria-label="Open filters">☰ Filters <span class="fcount" id="fcount">0</span></button>
+  <div class="htitle">
+    <h1>LA County CRE — Investment Explorer</h1>
+    <div class="sub" id="sub">loading…</div>
+  </div>
+  <div class="spacer"></div>
+  <div class="topnav">
+  <a href="/crcp.html" style="color:var(--warn);text-decoration:none;font-size:13px;margin-right:12px" title="Commercial Real Estate Control Panel (live)">🛰️ CRCP</a>
+  <a href="/map.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All properties on a map, colored by type, with overhead aerial photos">🗺️ Map</a>
+  <a href="/mls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Spreadsheet-style listings + grid — all property data, sortable & searchable">📋 Listings</a>
+  <a href="/valley-pools.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="San Fernando Valley single-family pool homes under $1.2M — deal-colored map + $/sqft + price-cut signals + payment estimate">🏊 Valley Pools</a>
+  <a href="/deals.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Liquid-glass deal-intelligence charts">◆ Deals</a>
+  <a href="/deals-flow.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Closed deal flow — public-record sales + FHA + SEC REIT, source-badged">💰 Deal Flow</a>
+  <a href="/rent-rolls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Rent-roll → DSCR underwriting">📑 Rent Rolls</a>
+  <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All brokers/agents — grid + list, sortable by every field">👥 Broker grid</a>
+  <a href="/brokers.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent relationship mind-map">🕸️ Broker map</a>
+  <a href="/graphics.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / firm / warrantability charts">📊 Graphics</a>
+  <a href="/condos.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LA condos — FHA warrantability">🏢 Condos</a>
+  <a href="/fha-loans.html" style="color:var(--gold,#e9c46a);text-decoration:none;font-size:13px;margin-right:12px" title="HUD/FHA multifamily loans originally ≤3.5% — historic-low-rate flagged">◆ FHA Loans</a>
+  <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent contacts — name, company, phone, email">👤 Brokers</a>
+  <a href="/linkedin.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LinkedIn follow-list">🔗 LinkedIn</a>
+  <a href="/licensed.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Licensed directory — RE agents, firms, title & escrow + firm IG">📋 Licensed</a>
+  </div><!-- /.topnav -->
+</header>
+
+<!-- Real natural-language chat bar at the TOP — "4 units in los angeles" → drives filters + re-renders (client-side, no server) -->
+<div id="chatbar" style="padding:9px 18px 10px;border-bottom:1px solid var(--line);background:var(--bg2,var(--card));position:sticky;top:57px;z-index:20;">
+  <div style="display:flex;align-items:center;gap:9px;max-width:860px;margin:0 auto;width:100%;">
+    <span style="font-size:16px" title="Ask for properties in plain English">💬</span>
+    <input id="chatq" type="text" autocomplete="off" spellcheck="false"
+      placeholder="Ask for what you want — e.g.  “4 units in Los Angeles”  ·  “multifamily under $2M”  ·  “retail 6% cap”  ·  “all cash”  (Enter)"
+      style="flex:1;min-width:0;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:9px 13px;font-size:14px;">
+    <button id="chatgo" class="ex" title="Apply this request">Ask ▸</button>
+    <button id="chatclear" class="ex" title="Clear the chat filters">Clear</button>
+    <span id="chatmsg" style="font-size:12px;color:var(--mut);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:22%"></span>
+  </div>
+  <div id="chatex" style="display:flex;flex-wrap:wrap;gap:6px;margin:7px auto 0;max-width:860px;justify-content:center;font-size:11px;color:var(--mut);align-items:center;">
+    <span style="opacity:.7">Try:</span>
+    <button class="chatchip" data-q="4 units in Los Angeles">4 units in Los Angeles</button>
+    <button class="chatchip" data-q="unwarrantable condos">unwarrantable condos</button>
+    <button class="chatchip" data-q="multifamily under $2M">multifamily under $2M</button>
+    <button class="chatchip" data-q="retail 6% cap">retail 6% cap</button>
+    <button class="chatchip" data-q="office in Pasadena">office in Pasadena</button>
+    <button class="chatchip" data-q="non-QM condos">non-QM condos</button>
+    <button class="chatchip" data-q="built after 2000">built after 2000</button>
+    <button class="chatchip" data-q="all cash">all cash</button>
+  </div>
+</div>
+<style>.chatchip{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:20px;padding:3px 11px;font-size:11px;cursor:pointer;transition:all .12s;} .chatchip:hover{border-color:var(--blue,#3b82f6);color:var(--blue,#3b82f6);}</style>
+
+<div id="scrim"></div>
+<div class="shell">
+<aside id="drawer">
+  <div class="dhead"><b>Filters</b><button class="x" id="dclose" aria-label="Close">✕</button></div>
+
+  <div class="fsec"><h4>Financing scenario</h4>
+    <span class="seg" id="scenario">
+      <button data-s="25" class="active">25% down</button>
+      <button data-s="30">30% down</button>
+      <button data-s="cash">All cash</button>
+    </span>
+    <div class="small" style="margin-top:6px">Underwriting lens — drives cash-on-cash, DSCR, cash-needed &amp; Deal Score across the view.</div>
+  </div>
+  <div class="fsec" id="fieldsSec"><h4>Fields — show / hide · reorder</h4>
+    <div class="fieldtoggles" id="fFields"></div>
+    <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fDef">Default</button><button class="minibtn" id="idxReset" title="Reset List columns to the tight default — order, widths &amp; visibility">↺ Reset layout</button></div>
+    <div class="small" style="margin-top:6px">Drag a row by its ⠿ handle to reorder columns. ▤ = also a column in List view · drag a List header to reorder · drag its right edge to resize</div>
+  </div>
+  <div class="fsec"><h4>Saved searches</h4><div id="savedList"></div><button id="savebtn">💾 Save current view</button></div>
+  <div class="fsec"><h4>Asset type</h4><div class="chips" id="fType"></div></div>
+  <div class="fsec"><h4>Listing firm</h4><div class="chips" id="fFirm"></div></div>
+  <div class="fsec"><h4>Submarket / city</h4>
+    <input type="text" id="fCity" placeholder="search city or street… e.g. Encino, Ventura Blvd">
+    <div class="chips" id="fCityChips" style="margin-top:8px"></div>
+  </div>
+  <div class="fsec"><h4>Price</h4>
+    <div class="row2"><div class="fld"><label>Min $</label><input type="number" id="fPriceMin" placeholder="any" step="50000"></div>
+    <div class="fld"><label>Max $</label><input type="number" id="fPriceMax" placeholder="any" step="50000"></div></div>
+  </div>
+  <div class="fsec"><h4>Cap rate <span class="rangeval" id="capVal">any</span></h4>
+    <input type="range" id="fCap" min="0" max="10" step="0.25" value="0">
+    <div class="small">minimum broker cap rate (0 = any)</div>
+  </div>
+  <div class="fsec"><h4>Units / size</h4>
+    <div class="row2"><div class="fld"><label>Min units</label><input type="number" id="fUnits" placeholder="any" min="0" step="1"></div>
+    <div class="fld"><label>Max units</label><input type="number" id="fUnitsMax" placeholder="any" min="0" step="1"></div></div>
+    <div class="row2" style="margin-top:8px"><div class="fld"><label>Built after</label><input type="number" id="fYear" placeholder="any" min="1900" max="2026" step="1"></div>
+    <div class="fld"><label>Built before</label><input type="number" id="fYearMax" placeholder="any" min="1900" max="2026" step="1"></div></div>
+  </div>
+  <div class="fsec"><h4>Status &amp; budget</h4>
+    <div class="chips" id="fStatus">
+      <span class="chip" data-st="all">All</span>
+      <span class="chip" data-st="active">Active only</span>
+    </div>
+    <label class="toggle" style="margin-top:10px"><input type="checkbox" id="fAfford"> In budget ($400k down) only</label>
+    <!-- Broker/firm contactability filters (Steve 2026-08-18): narrow to listings whose resolved broker/firm contact has a phone / email — so a reachable contact exists before working the lead. -->
+    <label class="toggle" style="margin-top:10px"><input type="checkbox" id="fHasPhone"> Has broker phone</label>
+    <label class="toggle" style="margin-top:6px"><input type="checkbox" id="fHasEmail"> Has broker email</label>
+  </div>
+  <div class="fsec"><h4>Sort &amp; density</h4>
+    <select id="sort">
+      <option value="deal">Deal Score ↓</option>
+      <option value="composite">Best Investment (blended)</option>
+      <option value="coc">Cash-on-Cash ↓</option>
+      <option value="yield">Rent Yield (rent÷price) ↓</option>
+      <option value="cap">Cap Rate ↓</option>
+      <option value="dscr">DSCR ↓</option>
+      <option value="priceAsc">Price ↑</option>
+      <option value="priceDesc">Price ↓</option>
+      <option value="cashNeeded">Cash Needed ↑</option>
+      <option value="unitsDesc">Units ↓</option>
+      <option value="rentRoi">Pro-forma Rent ROI ↓</option>
+    </select>
+    <div style="margin-top:10px"><label class="small">Cards per row (grid) <span class="rangeval" id="densVal">3</span></label>
+      <input type="range" id="density" min="1" max="10" step="1" value="3"></div>
+  </div>
+  <button class="resetbtn" id="reset">Reset all filters</button>
+</aside>
+
+<main>
+  <!-- ===== Brokers & agents — front-page contacts (name / company / phone / email) ===== -->
+  <style>
+    #brokers{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:14px 16px;margin-bottom:18px;}
+    #brokers h2{margin:0 0 2px;font-size:14px;text-transform:uppercase;letter-spacing:.6px;color:var(--warn);}
+    #brokers .bksub{color:var(--mut);font-size:12px;margin-bottom:12px;}
+    #bktbl{width:100%;border-collapse:collapse;font-size:13px;}
+    #bktbl td,#bktbl th{text-align:left;padding:7px 8px;border-top:1px solid var(--line);}
+    #bktbl th{color:var(--mut);font-size:11px;text-transform:uppercase;letter-spacing:.4px;border-top:0;}
+    #bktbl tr.bkrow{cursor:pointer;} #bktbl tr.bkrow:hover td{background:rgba(88,166,255,.06);}
+    #bktbl a{color:var(--blue);text-decoration:none;} #bktbl .miss{color:#6e7681;}
+    .bkatype{display:inline-block;font-size:10px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;padding:1px 6px;border-radius:9px;margin-left:6px;}
+    .bkatype.res{background:rgba(88,166,255,.16);color:var(--blue);} .bkatype.com{background:rgba(210,153,34,.16);color:var(--warn);}
+    #bkov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:60;align-items:flex-start;justify-content:center;padding:40px 14px;overflow-y:auto;}
+    #bkov.on{display:flex;}
+    #bkmodal{width:min(720px,96vw);background:var(--card);border:1px solid var(--line);border-radius:15px;padding:20px 22px;position:relative;}
+    #bkmodal h3{margin:0 0 2px;font-size:20px;} #bkmodal .mfirm{color:var(--warn);font-size:13px;}
+    #bkmodal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
+    #bkmodal .ci{display:flex;gap:16px;flex-wrap:wrap;margin:10px 0;font-size:14px;} #bkmodal .ci a{color:var(--blue);text-decoration:none;}
+    #bkmodal .sec{margin-top:14px;} #bkmodal .sec h4{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);}
+    #bkmodal .litem{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid var(--line);font-size:13px;} #bkmodal .litem .m{color:var(--mut);}
+    .bkbtn{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:7px 12px;font-size:13px;cursor:pointer;margin-right:8px;} .bkbtn:hover{border-color:var(--blue);}
+    .gate{font-size:12px;color:var(--mut);margin-top:8px;}
+    .lead{border:1px solid var(--line);border-radius:11px;margin-bottom:8px;overflow:hidden;background:rgba(255,255,255,.012);}
+    .lead .lhead{display:flex;align-items:center;gap:12px;padding:11px 14px;cursor:pointer;}
+    .lead .lhead:hover{background:rgba(88,166,255,.06);}
+    .lead .rk{font-weight:700;color:var(--warn);width:24px;text-align:center;font-variant-numeric:tabular-nums;}
+    .lead .who{flex:1;min-width:0;} .lead .who .nm{font-weight:600;} .lead .who .fm{color:var(--mut);font-size:12px;}
+    .lead .fit{font-size:11px;font-weight:700;color:var(--acc);background:rgba(63,185,80,.13);border:1px solid rgba(63,185,80,.35);border-radius:8px;padding:2px 8px;white-space:nowrap;}
+    .lead .meta{color:var(--mut);font-size:12px;white-space:nowrap;}
+    .lead .car{color:var(--mut);transition:transform .15s;} .lead.open .car{transform:rotate(90deg);}
+    .lead .body{display:none;padding:4px 14px 12px;border-top:1px solid var(--line);}
+    .lead.open .body{display:block;}
+    .lead .acts{display:flex;gap:8px;margin:10px 0 6px;flex-wrap:wrap;}
+    .figwrap{margin:8px 0 4px;display:flex;flex-direction:column;gap:4px;}
+    .fighd{font-size:11px;color:var(--mut);letter-spacing:.02em;margin-bottom:1px;}
+    .figli{display:flex;gap:9px;align-items:center;padding:5px 7px;border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.02);text-decoration:none;color:inherit;}
+    .figli:hover{border-color:var(--blue);}
+    .figli-static{cursor:default;} .figli-static:hover{border-color:var(--line);}
+    .figt{width:38px;height:38px;flex:0 0 38px;border-radius:6px;object-fit:cover;background:var(--line);display:inline-block;}
+    .figb{display:flex;flex-direction:column;min-width:0;gap:1px;}
+    .figc{font-size:12px;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:min(70vw,560px);}
+    .figm{font-size:10.5px;color:var(--mut);font-variant-numeric:tabular-nums;}
+    .mls{width:100%;border-collapse:collapse;font-size:12.5px;}
+    .mls th,.mls td{text-align:left;padding:5px 7px;border-top:1px solid var(--line);} .mls th{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.4px;border-top:0;}
+    .mls td.n{text-align:right;font-variant-numeric:tabular-nums;} .mls tr.mrow{cursor:pointer;} .mls tr.mrow:hover td{background:rgba(210,153,34,.07);}
+    .mls .hist{color:var(--blue);font-size:11px;}
+    .phitem{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid var(--line);font-size:13px;} .phitem .m{color:var(--mut);}
+  </style>
+  <!-- "Top brokers" people panel removed from the root onload (Steve 2026-08-18: show properties, not people). Broker directory lives at /broker-grid.html. The #bkov modal below is retained — it's shared by the property-grid ✉ contact popup. -->
+  <div id="bkov"><div id="bkmodal"><button class="x" aria-label="Close" onclick="document.getElementById('bkov').classList.remove('on')">✕</button><div id="bkmbody"></div></div></div>
+  <script>
+  (function(){
+    if(!document.getElementById('bkleads')) return;   // "Top brokers" panel removed from root onload — this populate script no-ops (property-grid ✉ uses the global openContact, not this closure).
+    const bq=s=>document.querySelector(s);
+    const money=n=>n?'$'+Number(n).toLocaleString():'—';
+    const esc=s=>(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+    const atype=t=>t==='residential'?'<span class="bkatype res">Residential</span>':'<span class="bkatype com">Commercial</span>';
+    const fdate=s=>{ if(!s) return ''; try{ return new Date(s).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }catch(e){ return ''; } };
+    async function openContact(name,id){
+      const ov=bq('#bkov'), mb=bq('#bkmbody'); ov.classList.add('on'); mb.innerHTML='<div class="gate">loading '+esc(name)+'…</div>';
+      const qs=id?'id='+encodeURIComponent(id):'name='+encodeURIComponent(name);
+      let d; try{ d=await (await fetch('/api/broker?'+qs)).json(); }catch(e){ mb.innerHTML='<div class="gate">error</div>'; return; }
+      if(d.error){ mb.innerHTML='<div class="gate">'+esc(d.error)+'</div>'; return; }
+      const b=d.broker, w=b.website?(/^https?:/.test(b.website)?b.website:'https://'+b.website):null;
+      mb.innerHTML='<h3>'+esc(b.name)+atype(b.agent_type)+'</h3><div class="mfirm">'+esc(b.firm||'—')+(b.title?' · '+esc(b.title):'')+'</div>'
+        +'<div class="ci">'
+        +(b.phone?'<a href="tel:'+esc(b.phone)+'">☎ '+esc(b.phone)+'</a>':'<span class="gate">☎ —</span>')
+        +(b.email?'<a href="mailto:'+esc(b.email)+'">✉ '+esc(b.email)+'</a>':'<span class="gate">✉ —</span>')
+        +(w?'<a href="'+esc(w)+'" target="_blank" rel="noopener noreferrer">🌐 website</a>':'')+'</div>'
+        +'<div class="sec"><h4>Current listings ('+d.current.length+')</h4>'+(d.current.length?d.current.map(l=>'<div class="litem"><span>'+esc(l.address)+', '+esc(l.city)+(l.zip?' '+esc(l.zip):'')+'</span><span class="m">'+esc(l.type)+' · '+money(l.price)+(l.units?' · '+l.units+'u':'')+(l.cap_rate?' · '+l.cap_rate+'% cap':'')+(l.listed_at?' · listed '+fdate(l.listed_at):'')+'</span></div>').join(''):'<div class="gate">none in our set</div>')+'</div>'
+        +'<div class="sec"><h4>Expired / off-market ('+((d.expired&&d.expired.length)||0)+')</h4>'+((d.expired&&d.expired.length)?d.expired.map(x=>'<div class="litem"><span>'+esc(x.address)+', '+esc(x.city)+'</span><span class="m">'+money(x.list_price)+' → sold '+money(x.sold_price)+' · '+fdate(x.sold_date)+(x.mls?' · MLS '+esc(x.mls):'')+((x.url&&!_isAgg(x.url))?' · <a href="'+esc(x.url)+'" target="_blank" rel="noopener noreferrer">↗</a>':'')+'</span></div>').join(''):'<div class="gate">'+esc(d.expiredNote||'none')+'</div>')+'</div>'
+        +'<div class="sec"><h4>Sales — area sold ('+((d.comps&&d.comps.length)||0)+')</h4>'+((d.comps&&d.comps.length)?d.comps.map(c=>'<div class="litem"><span>'+esc(c.address)+', '+esc(c.city)+(c.beds?' · '+c.beds+'bd':'')+(c.baths?'/'+c.baths+'ba':'')+(c.sqft?' · '+Number(c.sqft).toLocaleString()+'sf':'')+'</span><span class="m">'+money(c.sold_price)+' · '+fdate(c.sold_date)+(c.mls?' · MLS '+esc(c.mls):'')+((c.url&&!_isAgg(c.url))?' · <a href="'+esc(c.url)+'" target="_blank" rel="noopener noreferrer">↗</a>':'')+'</span></div>').join(''):'<div class="gate">no area sales</div>')+(d.compsNote?'<div class="gate">'+esc(d.compsNote)+'</div>':'')+'</div>';
+    }
+    bq('#bkov').addEventListener('click',e=>{ if(e.target.id==='bkov') e.currentTarget.classList.remove('on'); });
+    const moneyK=n=>{ if(n==null) return '—'; n=+n; return n>=1e6?'$'+(n/1e6).toFixed(2)+'M':'$'+Math.round(n/1e3)+'k'; };
+    // 100-year property history popup — address-only, NO owner/resident names (by design).
+    async function propHistory(address,city){
+      const ov=bq('#bkov'), mb=bq('#bkmbody'); ov.classList.add('on'); mb.innerHTML='<div class="gate">loading history for '+esc(address)+'…</div>';
+      let d; try{ d=await (await fetch('/api/property-history?address='+encodeURIComponent(address)+'&city='+encodeURIComponent(city||''))).json(); }catch(e){ mb.innerHTML='<div class="gate">error</div>'; return; }
+      const a=d.assessor||d.assessorArchive;
+      let h='<h3>'+esc(address)+'</h3><div class="mfirm">'+esc(city||'')+' · property history (address only — no names)</div>';
+      if(a){ h+='<div class="sec"><h4>Assessor parcel'+(a.roll_year?' ('+esc(a.roll_year)+' roll)':'')+'</h4><div class="ci">'
+        +(a.year_built?'<span>🏗 Built '+a.year_built+'</span>':'')
+        +(a.bedrooms!=null?'<span>🛏 '+a.bedrooms+' bd</span>':'')+(a.bathrooms!=null?'<span>🛁 '+a.bathrooms+' ba</span>':'')
+        +(a.sqft_main?'<span>📐 '+Number(a.sqft_main).toLocaleString()+' sqft</span>':'')+(a.units?'<span>🏠 '+a.units+' units</span>':'')
+        +((a.use_desc2||a.use_desc1||a.use_description)?'<span>🏷 '+esc(a.use_desc2||a.use_desc1||a.use_description)+'</span>':'')
+        +((a.roll_total_value||a.total_value)?'<span>💰 assessed '+money(a.roll_total_value||a.total_value)+'</span>':'')+'</div></div>'; }
+      h+='<div class="sec"><h4>Sold history ('+d.sales.length+')</h4>'+(d.sales.length?d.sales.map(x=>'<div class="phitem"><span>'+esc(x.address)+(x.city?', '+esc(x.city):'')+'</span><span class="m">'+money(x.sold_price)+' · '+String(x.sold_date||'').slice(0,10)+'</span></div>').join(''):'<div class="gate">no sold records on file for this address</div>')+'</div>';
+      if(d.permits&&d.permits.length) h+='<div class="sec"><h4>Permits ('+d.permits.length+')</h4>'+d.permits.map(p=>'<div class="phitem"><span>'+esc(p.permit_type||p.use_description||'permit')+'</span><span class="m">'+esc(String(p.issue_date||'').slice(0,10))+(p.valuation?' · '+money(p.valuation):'')+'</span></div>').join('')+'</div>';
+      if(d.events&&d.events.length) h+='<div class="sec"><h4>Place history</h4>'+d.events.map(ev=>'<div class="phitem"><span>'+esc(ev.summary||ev.kind)+'</span><span class="m">'+esc(String(ev.valid_from||'').slice(0,10))+'</span></div>').join('')+'</div>';
+      if(!a && !d.sales.length && !(d.permits||[]).length) h+='<div class="gate">No assessor record matched this address in the LA County parcel roll (2.4M parcels), and no sold or permit history is on file. Try the street number plus street name exactly as it appears on the deed.</div>';
+      mb.innerHTML=h;
+    }
+    // Sold / area history for a lead (per-broker closings aren't in our data; show area sold comps).
+    async function brokerSold(id,btn){
+      const box=btn.closest('.body').querySelector('.soldbox'); box.innerHTML='<div class="gate">loading sold history…</div>';
+      try{ const d=await (await fetch('/api/broker?id='+id)).json(); const sold=d.comps||[];
+        box.innerHTML='<div class="gate">Per-broker closings need a gated detail-scrape; showing recent <b>area sold comps</b> in this broker’s cities ('+sold.length+'):</div>'
+          +(sold.length?sold.map(c=>'<div class="phitem"><span>'+esc(c.address)+', '+esc(c.city)+'</span><span class="m">'+money(c.sold_price)+' · '+String(c.sold_date||'').slice(0,10)+'</span></div>').join(''):'<div class="gate">none</div>');
+      }catch(e){ box.innerHTML='<div class="gate">'+esc(e.message)+'</div>'; }
+    }
+    // Firm Instagram (Business Discovery — public firm business accounts only).
+    // Loads the last-3-posts cache + firm->handle map, builds a normalized index.
+    window.FIRM_IG={}; window.FIRM_IG_IDX={};
+    const firmIgReady=Promise.all([
+      fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
+      fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}}))
+    ]).then(([cache,map])=>{ window.FIRM_IG=cache||{}; const idx={};
+      for(const [h,al] of Object.entries((map.handles)||{})) for(const a of al) idx[a]=h; window.FIRM_IG_IDX=idx; }).catch(()=>{});
+    const normFirm=s=>String(s||'').toLowerCase().replace(/[^a-z0-9]/g,'');
+    function firmHandle(firm){ const n=normFirm(firm); if(!n) return null;
+      if(window.FIRM_IG_IDX[n]) return window.FIRM_IG_IDX[n];
+      for(const [a,h] of Object.entries(window.FIRM_IG_IDX)){ if(n===a || (a.length>=5 && n.includes(a)) || (a.length>=3 && n.startsWith(a))) return h; } return null; }
+    function firmIgStrip(firm){ const h=firmHandle(firm); if(!h) return '';
+      const c=window.FIRM_IG[h]; if(!c||!c.posts||!c.posts.length) return '';
+      const items=c.posts.slice(0,3).map(p=>{ const cap=esc((p.caption||'').replace(/\s+/g,' ').slice(0,90))||'(no caption)';
+        const when=p.timestamp?String(p.timestamp).slice(0,10):''; const vid=p.media_type==='VIDEO'?'▶ ':'';
+        const thumb=p.image?'<img class="figt" loading="lazy" src="'+esc(p.image)+'" alt="">':'<span class="figt"></span>';
+        return '<a class="figli" href="'+esc(p.permalink)+'" target="_blank" rel="noopener noreferrer">'+thumb+'<span class="figb"><span class="figc">'+vid+cap+' ↗</span><span class="figm">'+(when?when+' · ':'')+'♥ '+(p.likes||0)+' · 💬 '+(p.comments||0)+'</span></span></a>'; }).join('');
+      return '<div class="figwrap"><div class="fighd">📷 @'+esc(h)+' · firm’s latest 3</div>'+items+'</div>'; }
+    // Ranked top brokers — expandable to spreadsheet-style current listings + Sold + per-address history.
+    (function loadBkLeads(_retry){ fetch('/api/leads/top?limit=40').then(r=>{ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); }).then(async d=>{
+      await firmIgReady;
+      const leads=d.leads||[];
+      if(!leads.length){ bq('#bkleads').innerHTML='<div class="gate">No brokers to show yet.</div>'; return; }
+      bq('#bkleads').innerHTML=leads.map((l,i)=>{
+        const rows=(l.listings_json||[]).map(p=>'<tr class="mrow" data-addr="'+esc(p.address)+'" data-city="'+esc(p.city||'')+'">'
+          +'<td>'+esc(p.address)+'<div style="color:var(--mut);font-size:11px">'+esc(p.city||'')+(p.zip?' '+esc(p.zip):'')+'</div>'+(p.created_at?'<div style="color:var(--mut);font-size:10px" title="captured '+esc(String(p.created_at))+'">🕓 '+new Date(p.created_at).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})+'</div>':'')+'</td>'
+          +'<td>'+esc(p.type||'—')+'</td><td class="n">'+money(p.price)+'</td><td class="n">'+(p.units||'—')+'</td>'
+          +'<td class="n">'+(p.cap_rate!=null?p.cap_rate+'%':'—')+'</td><td class="hist">📜 history</td></tr>').join('');
+        return '<div class="lead" data-id="'+l.broker_id+'" data-name="'+esc(l.name)+'">'
+          +'<div class="lhead"><span class="rk">'+(i+1)+'</span>'
+          +'<div class="who"><span class="nm">'+esc(l.name)+'</span>'+atype(l.agent_type)+'<div class="fm">'+esc(l.firm||'—')+'</div></div>'
+          +'<span class="fit">fit '+l.fit_score+'</span><span class="meta">'+l.listings+' listings · '+moneyK(l.total_value)+'</span><span class="car">▸</span></div>'
+          +'<div class="body"><div class="acts"><button class="bkbtn" data-act="contact">✉ Contact</button>'
+          +'<button class="bkbtn" data-act="sold">🔴 Sold / area history</button></div>'
+          +window.firmIgInline(l.firm)
+          +'<table class="mls"><thead><tr><th>Address</th><th>Type</th><th>Price</th><th>Units</th><th>Cap</th><th></th></tr></thead><tbody>'+rows+'</tbody></table>'
+          +'<div class="soldbox"></div></div></div>';
+      }).join('');
+      bq('#bkleads').onclick=e=>{
+        const lead=e.target.closest('.lead'); if(!lead) return;
+        const mrow=e.target.closest('.mrow'); if(mrow){ propHistory(mrow.dataset.addr, mrow.dataset.city); return; }
+        const act=e.target.closest('[data-act]'); if(act){ if(act.dataset.act==='contact') openContact(lead.dataset.name, lead.dataset.id); else brokerSold(lead.dataset.id, act); return; }
+        if(e.target.closest('.lhead')) lead.classList.toggle('open');
+      };
+    }).catch(e=>{ if(_retry>0){ setTimeout(()=>loadBkLeads(_retry-1),1500); return; } bq('#bkleads').innerHTML='<div class="gate">Broker data is temporarily unavailable — please refresh.</div>'; console.warn('leads/top load failed:',e); }); })(1);
+  })();
+  </script>
+  <div class="bar">
+    <span class="count" id="count">…</span>
+    <span class="viewseg" id="viewseg">
+      <button data-view="grid">▦ Grid</button>
+      <button data-view="list">☰ List</button>
+      <button data-view="table">▤ Table</button>
+      <button data-view="kv">🗂 KV</button>
+    </span>
+    <div class="right">
+      <input id="idxSearch" type="text" placeholder="🔎 search all fields — address, firm, agent, type… (space = AND)" style="background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 11px;font-size:12.5px;min-width:230px" title="Filters across EVERY field on each listing (nested-safe). Space = AND.">
+      <button id="shortbtn" class="ex" title="Show only your shortlisted (starred) listings">⭐ Shortlist (0)</button>
+      <button id="cmpbtn" class="ex" title="Compare your shortlisted listings side by side">⚖ Compare</button>
+      <button id="csvbtn" class="ex" title="Download the current filtered + sorted view (visible columns) as CSV">⬇ CSV</button>
+      <button id="linkbtn" class="ex" title="Copy a shareable link to this exact filtered view">🔗 Copy link</button>
+      <select id="heat" title="Color cards/rows by a metric — red (low) → green (high) over the filtered set" style="width:auto"><option value="none">Heat: off</option><option value="deal">Heat: Deal score</option><option value="cap">Heat: Cap rate</option><option value="coc">Heat: Cash-on-cash</option></select>
+      <span id="heatlegend" class="heatlegend">low <span class="bar"></span> high · <span class="hl"></span></span>
+      <label>Sort</label>
+      <select id="sortTop" style="width:auto;min-width:180px"></select>
+    </div>
+  </div>
+  <div class="activebar" id="activebar"></div>
+  <div class="statsbar" id="statsbar"></div>
+  <div class="grid" id="grid"></div>
+  <div class="foot" id="foot"></div>
+</main>
+<aside id="ctrlrail" aria-label="View controls">
+  <div class="railtitle">🧭 Controls</div>
+  <div class="rpanel" data-rp="fields"><button class="rphead" type="button">Fields <span class="caret">▸</span></button><div class="rpbody"><div id="railFields"></div></div></div>
+  <div class="rpanel" data-rp="sort"><button class="rphead" type="button">Sort <span class="caret">▸</span></button><div class="rpbody">
+    <select id="railSort"></select>
+    <label class="rlbl">Direction (any field)</label>
+    <select id="railColSort"><option value="">— sort by column instead —</option></select>
+    <select id="railColDir"><option value="1">Ascending ↑</option><option value="-1">Descending ↓</option></select>
+  </div></div>
+  <div class="rpanel" data-rp="group"><button class="rphead" type="button">Group <span class="caret">▸</span></button><div class="rpbody">
+    <label class="rlbl">Group by field</label>
+    <select id="railGroup"></select>
+  </div></div>
+  <div class="rpanel" data-rp="density"><button class="rphead" type="button">Density <span class="caret">▸</span></button><div class="rpbody">
+    <label class="rlbl">Cards per row (grid) <span class="rangeval" id="railDensVal">3</span></label>
+    <input type="range" id="railDensity" min="1" max="10" step="1" value="3" style="width:100%">
+  </div></div>
+  <div class="rpanel" data-rp="search"><button class="rphead" type="button">Search <span class="caret">▸</span></button><div class="rpbody">
+    <input id="railSearch" type="text" placeholder="🔎 search all fields…">
+  </div></div>
+</aside>
+</div><!-- /shell -->
+
+<div id="chatwrap">
+  <h2>💬 Ask the analyst &nbsp;<span class="small">— grounded in this dataset · free (Claude CLI)</span></h2>
+  <div class="hint">Ask questions <b>or tell it to drive the view</b>: “show only multifamily under $1M at 30% down sorted by cash-on-cash”, “take me to the Reseda mixed-use”.</div>
+  <div id="log"></div>
+  <div class="examples" id="examples"></div>
+  <form id="chatform" autocomplete="off">
+    <input id="q" placeholder="Ask about the deals, or tell me to filter / sort / change financing…">
+    <button id="send" type="submit">Send</button>
+  </form>
+</div>
+
+<div id="cmpov"><div id="cmpmodal"><button class="cmpx" aria-label="Close" onclick="document.getElementById('cmpov').classList.remove('on')">✕</button><h3 style="margin:0 0 10px">⚖ Compare shortlist</h3><div id="cmpbody"></div></div></div>
+<script src="finance.js"></script>
+<script src="deal-score.js"></script>
+<!-- href-to-deeper-data primitives (shared atoms). CRCP drives drilling through its
+     own native Set-based facet model (data-firmfilter / data-typefilter / data-cityfilter
+     → F.firms/F.types/F.cityQuery → render()→stateToURL()); this shared module is loaded
+     so the atom contract + URL-addressable filter state are the single source of truth. -->
+<script src="/drill.js"></script>
+<!-- click a list row's open space → expand consolidated detail + PRIVATE per-user notes (localStorage, not shared) -->
+<script src="/row-notes.js" defer></script>
+<script>
+const $ = s => document.querySelector(s);
+const $$ = s => Array.from(document.querySelectorAll(s));
+const fmt = n => n==null ? '—' : '$'+Math.round(n).toLocaleString();
+const pct = n => n==null ? '—' : n+'%';
+const cls = (v, good, ok) => v==null?'na':(v>=good?'pos':(v>=ok?'':'neg'));
+
+// ── Dated status line (HARD RULE: every property shows EITHER a current listing
+// with its date OR historical data with dates — never a blank/undated status) ──
+const MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
+function fmtMonYear(d){ return d ? MON[d.getMonth()]+' '+d.getFullYear() : null; }
+function fmtFullDate(d){ return d ? MON[d.getMonth()]+' '+d.getDate()+', '+d.getFullYear() : null; }
+// Pull the most reliable known date for a listing: the refresh/source stamp in the
+// note (ISO YYYY-MM-DD or "Mon D, YYYY" or MM/DD/YYYY), else the dataset's sourced date.
+function listingDate(p){
+  const t = p.upside_note || '';
+  let m = t.match(/(20\d\d)-(\d\d)-(\d\d)/);                                 // 2026-06-27
+  if (m) return { d:new Date(+m[1], +m[2]-1, +m[3]), exact:true };
+  m = t.match(/\b([A-Z][a-z]{2,8})\s+(\d{1,2}),?\s+(20\d\d)/);              // June 21, 2026
+  if (m){ const mi=MON.findIndex(x=>m[1].slice(0,3)===x); if(mi>=0) return { d:new Date(+m[3], mi, +m[2]), exact:true }; }
+  m = t.match(/\b(\d{1,2})\/(\d{1,2})\/(20\d\d)/);                          // 06/27/2026
+  if (m) return { d:new Date(+m[3], +m[1]-1, +m[2]), exact:true };
+  // Fallback: the whole dataset's sourcing date (meta.sourced begins YYYY-MM-DD).
+  const src = (DATA && DATA.meta && DATA.meta.sourced || '').match(/(20\d\d)-(\d\d)-(\d\d)/);
+  if (src) return { d:new Date(+src[1], +src[2]-1, +src[3]), exact:false };
+  return null;
+}
+// The guaranteed status line. Current listing → "Listed <date>". Off-market states
+// (Under Contract / Pending / Sold) → labeled with date as historical. No date at
+// all → an explicit "no recorded history" label, never a blank.
+function statusLine(p){
+  const ld = listingDate(p);
+  const dateTxt = ld ? (ld.exact ? fmtFullDate(ld.d) : fmtMonYear(ld.d)+' (dataset)') : null;
+  const title = ld ? ' title="'+(ld.exact?'Listing data as of '+ld.d.toISOString().slice(0,10):'No per-listing date; using dataset source date '+ld.d.toISOString().slice(0,10))+'"' : '';
+  let verb, klass;
+  if (/Under Contract|Pending|Escrow/i.test(p.status)) { verb='In escrow / '+p.status; klass='warn'; }
+  else if (/Sold|Closed/i.test(p.status))             { verb='Last sold'; klass='mut'; }
+  else if (/OUT OF BUDGET/i.test(p.status))           { verb='Listed (over budget)'; klass='bad'; }
+  else                                                { verb='Listed'; klass='ok'; }
+  if (!dateTxt) return `<div class="statusline none" title="No current listing date and no recorded sale history in this dataset">🕓 ${verb} · no recorded date · no sale history on file</div>`;
+  const priced = p.price ? ' '+fmt(p.price) : '';
+  return `<div class="statusline ${klass}"${title}>🕓 ${verb}${priced} · as of ${dateTxt}</div>`;
+}
+
+const SCEN = { '25':{budget:400000,downPct:25,ratePct:6.75,amortYears:30,closingPct:2.5},
+               '30':{budget:400000,downPct:30,ratePct:6.75,amortYears:30,closingPct:2.5},
+               'cash':{budget:400000,downPct:100,ratePct:0,amortYears:30,closingPct:2.5} };
+
+let DATA=null, scenario='25', history=[];
+window.DEMO = {};  // US Census ACS demographics by lowercased city (data/demographics.json)
+function demoLine(p){
+  const d = window.DEMO[(p.city||'').toLowerCase()]; if(!d) return '';
+  const bits=[];
+  if(d.renter_pct!=null) bits.push(`${d.renter_pct}% renters`);
+  if(d.median_gross_rent) bits.push(`$${d.median_gross_rent.toLocaleString()} med rent`);
+  if(d.median_income) bits.push(`$${Math.round(d.median_income/1000)}k income`);
+  if(d.population) bits.push(`${Math.round(d.population/1000)}k pop`);
+  return bits.length ? `<div class="small" title="US Census ACS 2022 — ${p.city} (ZCTA ${d.zcta})">📊 ${bits.join(' · ')}</div>` : '';
+}
+// Neighborhood-based rent estimate from Census median gross rent. std = market median × units
+// (conservative in-place proxy); proforma = +12% market upside. Honest estimate, NOT an appraisal/1007.
+function rentEstimate(p){
+  const d = window.DEMO[(p.city||'').toLowerCase()]; if(!d || !d.median_gross_rent) return null;
+  const base = d.median_gross_rent, units = p.units || 1;
+  const std = Math.round(base*units), pro = Math.round(std*1.12);
+  const annualPro = pro*12;
+  return { std, pro, base, units, zcta:d.zcta, grossYieldPro: p.price? +(100*annualPro/p.price).toFixed(2):null };
+}
+function rentLine(p){
+  const r = rentEstimate(p); if(!r) return '';
+  // Plausibility guard: LA gross yields run ~5-10%. >18% almost always means a bad units count, not a deal.
+  const fishy = r.grossYieldPro && r.grossYieldPro > 18;
+  const yld = r.grossYieldPro ? ` · ${r.grossYieldPro}% gross yld (pro)${fishy?` <span class="b warn" title="Implausibly high — likely a bad unit count on the listing; verify.">⚠ verify units</span>`:''}` : '';
+  return `<div class="small" title="Estimate from Census median gross rent ($${r.base.toLocaleString()}/unit, ${p.city} ZCTA ${r.zcta}) × ${r.units} unit${r.units>1?'s':''}. Not an appraisal — DSCR screen only.">💵 Est. rent: <b style="color:var(--ink)">$${r.std.toLocaleString()}/mo</b> std · <b style="color:var(--acc)">$${r.pro.toLocaleString()}/mo</b> proforma${yld}</div>`;
+}
+// For ranking, treat implausible (>18%) pro-forma yields as low-confidence so bad-units outliers don't top the list.
+function rentRoiScore(p){ const r=rentEstimate(p); if(!r||!r.grossYieldPro) return -1e9; return r.grossYieldPro>18 ? -1 : r.grossYieldPro; }
+
+// ===== Field show/hide + list/grid view + extra data points =====
+const esc = s => String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+// Every data field the UI can render. card=shows on the grid card · col=available as a List column · def=default-on.
+const FIELDS = [
+  {k:'score',      label:'Investment score',  card:1, col:1, def:1},
+  {k:'deal',       label:'Deal score',        card:1, col:1, def:1},
+  {k:'agent',      label:'Agent · contact',   card:1, col:1, def:1},
+  {k:'phone',      label:'Agent phone',       card:1, col:1, def:1},
+  {k:'email',      label:'Email',             card:1, col:1, def:0},
+  {k:'firmweb',    label:'Firm website',      card:1, col:1, def:0},
+  {k:'firm',       label:'Listing firm',      card:1, col:1, def:1},
+  {k:'type',       label:'Asset type',        card:1, col:1, def:1},
+  {k:'units',      label:'Units',             card:1, col:1, def:1},
+  {k:'price',      label:'Price',             card:1, col:1, def:1},
+  {k:'ppu',        label:'Price / unit',      card:1, col:1, def:1},
+  {k:'pricePerSf', label:'Price / SF',        card:1, col:1, def:0},
+  {k:'cap',        label:'Cap rate',          card:1, col:1, def:1},
+  {k:'verified',   label:'Cap-verified flag', card:1, col:0, def:1},
+  {k:'yield',      label:'Rent yield',        card:1, col:1, def:1},
+  {k:'grm',        label:'GRM (rent mult.)',  card:1, col:1, def:0},
+  {k:'yearBuilt',  label:'Year built',        card:1, col:1, def:0},
+  {k:'dom',        label:'Days on market',    card:1, col:1, def:0},
+  {k:'zip',        label:'ZIP',               card:0, col:1, def:0},
+  {k:'status',     label:'Status badge',      card:1, col:1, def:1},
+  {k:'afford',     label:'Affordability',     card:1, col:1, def:1},
+  {k:'rec',        label:'AI recommendation', card:1, col:1, def:0},
+  {k:'statusline', label:'Listing-date line', card:1, col:0, def:1},
+  {k:'brokerflag', label:'Prior-broker flag', card:1, col:0, def:1},
+  {k:'txhist',     label:'Transaction history',card:1,col:0, def:0},
+  {k:'brokerline', label:'Current broker',    card:1, col:1, def:1},
+  {k:'brokerPhone',label:'Broker phone',      card:0, col:1, def:1},
+  {k:'coc',        label:'Cash-on-cash',      card:1, col:1, def:1},
+  {k:'dscr',       label:'DSCR',              card:1, col:1, def:1},
+  {k:'noi',        label:'Est. NOI',          card:1, col:1, def:1},
+  {k:'debt',       label:'Debt service /yr',  card:1, col:1, def:1},
+  {k:'cashNeeded', label:'Cash needed',       card:1, col:1, def:1},
+  {k:'cashFlow',   label:'Cash flow /yr',     card:1, col:1, def:1},
+  {k:'thesis',     label:'AI thesis',         card:1, col:0, def:1},
+  {k:'risks',      label:'Risks',             card:1, col:0, def:0},
+  {k:'upside',     label:'Upside note',       card:1, col:0, def:1},
+  {k:'demo',       label:'Demographics',      card:1, col:0, def:1},
+  {k:'rent',       label:'Rent estimate',     card:1, col:1, def:1},
+  {k:'links',      label:'Listing links',     card:1, col:0, def:1},
+  {k:'beds',       label:'Beds (condo)',       card:1, col:1, def:0},
+  {k:'baths',      label:'Baths (condo)',      card:1, col:1, def:0},
+  {k:'sqft',       label:'Sq Ft',              card:1, col:1, def:0},
+  {k:'hoa',        label:'HOA / mo (condo)',   card:1, col:1, def:0},
+  {k:'warr',       label:'Warrantability',     card:1, col:1, def:0},
+  {k:'broker',     label:'Broker (condo)',     card:1, col:1, def:1},
+  {k:'assessed',   label:'Assessed value ✦',   card:1, col:0, def:0, assessor:1},
+  {k:'landimp',    label:'Land / Improvement ✦',card:1, col:0, def:0, assessor:1},
+  {k:'usecode',    label:'Assessor use code ✦', card:1, col:0, def:0, assessor:1},
+  {k:'priorsale',  label:'Prior sale ✦',        card:1, col:0, def:0, assessor:1},
+];
+// Steve (2026-08-20): ALWAYS show agent · contact, phone, listing firm, current broker — never hideable,
+// override any persisted localStorage off-state (a prior toggle-off used to hide them forever despite def:1).
+const ALWAYS_ON = ['agent','phone','brokerPhone','firm','brokerline','broker'];
+const VIS = (function(){ let s={}; try{ s=JSON.parse(localStorage.getItem('cre_fields')||'{}'); }catch(e){} const o={}; FIELDS.forEach(f=>o[f.k]=ALWAYS_ON.includes(f.k)?true:((f.k in s)?!!s[f.k]:!!f.def)); return o; })();
+function vis(k){ return !!VIS[k]; }
+function saveVis(){ ALWAYS_ON.forEach(k=>VIS[k]=true); try{ localStorage.setItem('cre_fields', JSON.stringify(VIS)); }catch(e){} }
+let view = localStorage.getItem('cre_view') || 'list';
+let groupBy = localStorage.getItem('cre_group') || '';   // group-by-any-field; '' = off
+let colSort = null;  // {key, dir} — click-to-sort on a List column; overrides the sort dropdown when set
+const SHORT = new Set((function(){ try{ return JSON.parse(localStorage.getItem('cre_shortlist')||'[]'); }catch(e){ return []; } })());
+function saveShort(){ try{ localStorage.setItem('cre_shortlist', JSON.stringify([...SHORT])); }catch(e){} }
+function toggleStar(id){ SHORT.has(id)?SHORT.delete(id):SHORT.add(id); saveShort(); }
+let heat = localStorage.getItem('cre_heat') || 'none';
+function heatVal(p){ const f=p.finance||{}; return heat==='deal'?(p.deal&&p.deal.score):heat==='cap'?p.cap_rate:heat==='coc'?f.coc:null; }
+function heatColor(t){ t=Math.max(0,Math.min(1,t)); return `hsl(${Math.round(t*120)},70%,45%)`; }
+function applyHeat(rows){
+  const lg=$('#heatlegend'); if(lg){ lg.classList.toggle('on',heat!=='none'); if(heat!=='none'){ lg.querySelector('.hl').textContent={deal:'Deal score',cap:'Cap rate',coc:'Cash-on-cash'}[heat]; } }
+  if(heat==='none') return;
+  const vals=rows.map(heatVal).filter(v=>v!=null&&isFinite(v)); if(!vals.length) return;
+  const min=Math.min(...vals), max=Math.max(...vals), span=(max-min)||1;
+  const gc=$('#grid').classList;
+  // Per-view heat target: Table stripes the sticky cell; List/KV stripe the row/card via a left inset;
+  // Grid tints the card left border. (Regression fix TK-10747: List + KV were previously un-heated.)
+  const mode = gc.contains('listhost') ? 'table' : gc.contains('compacthost') ? 'compact' : gc.contains('kvhost') ? 'kv' : 'grid';
+  const targetFor=id => mode==='table' ? document.querySelector('.listtbl tr[data-id="'+id+'"] td.stick')
+    : mode==='compact' ? document.querySelector('#grid .crow[data-id="'+id+'"]')
+    : mode==='kv' ? document.querySelector('#grid .kvcard[data-id="'+id+'"]')
+    : document.getElementById('card-'+id);
+  const usesBoxShadow = mode==='table'||mode==='compact'||mode==='kv';
+  rows.forEach(p=>{ const v=heatVal(p);
+    const el=targetFor(p.id); if(!el) return;
+    if(v==null||!isFinite(v)){ if(usesBoxShadow) el.style.boxShadow=''; else el.style.borderLeft=''; return; }
+    const col=heatColor((v-min)/span);
+    if(usesBoxShadow) el.style.boxShadow='inset 5px 0 0 '+col; else el.style.borderLeft='6px solid '+col;
+  });
+}
+// ===== right-hand control rail: disclosure panels (collapsed on load) + nav-agent controls =====
+function wireCtrlRail(densVal){
+  const rail=$('#ctrlrail'); if(!rail) return;
+  // Disclosure panels — collapsed by default; open only if a saved cre_rail_<name>==='1'.
+  rail.querySelectorAll('.rpanel').forEach(p=>{
+    const name=p.dataset.rp; const saved=localStorage.getItem('cre_rail_'+name);
+    if(saved==='1') p.classList.add('open');   // default (null) = collapsed
+    const head=p.querySelector('.rphead');
+    head.setAttribute('aria-expanded', p.classList.contains('open')?'true':'false');
+    head.onclick=()=>{ const open=p.classList.toggle('open'); head.setAttribute('aria-expanded',open?'true':'false'); localStorage.setItem('cre_rail_'+name, open?'1':'0'); };
+  });
+  // Sort — mirror the existing named SORTS (#sort/#sortTop).
+  const rs=$('#railSort'); if(rs){ rs.innerHTML=$('#sort').innerHTML; rs.value=$('#sort').value;
+    rs.onchange=()=>{ colSort=null; $('#sort').value=rs.value; $('#sortTop').value=rs.value; if($('#railColSort'))$('#railColSort').value=''; render(); }; }
+  // Sort-by-ANY-field asc/desc via colSort (uses fieldVal).
+  const groupable=[{k:'addr',label:'Property'}].concat(FIELDS.filter(f=>f.col||['type','firm','status','city'].includes(f.k)));
+  const rcs=$('#railColSort'); if(rcs){ rcs.innerHTML='<option value="">— sort by column instead —</option>'+groupable.map(f=>`<option value="${f.k}">${esc(f.label)}</option>`).join('');
+    if(colSort) rcs.value=colSort.key;
+    const applyColSort=()=>{ const k=rcs.value; if(!k){ colSort=null; } else { colSort={key:k, dir:+($('#railColDir').value)||1}; } render(); };
+    rcs.onchange=applyColSort; $('#railColDir').onchange=()=>{ if(rcs.value) applyColSort(); };
+    if(colSort&&$('#railColDir')) $('#railColDir').value=String(colSort.dir); }
+  // Group-by-ANY-field.
+  const rg=$('#railGroup'); if(rg){ rg.innerHTML='<option value="">— no grouping —</option>'+groupable.map(f=>`<option value="${f.k}">${esc(f.label)}</option>`).join('');
+    rg.value=groupBy; rg.onchange=()=>{ groupBy=rg.value; localStorage.setItem('cre_group',groupBy); render(); }; }
+  // Density — mirror #density.
+  const rd=$('#railDensity'); if(rd){ rd.value=densVal; $('#railDensVal').textContent=densVal;
+    rd.oninput=()=>{ document.documentElement.style.setProperty('--cols',rd.value); $('#railDensVal').textContent=rd.value; if($('#density')){$('#density').value=rd.value; $('#densVal').textContent=rd.value;} localStorage.setItem('cre_density',rd.value); }; }
+  // Search — synced to the top #idxSearch.
+  const rse=$('#railSearch'); if(rse){ const top=$('#idxSearch'); if(top) rse.value=top.value;
+    rse.oninput=()=>{ idxQuery=rse.value.trim().toLowerCase(); if(top) top.value=rse.value; render(); }; }
+}
+function buildFieldToggles(){
+  const host=$('#fFields'); if(!host) return;
+  // emit rows in COLORDER so panel order matches list column order; non-col fields appended after
+  const ordered=(function(){
+    syncColOrder();
+    const colFs=COLORDER.map(k=>FIELDS.find(f=>f.k===k)).filter(Boolean);
+    const rest=FIELDS.filter(f=>!f.col);
+    return colFs.concat(rest);
+  })();
+  host.innerHTML = ordered.map(f=>`<label class="ftog" draggable="true" data-fk="${f.k}"><span class="fdrag" title="Drag to reorder column">⠿</span><input type="checkbox" data-fk="${f.k}" ${vis(f.k)?'checked':''}><span class="flbl">${f.label}</span>${f.col?'<span class="colbadge" title="Also shows as a column in List view">▤</span>':''}</label>`).join('');
+  // Mirror into the right-rail Fields panel (checkbox-only, shares the same VIS state; no drag here).
+  const rail=$('#railFields'); if(rail){ rail.innerHTML=ordered.map(f=>`<label class="ftog" data-fk="${f.k}"><input type="checkbox" data-fk="${f.k}" ${vis(f.k)?'checked':''}><span class="flbl">${f.label}</span></label>`).join(''); }
+}
+// ---- extra computed data points ----
+function ppu(p){ return (p.price&&p.units)?Math.round(p.price/p.units):null; }
+function ppsf(p){ return (p.price&&p.sqft)?Math.round(p.price/p.sqft):null; }
+function grm(p){ const r=rentEstimate(p); if(!r||!r.std) return null; const annual=r.std*12; return annual?+(p.price/annual).toFixed(1):null; }
+function daysOnMarket(p){ const ld=listingDate(p); if(!ld||!ld.exact) return null; const d=Math.round((Date.now()-ld.d.getTime())/86400000); return d>=0?d:null; }
+function shortStatus(p){ if(/Under Contract|Pending|Escrow/i.test(p.status)) return p.status; if(/Sold|Closed/i.test(p.status)) return 'Sold'; if(/OUT OF BUDGET/i.test(p.status)) return 'Over budget'; return 'Active'; }
+// ---- list-view renderer ----
+// ── PRIMARY-TABLE column ORDER (Steve list-build rule: draggable + persisted per-browser) ──
+// COLORDER is the display order over the col-eligible FIELDS keys; new/removed keys are
+// reconciled on every read. Persisted as idxColOrder. The always-first "Property" (addr)
+// stick column is NOT part of COLORDER — it's a fixed anchor, reordering happens among the rest.
+let COLORDER=(function(){ let a=[]; try{ a=JSON.parse(localStorage.getItem('idxColOrder')||'[]'); }catch(e){} return Array.isArray(a)?a:[]; })();
+function colKeys(){ return FIELDS.filter(f=>f.col).map(f=>f.k); }               // every col-eligible field key
+function syncColOrder(){ const keys=colKeys();
+  COLORDER=COLORDER.filter(k=>keys.includes(k));                                // prune removed
+  // Insert any NEW col key next to its canonical FIELDS neighbor (its preceding col-field), NOT at the
+  // end — so newly-added fields (Phone/Email/Firm website) surface beside Agent·contact where they
+  // belong, instead of stranded at the bottom of a persisted column order (Steve 2026-08-18).
+  keys.forEach(k=>{ if(COLORDER.includes(k)) return;
+    const ci=keys.indexOf(k); let anchor=null;
+    for(let j=ci-1;j>=0;j--){ if(COLORDER.includes(keys[j])){ anchor=keys[j]; break; } }
+    if(anchor==null){ COLORDER.unshift(k); }
+    else { COLORDER.splice(COLORDER.indexOf(anchor)+1,0,k); }
+  });
+}
+function saveColOrder(){ try{ localStorage.setItem('idxColOrder',JSON.stringify(COLORDER)); }catch(e){} }
+// Ordered, col-eligible FIELDS in the user's chosen order.
+function orderedCols(){ syncColOrder(); return COLORDER.map(k=>FIELDS.find(f=>f.k===k)).filter(Boolean); }
+// The visible List columns, in order — drives the header + body + resize signature.
+function listCols(){ return orderedCols().filter(f=>vis(f.k)); }
+// Column-set/order signature guard: when it changes, hand the table back to col-resize.js so
+// it rebuilds its <colgroup> + drag handles for the current columns (Steve list-build rule 1).
+let lastColSig='';
+function reinitResize(){
+  const tbl=document.querySelector('#grid .listtbl'); if(!tbl) return;
+  const cg=tbl.querySelector('colgroup[data-cr]'); if(cg) cg.remove();
+  delete tbl.dataset.crDone; tbl.style.tableLayout=''; tbl.style.width='';
+  if(window.ColResize && window.ColResize.refresh) window.ColResize.refresh();
+}
+function colCell(k,p){ const f=p.finance||{};
+  switch(k){
+    case 'score': return p.composite??'—';
+    case 'deal': return (p.deal&&p.deal.score!=null)?p.deal.score:'—';
+    case 'agent': return agentContactCell(p);
+    case 'phone': { const a=agentInfo(p); if(!a.phone) return '—';
+      const tier=a.phoneTier==='firm'?' <span class="tierb" title="Firm branch line'+(a.phoneCity?' — '+esc(a.phoneCity)+' office':'')+'">firm</span>':'';
+      return `<span class="ain"><a href="${telHref(a.phone)}" title="Call ${esc(a.phone)}">📞 ${esc(a.phone)}</a>${tier} <button class="copyb" data-copyval="${esc(a.phone)}" title="Copy phone">⧉</button></span>`; }
+    case 'email': { const a=agentInfo(p); return a.email?`<span class="ain"><a href="mailto:${esc(a.email)}" title="Email ${esc(a.email)}">✉ ${esc(a.email)}</a> <button class="copyb" data-copyval="${esc(a.email)}" title="Copy email">⧉</button></span>`:'—'; }
+    case 'brokerPhone': { const bp=brokerPhoneOf(p); if(!bp) return '—';
+      return `<span class="ain"><a href="${telHref(bp)}" title="Call broker ${esc(bp)}">📞 ${esc(bp)}</a> <button class="copyb" data-copyval="${esc(bp)}" title="Copy broker phone">⧉</button></span>`; }
+    case 'firmweb': { const a=agentInfo(p); const w=a.firmWebsite; if(!w) return '—';
+      const disp=String(w).replace(/^https?:\/\//,'').replace(/\/$/,''); const href=/^https?:\/\//.test(w)?w:'https://'+w;
+      return `<a class="ain" href="${esc(href)}" target="_blank" rel="noopener noreferrer" title="Open the listing firm’s website">🔗 ${esc(disp)}</a>`; }
+    case 'firm': return firmCell(p);
+    case 'type': return esc(p.type||'—');
+    case 'units': return p.units||'—';
+    case 'price': return fmt(p.price);
+    case 'ppu': { const v=ppu(p); return v?'$'+v.toLocaleString():'—'; }
+    case 'pricePerSf': { const v=ppsf(p); return v?'$'+v.toLocaleString():'—'; }
+    case 'cap': return p.cap_rate!=null?p.cap_rate+'%':'—';
+    case 'yield': return f.grossYield?f.grossYield+'%':'—';
+    case 'grm': { const v=grm(p); return v!=null?v:'—'; }
+    case 'yearBuilt': return p.year_built||'—';
+    case 'dom': { const v=daysOnMarket(p); return v!=null?v+'d':'—'; }
+    case 'zip': return p.zip||'—';
+    case 'status': return esc(shortStatus(p));
+    case 'afford': return f.affordable?'✓ in budget':'✗ over';
+    case 'rec': return (p.qwen&&p.qwen.recommendation)||'—';
+    case 'brokerline': { const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()]; const nm=(bh&&bh.current_broker)?(bh.current_broker.name||bh.current_broker.firm):null; return nm?agentCell(nm,(bh.current_broker&&bh.current_broker.firm)||firmLabel(p)):'—'; }
+    case 'coc': return f.coc!=null?f.coc+'%':'—';
+    case 'dscr': return f.dscr??'—';
+    case 'noi': return fmt(f.noi);
+    case 'debt': return fmt(f.annualDebtService);
+    case 'cashNeeded': return fmt(f.cashNeeded);
+    case 'cashFlow': return fmt(f.cashFlow);
+    case 'rent': { const r=rentEstimate(p); return r?'$'+r.std.toLocaleString():'—'; }
+    case 'beds': return p.beds??'—';
+    case 'baths': return p.baths??'—';
+    case 'sqft': return p.sqft?(+p.sqft).toLocaleString():'—';
+    case 'hoa': return p.hoa?'$'+(+p.hoa).toLocaleString():'—';
+    case 'warr': return esc(p.warrantable_status||'—');
+    case 'broker': return p.broker_name?agentCell(p.broker_name,firmLabel(p)):'—';
+    default: return '—';
+  }
+}
+// arrow as a CSS CLASS (asc/desc), NOT literal text — keeps the <th> text stable so col-resize's
+// tableSig doesn't change on sort and saved column widths survive the re-render (TK-10089).
+// "Property" (addr) stays a fixed anchor; the rest are draggable to reorder + click to sort.
+// renderListShell (defined above) builds the identical thead for both renderList and renderTableGrouped.
+function renderList(rows){ return renderListShell(rows.map(renderListBodyRow).join('')); }
+// The inline panel that opens under a list row: deep property/agent searches + the agent CRM
+// (editable contact info, mark-contacted, saved-letter history, compose). Reuses the same
+// #lc-/#hist- containers the card view uses so fetchLiveComps/fetchHistory work unchanged.
+function expandBody(p){
+  const a=agentInfo(p), firm=a.firm, id=p.id;
+  // In-app actions ONLY — no external links. Live comps + Property history render inline in the
+  // panel below; "More from this firm" filters the grid in place.
+  const deep=[];
+  deep.push(`<button class="lcbtn" data-id="${id}" title="Pull fresh comps inline (~$0.03)">🔄 Live comps</button>`);
+  deep.push(`<button class="histbtn" data-id="${id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (inline, free)">📜 Property history</button>`);
+  if(firm&&firm!=='Unknown') deep.push(`<button data-firmfilter="${esc(firm).replace(/"/g,'&quot;')}" title="Show only ${esc(firm)} listings">▦ More from ${esc(firm)}</button>`);
+  // Agent summary + notes preview. Full editing (info, notes, letters, draft) lives in the inline
+  // agent window that ✎ Open launches — no external search, no new tabs.
+  const summary=`<div class="crm-summary">${a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a> `:(isPocket(a)?pocketBadge+' ':'<span class="amut">no agent on file — add via ✎ Open</span> ')}${contactBits(a)}`
+    +`${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}${a.letters.length?`<span class="achip">✎ ${a.letters.length} saved</span>`:''}</div>`
+    +(a.notes?`<div class="crm-note">🗒 ${esc(a.notes)}</div>`:'');
+  return `<div class="src">${deep.join(' ')}</div>`
+    +`<div class="crm">${summary}
+        <div class="crm-acts">
+          <button class="abtn contactbtn" data-contact="${id}">✎ Open agent window</button>
+          <button class="abtn markbtn" data-mark="${id}">${a.contacted_at?'↻ Update contacted date':'✓ Mark contacted'}</button>
+        </div>
+      </div>`
+    +`<div class="lcout" id="lc-${id}"></div><div class="histout" id="hist-${id}"></div>`;
+}
+// raw (unformatted) value for a field, for generic column sorting
+function fieldVal(p,k){ const f=p.finance||{};
+  switch(k){
+    case 'addr': return p.address; case 'score': return p.composite; case 'deal': return p.deal&&p.deal.score;
+    case 'agent': { const a=agentInfo(p); return a.name||('~'); } // contactless rows sort last (~ > letters)
+    case 'phone': { const a=agentInfo(p); return a.phone||'~'; }   // rows without a resolved phone sort last
+    case 'brokerPhone': { const bp=brokerPhoneOf(p); return bp||'~'; }
+    case 'email': { const a=agentInfo(p); return a.email||'~'; }
+    case 'firmweb': { const a=agentInfo(p); return a.firmWebsite||'~'; }
+    case 'firm': return firmLabel(p); case 'type': return p.type; case 'units': return p.units; case 'price': return p.price;
+    case 'ppu': return ppu(p); case 'pricePerSf': return ppsf(p); case 'cap': return p.cap_rate; case 'yield': return f.grossYield;
+    case 'grm': return grm(p); case 'yearBuilt': return p.year_built; case 'dom': return daysOnMarket(p); case 'zip': return p.zip;
+    case 'status': return shortStatus(p); case 'afford': return f.affordable?1:0; case 'rec': return p.qwen&&p.qwen.recommendation;
+    case 'coc': return f.coc; case 'dscr': return f.dscr; case 'noi': return f.noi; case 'debt': return f.annualDebtService;
+    case 'cashNeeded': return f.cashNeeded; case 'cashFlow': return f.cashFlow;
+    case 'rent': { const r=rentEstimate(p); return r?r.std:null; }
+    case 'beds': return p.beds; case 'baths': return p.baths; case 'sqft': return p.sqft; case 'hoa': return p.hoa;
+    case 'warr': return p.warrantable_status; case 'broker': return p.broker_name;
+    default: return null;
+  }
+}
+function sortRows(r){
+  if(colSort){ const k=colSort.key, d=colSort.dir;
+    r.sort((a,b)=>{ let x=fieldVal(a,k), y=fieldVal(b,k);
+      const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
+      if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
+      if(x<y) return -d; if(x>y) return d; return 0; });
+  } else {
+    r.sort(SORTS[$('#sort').value]||SORTS.composite);
+  }
+  // group-by: keep the within-group order, but bucket rows so each group is contiguous (stable partition).
+  if(groupBy){
+    const buckets=new Map();
+    r.forEach(p=>{ const gk=groupKeyLabel(p); if(!buckets.has(gk)) buckets.set(gk,[]); buckets.get(gk).push(p); });
+    const keys=[...buckets.keys()].sort((a,b)=>{ const an=!isNaN(+a),bn=!isNaN(+b); if(an&&bn) return +a-+b; return a.localeCompare(b); });
+    const out=[]; keys.forEach(k=>out.push(...buckets.get(k)));
+    r.length=0; r.push(...out);
+  }
+  return r;
+}
+
+// ---- Unified Deal Score (0-100) — DealScore.compute() over the signals we already have. ----
+// Gathers: pro-forma gross yield (Census rent estimate), cap rate, in-place DSCR (finance.js),
+// a PROXY DSCR when no cap is disclosed (est NOI from pro-forma rent vs the active-scenario debt
+// service), and the assumable-FHA/VA bonus. Transparent + labeled; never NaN. Cached on the row.
+function dealScore(p){
+  const r = rentEstimate(p);
+  const grossYieldPro = (r && r.grossYieldPro != null && isFinite(r.grossYieldPro)) ? r.grossYieldPro : null;
+  // Proxy DSCR only when there is NO in-place cap (else the real DSCR carries it). Estimate in-place
+  // NOI as pro-forma annual gross × 0.6 (a 40% commercial expense ratio) and compare to debt service.
+  let proxyDscr = null;
+  const f = p.finance;
+  if (p.cap_rate == null && r && r.pro && f && f.annualDebtService > 0){
+    const estNoi = r.pro * 12 * 0.6;
+    proxyDscr = +(estNoi / f.annualDebtService).toFixed(2);
+  }
+  return DealScore.compute({
+    grossYieldPro,
+    cap_rate: p.cap_rate,
+    verified: p.verified,
+    dscr: f ? f.dscr : null,
+    proxyDscr,
+    assum: p.assum,           // present only if the geocode pipeline tagged this listing (heuristic)
+    fhaApproved: p.fhaApproved
+  });
+}
+// LoopNet cross-reference for EVERY listing — free deep-links ($0, no scrape). Mirrors scripts/sources/loopnet.js.
+const LN_TYPE={'Multifamily':'multifamily','Office':'office','Retail':'retail','Industrial':'industrial','Mixed-use':'mixed-use','Mixed Use':'mixed-use','Land':'land'};
+function loopnetLink(p){
+  const sl=s=>String(s||'').toLowerCase().trim().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
+  const city=p.city?`${sl(p.city)}-ca`:'los-angeles-ca';
+  const ptype=LN_TYPE[p.type]||'';
+  // exact: site-search resolves the specific LoopNet listing for this address if it exists.
+  const exact=`https://www.google.com/search?q=${encodeURIComponent(`site:loopnet.com "${[p.address,p.city].filter(Boolean).join(' ')}"`)}`;
+  const compsQ=`site:loopnet.com ${ptype||'commercial'} for sale ${p.city||'Los Angeles'} CA`;
+  const comps=`https://www.google.com/search?q=${encodeURIComponent(compsQ)}`;
+  return `<a href="${exact}" target="_blank" rel="noopener noreferrer" title="Find this exact property on LoopNet (CoStar) — free search, opens in a new tab">LoopNet ↗</a> <a href="${comps}" target="_blank" rel="noopener noreferrer" class="small" title="Browse comparable ${p.type||'commercial'} for-sale on LoopNet in ${p.city||'LA'}" style="opacity:.7">(comps)</a>`;
+}
+// Rich filter state
+const F = { types:new Set(), firms:new Set(), cityQuery:'', priceMin:null, priceMax:null,
+            capMin:0, unitsMin:null, unitsMax:null, yearMin:null, yearMax:null, status:'all', afford:false, unwarrantable:false, nonqm:false, shortlist:false, hasPhone:false, hasEmail:false };
+
+// ── SEARCH ALL DATA (Steve list-build rule 7) ──────────────────────────────────
+// Free-text search across EVERY field on a listing — not just the visible/defined columns,
+// nested objects included (finance/qwen/deal…) — via JSON.stringify. Space = AND.
+// A per-row haystack is memoized on `p.__idxHay` and invalidated when the row identity changes.
+let idxQuery='';
+function idxHay(p){
+  if(p.__idxHay!=null) return p.__idxHay;
+  let s;
+  try{ s=JSON.stringify(p, (k,v)=> k==='__idxHay'?undefined:v); }
+  catch(e){ s=Object.keys(p).map(k=>{ const v=p[k]; return v==null?'':(typeof v==='object'?'':String(v)); }).join(' '); }
+  // fold in the CRM-saved contact (agent name/phone/email) which lives outside the row object
+  const c=window.CONTACTS&&window.CONTACTS[p.id]; if(c) s+=' '+JSON.stringify(c);
+  s=s.toLowerCase();
+  try{ Object.defineProperty(p,'__idxHay',{value:s,enumerable:false,configurable:true,writable:true}); }catch(_){ p.__idxHay=s; }
+  return s;
+}
+function idxSearchPass(p){
+  if(!idxQuery) return true;
+  const hay=idxHay(p);
+  return idxQuery.split(/\s+/).filter(Boolean).every(t=>hay.includes(t));
+}
+
+function recompute(){
+  const A = SCEN[scenario];
+  DATA.ranked.forEach(p=>{
+    try{ delete p.__idxHay; }catch(_){}   // finance/deal recomputed → refresh the search haystack
+    p.finance = CREFin.finance(p, A);
+    const fsc = CREFin.financeScore(p, p.finance);
+    p.financeScore = fsc.score; p.financeConf = fsc.conf;
+    p.composite = CREFin.composite(p, p.finance, fsc.score, p.qwenScore);
+    p.deal = dealScore(p);   // {score, parts, confidence, coverage} — cached for badge + sort + tooltip
+  });
+  DATA.ranked.sort((a,b)=>b.composite-a.composite);
+  DATA.ranked.forEach((p,i)=>p.rank=i+1);
+}
+
+function firmLabel(p){
+  if(p.firm) return p.firm;
+  const d=(p.source||'').replace(/^https?:\/\/(www\.)?/,'').split('/')[0]||'';
+  const map={'crexi.com':'Crexi','loopnet.com':'LoopNet','compass.com':'Compass','remax.com':'RE/MAX',
+    'coldwellbankerhomes.com':'Coldwell Banker','marcusmillichap.com':'Marcus & Millichap',
+    'lee-associates.com':'Lee & Associates','colliers.com':'Colliers','cbre.com':'CBRE','jll.com':'JLL'};
+  if(map[d]) return map[d];
+  return d ? d.split('.')[0].replace(/^./,c=>c.toUpperCase()) : 'Unknown';
+}
+
+// ── Agent-contact CRM (front-end) ──────────────────────────────────────────────────
+// CONTACTS[listingId] = {name,phone,email,firm,contacted_at,letters:[]}. Loaded once at
+// boot from /api/contacts; every save re-writes the record here so the grid re-renders
+// without a reload. Sending a letter is NOT done here — letters are SAVED only.
+window.CONTACTS = window.CONTACTS || {};
+fetch('/api/contacts').then(r=>r.json()).then(j=>{ window.CONTACTS=j.contacts||{}; if(window.DATA_READY) render(); }).catch(()=>{});
+
+// ── Real phone reveal (TK-10687) ── each listing carries a broker/agent NAME but no phone; the real
+// numbers live in the usre DRE registry. resolveContacts() batches the visible page's agents to the
+// /api/broker-contacts proxy, which returns a REAL city-scoped contact (see serve.js). National
+// toll-free numbers arrive flagged phone_national and are SUPPRESSED below (Steve: "not a national
+// number — closest to the listing"); the firm website link stands in so the local branch is reachable.
+window.BROKER_RESOLVED = window.BROKER_RESOLVED || {};
+const _TOLLFREE=new Set(['800','833','844','855','866','877','888','822','880','887','889']);
+// CRCP is LA-County-only, so the "closest to the listing" phone (Steve's rule) must be a CALIFORNIA
+// number. Every CA area code — a firm phone outside this set (e.g. a Compass 212 NY HQ line) is a
+// national/HQ leak and is SUPPRESSED (the firm website stands in) exactly like a toll-free number.
+const _CA_AREA=new Set(['209','213','279','310','323','341','350','408','415','424','442','510','530','559','562','619','626','628','650','657','661','669','707','714','747','760','805','818','820','831','858','909','916','925','935','949','951']);
+const _area=ph=>{ const d=String(ph||'').replace(/\D/g,'').replace(/^1(?=\d{10}$)/,''); return d.slice(0,3); };
+const isNat=ph=>{ const a=_area(ph); return _TOLLFREE.has(a) || (a.length===3 && !_CA_AREA.has(a)); };
+const _resolveDone=new Set();
+async function resolveContacts(rows){
+  const items=[];
+  for(const p of (rows||[])){ if(p&&p.id!=null&&!_resolveDone.has(p.id)){ _resolveDone.add(p.id);
+    const a=agentInfo(p); if(a.name) items.push({id:p.id,name:a.name,firm:a.firm||'',city:p.city||''}); } }
+  if(!items.length) return;
+  try{ const j=await postJSON('/api/broker-contacts',{items});
+    if(j&&j.contacts&&Object.keys(j.contacts).length){ Object.assign(window.BROKER_RESOLVED,j.contacts); paintPage(); } }catch(_){}
+}
+
+// ── Firm Instagram (global, link-free) ── each brokerage firm's last 3 IG posts,
+// rendered INLINE under the agent-contact card (no outside links — Business
+// Discovery, thumbnails cached same-origin). Loads once at boot, re-renders on ready.
+window.FIRM_IG = window.FIRM_IG || {}; window.FIRM_IG_IDX = window.FIRM_IG_IDX || {};
+Promise.all([
+  fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
+  fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}}))
+]).then(([cache,map])=>{ window.FIRM_IG=cache||{}; const idx={};
+  for(const [h,al] of Object.entries((map.handles)||{})) for(const a of al) idx[a]=h; window.FIRM_IG_IDX=idx;
+  if(window.DATA_READY) render(); }).catch(()=>{});
+window.firmHandleOf=function(firm){ const n=String(firm||'').toLowerCase().replace(/[^a-z0-9]/g,''); if(!n) return null;
+  if(window.FIRM_IG_IDX[n]) return window.FIRM_IG_IDX[n];
+  for(const [a,h] of Object.entries(window.FIRM_IG_IDX)){ if(n===a||(a.length>=5&&n.includes(a))||(a.length>=3&&n.startsWith(a))) return h; } return null; };
+window.firmIgInline=function(firm){ const h=window.firmHandleOf(firm); if(!h) return '';
+  const c=window.FIRM_IG[h]; if(!c||!c.posts||!c.posts.length) return '';
+  const e=s=>String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]));
+  const items=c.posts.slice(0,3).map(p=>{ const cap=e((p.caption||'').replace(/\s+/g,' ').slice(0,90))||'(no caption)';
+    const when=p.timestamp?String(p.timestamp).slice(0,10):''; const vid=p.media_type==='VIDEO'?'▶ ':'';
+    const thumb=p.image?'<img class="figt" loading="lazy" src="'+e(p.image)+'" alt="">':'<span class="figt"></span>';
+    return '<div class="figli figli-static">'+thumb+'<span class="figb"><span class="figc">'+vid+cap+'</span><span class="figm">'+(when?when+' · ':'')+'♥ '+(p.likes||0)+' · 💬 '+(p.comments||0)+'</span></span></div>'; }).join('');
+  return '<div class="figwrap"><div class="fighd">📷 @'+e(h)+' · firm’s latest 3</div>'+items+'</div>'; };
+// Best-known contact info for a listing: the saved CRM record wins, else what the scrape gave us.
+function agentInfo(p){
+  const c=window.CONTACTS[p.id]||{};
+  const rv=window.BROKER_RESOLVED[p.id]||{};                       // real contact resolved from usre (TK-10687)
+  const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
+  const dName=p.broker_name||p.broker_agent||(Array.isArray(p.broker_agents)&&p.broker_agents[0])||(bh&&bh.current_broker&&(bh.current_broker.name||bh.current_broker.firm))||'';
+  // Phone precedence: hand-saved CRM > scraped row > resolved usre line — but a NATIONAL/toll-free
+  // number is never shown as the phone (Steve: "not a national number"); the firm website stands in.
+  const rvPhone=(rv.phone && !rv.phone_national && !isNat(rv.phone))?rv.phone:'';   // isNat now also drops non-CA area codes (out-of-region HQ lines)
+  return { name:c.name||dName||'', phone:c.phone||p.broker_phone||rvPhone||'', email:c.email||p.broker_email||rv.email||'',
+           firm:c.firm||(rv.firm)||firmLabel(p),
+           phoneTier:rv.phone_tier||null, phoneCity:rv.phone_city||null, phoneNational:!!rv.phone_national,
+           firmWebsite:rv.firm_website||'', firmPhone:(rv.firm_phone && !isNat(rv.firm_phone))?rv.firm_phone:'', firmAddress:rv.firm_address||'',
+           contacted_at:c.contacted_at||null, notes:c.notes||'', letters:(c.letters||[]) };
+}
+// Broker (listing broker of record) phone — the enriched current-broker line, distinct from the
+// agent/contact phone in agentInfo (which folds in the scraped row's broker_phone). Steve 2026-08-25:
+// "add broker and agent phone numbers to the fields."
+function brokerPhoneOf(p){
+  const a=agentInfo(p);
+  const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
+  const chist=(bh&&bh.current_broker&&bh.current_broker.phone)||'';
+  // Prefer a DISTINCT firm/broker-of-record line (resolved usre firm_phone or broker-history), then the
+  // raw row, then — if the agent's own resolved line IS a firm/branch line — use that (it's the broker
+  // office number). Keeps "Broker phone" the firm-of-record line, distinct from the agent-direct phone.
+  return a.firmPhone || chist || p.broker_phone || (a.phoneTier==='firm' ? a.phone : '') || '';
+}
+function gsearch(q){ return 'https://www.google.com/search?q='+encodeURIComponent(q); } // retained; no longer linked out
+function safeUrl(u){ u=String(u==null?'':u); return /^https?:\/\//i.test(u)?u:'#'; }
+// "↗ View property" — a link OUT to the real listing on whichever service it's on (Crexi / LoopNet /
+// the broker's own site). Uses p.source, the actual listing URL. Steve 2026-08-18: "need to see the property."
+function sourceLink(p){ const u=p.source; if(!u||!/^https?:\/\//i.test(u)) return '';
+  const host=u.replace(/^https?:\/\/(www\.)?/,'').split('/')[0];
+  const map={'crexi.com':'Crexi','loopnet.com':'LoopNet','compass.com':'Compass','remax.com':'RE/MAX','lyonstahl.com':'Lyon Stahl','coldwellbankerhomes.com':'Coldwell Banker'};
+  const name=map[host]||host.replace(/\.(com|net|org)$/,'').replace(/^./,c=>c.toUpperCase());
+  return `<a class="srclink" href="${safeUrl(u)}" target="_blank" rel="noopener noreferrer" title="See this property on ${esc(name)}">↗ View on ${esc(name)}</a>`;
+}
+// Aggregator doctrine: never surface a clickable href to crexi/redfin/zillow/costar/loopnet/realtor/myelisting.
+// Mirror of mls.html brokerListing() — resolve canonical click-through from listing data.
+const _AGG=['crexi','redfin','zillow','costar','loopnet','realtor','myelisting'];
+const _host=u=>{try{return new URL(u).hostname.replace(/^www\./,'');}catch{return null;}};
+const _isAgg=u=>{const h=_host(u);return !!(h&&_AGG.some(a=>h.includes(a)));};
+// Refined listing-link doctrine (2026-07-31):
+// 1. Non-agg source → firm-direct listing link.
+// 2. Agg/absent source + non-agg broker_url → honest "firm site" label (NOT "the listing").
+// 3. Always append a Google ground-truth finder (never crexi/aggregator).
+// Returns array of HTML strings to push() into the find[] block.
+function listingLinkBits(p, firm){
+  const bits=[];
+  const listingQ='"'+(p.address||'')+'" '+(p.city||'')+' '+(firm&&firm!=='Unknown'?firm:'')+' commercial listing';
+  if(p.firm_direct_url&&!_isAgg(p.firm_direct_url)){   // harvested broker's-own-page deep link — highest confidence
+    bits.push(`<a class="findlink" href="${safeUrl(p.firm_direct_url)}" target="_blank" rel="noopener noreferrer">↗ View firm listing</a>`);
+  } else if(p.source&&!_isAgg(p.source)){
+    bits.push(`<a class="findlink" href="${safeUrl(p.source)}" target="_blank" rel="noopener noreferrer">↗ View firm listing</a>`);
+  } else {
+    if(p.broker_url&&!_isAgg(p.broker_url)){
+      bits.push(`<a class="findlink" href="${safeUrl(p.broker_url)}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm&&firm!=='Unknown'?firm:'Broker')} site</a>`);
+    }
+    bits.push(`<a class="findlink" href="https://www.google.com/search?q=${encodeURIComponent(listingQ)}" target="_blank" rel="noopener noreferrer">🔎 Find this listing</a>`);
+  }
+  return bits;
+}
+// A listing with NO contact of any kind (no agent name, phone, email, or firm site) is an off-market /
+// POCKET listing — the broker deliberately isn't published. Show that honestly instead of "no agent on
+// file" (which reads like missing data). Steve 2026-08-19: "show the word pocket listing if appropriate."
+function isPocket(a){ return !a.name && !a.phone && !a.email && !a.firmWebsite && !a.firm; }
+const pocketBadge = `<span class="pocketbadge" title="Off-market / pocket listing — the broker isn't publicly listed for this property">🔒 Pocket listing</span>`;
+// Firm cell — clicking filters the grid to that firm IN-APP (no external link).
+function firmCell(p){ const n=firmLabel(p); if(!n||n==='Unknown') return esc(n||'—');
+  return `<span class="flink" data-firmfilter="${esc(n).replace(/"/g,'&quot;')}" title="Show only ${esc(n)} listings">${esc(n)}</span>`; }
+// Agent name — plain in-app text (no external search).
+function agentCell(name){ return name?esc(name):'—'; }
+// Inline phone/email. The phone is a REAL tel: link (Steve 2026-08-18 "make phone icon actually call
+// out") so clicking 📞 or the number places the call; email is a mailto:. Copy buttons stay for both.
+const telHref=ph=>'tel:'+String(ph||'').replace(/[^0-9+]/g,'');
+function contactBits(a){
+  const tier=a.phone&&a.phoneTier==='firm'?`<span class="tierb" title="Firm branch line${a.phoneCity?' — '+esc(a.phoneCity)+' office':''}">firm</span>`:'';
+  const tel=a.phone?`<span class="ain"><a href="${telHref(a.phone)}" title="Call ${esc(a.phone)}">📞 ${esc(a.phone)}</a> ${tier} <button class="copyb" data-copyval="${esc(a.phone)}" title="Copy phone">⧉</button></span>`:'';
+  const eml=a.email?`<span class="ain"><a href="mailto:${esc(a.email)}" title="Email ${esc(a.email)}">✉ ${esc(a.email)}</a> <button class="copyb" data-copyval="${esc(a.email)}" title="Copy email">⧉</button></span>`:'';
+  // Firm website — "open listing firm website" (and the way to reach the LOCAL branch when we only
+  // have a national/missing number). Opens in a new tab; noopener for safety.
+  const w=a.firmWebsite?String(a.firmWebsite).replace(/^https?:\/\//,'').replace(/\/$/,''):'';
+  const web=w?`<span class="ain">🔗 <a href="${esc(a.firmWebsite.match(/^https?:\/\//)?a.firmWebsite:'https://'+a.firmWebsite)}" target="_blank" rel="noopener noreferrer" title="Open the listing firm’s website — find the local office">${esc(w)}</a></span>`:'';
+  const inner=[tel,eml,web].filter(Boolean).join(' ');
+  return inner?`<div class="ainrow">${inner}</div>`:'';
+}
+// The Agent · contact column cell: name + copyable phone/email + Open/Mark buttons + chips.
+function agentContactCell(p){
+  const a=agentInfo(p);
+  const nm=a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a>`:(isPocket(a)?pocketBadge:`<span class="amut">no agent on file</span>`);
+  const contacted=a.contacted_at?`<span class="achip done" title="Marked contacted ${esc(a.contacted_at)}">✓ ${fmtWhen(a.contacted_at)}</span>`:'';
+  const lc=a.letters.length?`<span class="achip" title="${a.letters.length} saved letter(s)">✎ ${a.letters.length}</span>`:'';
+  const nt=a.notes?`<span class="achip" title="Has notes">🗒 notes</span>`:'';
+  // Steve 2026-08-18 "phone on front page. no OPEN": the contact info (phone/email/firm website) shows
+  // DIRECTLY in the cell — no "Open" button gating it. Only a compact Contacted mark remains (click the
+  // ROW to open the full agent window if needed; the row click handler already does that).
+  const btns=`<span class="acts">`
+    +`<button class="abtn markbtn" data-mark="${p.id}" title="${a.contacted_at?'Update the contacted date':'Mark contacted (saves the date)'}">${a.contacted_at?'↻':'✓'} Contacted</button>`
+    +`</span>`;
+  return `<div class="agentcell">${nm}${contactBits(a)}<div class="chiprow">${contacted}${lc}${nt}</div>${btns}</div>`;
+}
+function fmtWhen(iso){ try{ return new Date(iso).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}); }catch(_){ return String(iso||'').slice(0,10); } }
+
+// Deal-Score badge + plain-text breakdown tooltip (explains WHY the score is what it is).
+function dealBadge(p){
+  const d = p.deal; if(!d || d.score==null) return `<span class="b" title="Not enough signal to score this deal (no cap rate, rent estimate, or DSCR available).">⬡ Deal —</span>`;
+  const col = DealScore.scoreColor(d.score);
+  const lines = d.parts.map(pt=>`• ${pt.label}: ${Math.round(pt.score)}/100 (${pt.conf}, weight ${Math.round(pt.weight)})`).join('\n');
+  const tip = `Deal Score ${d.score}/100 — weighted blend of the signals below, re-normalized over what's present (missing signals drop out, never NaN).\nConfidence: ${d.confidence}${d.coverage!=null?` (core coverage ${Math.round(d.coverage*100)}%)`:''}.\n\n${lines}\n\nA screen, not an appraisal. Verify NOI + units in DD.`;
+  return `<span class="b deal-badge" style="color:${col};border-color:${col}" title="${tip.replace(/"/g,'&quot;')}">⬡ Deal ${d.score}${d.confidence==='low'||d.confidence==='thin'?' ?':''}</span>`;
+}
+function card(p){
+  const f=p.finance, uc=/Under Contract|Pending/i.test(p.status);
+  // firm badge is now a DRILL atom → filters the grid to this firm (reuses data-firmfilter handler)
+  const _fn=firmLabel(p);
+  const firmB = (_fn && _fn!=='Unknown')
+    ? `<span class="b drill" data-firmfilter="${esc(_fn).replace(/"/g,'&quot;')}" data-filter="firm" data-v="${esc(_fn).replace(/"/g,'&quot;')}" title="Filter to ${esc(_fn)} listings">🏢 ${esc(_fn)}</span>`
+    : `<span class="b" title="Listing source / brokerage firm">🏢 ${esc(_fn||'—')}</span>`;
+  const aff = f.affordable ? `<span class="b ok">✓ $${(f.cashNeeded/1000).toFixed(0)}k in budget</span>`
+                           : `<span class="b bad">✗ needs $${(f.cashNeeded/1000).toFixed(0)}k</span>`;
+  const rec = p.qwen && p.qwen.recommendation ? `<span class="b rec-${p.qwen.recommendation}">${p.qwen.recommendation}</span>` : '';
+  const statusB = uc ? `<span class="b warn">${p.status}</span>` : (/OUT OF BUDGET/i.test(p.status)?`<span class="b bad">over budget</span>`:`<span class="b ok">Active</span>`);
+  const capB = p.cap_rate!=null
+     ? `<span class="b">${p.cap_rate}% cap${p.cap_rate_projected?` →${p.cap_rate_projected}%*`:''}</span>`
+     : `<span class="b warn">cap n/d</span>`;
+  const verB = p.verified===true ? `<span class="b ok" title="In-place NOI confirmed against MLS">✓ cap verified</span>`
+     : (p.cap_rate!=null ? `<span class="b" title="Broker-stated cap, not independently verified">broker cap</span>`
+     : `<span class="b warn" title="No in-place income disclosed">⚠ no income data</span>`);
+  // Broker-history enrichment — keyed by listing id then by address fallback
+  const bh = window.BROKER_HIST[p.id] || window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
+  const brokerFlagBadge = bh && bh.broker_flag
+    ? `<span class="brokerflag" title="${esc(bh.flag_reason||'')}">⚑ Prior broker on file</span>`
+    : '';
+  function txHistHTML(bh){
+    if(!bh || !bh.tx_history || !bh.tx_history.length) return '';
+    const rows = bh.tx_history.map(t=>{
+      const price = t.price ? `<span class="tx-price">$${Number(t.price).toLocaleString()}</span>` : '';
+      const src = t.tier==='A' ? ' <span style="color:var(--acc);font-size:9px">[A]</span>' : ' <span style="color:var(--mut);font-size:9px">[B]</span>';
+      return `<div class="tx-row"><span class="tx-date">${esc(String(t.date||'').slice(0,10))}</span><span class="tx-ev">${esc(t.event||'')}${src}</span>${price}</div>`;
+    }).join('');
+    // Also show the last deed recording from assessor if we have AIN
+    const ain = bh.ain ? `<div style="font-size:10px;color:var(--mut);margin-top:2px">AIN ${bh.ain} · built ${bh.assessor_year_built||'—'} · ${bh.assessor_units||'?'} units (assessor)</div>` : '';
+    return `<div class="txhist">${rows}${ain}</div>`;
+  }
+  function brokerLineHTML(bh){
+    if(!bh || !bh.current_broker) return '';
+    const b = bh.current_broker;
+    if(!b.name && !b.firm) return '';
+    const nameStr = b.name ? `<strong>${esc(b.name)}</strong>` : '';
+    const firmStr = b.firm ? esc(b.firm) : '';
+    const dreStr  = b.dre ? (function(){const L=String(b.dre).trim();return /^\d{8}$/.test(L)?` · DRE# <a href="https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=${encodeURIComponent(L)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record ↗">${esc(L)} ↗</a>`:` · DRE ${esc(L)}`;})() : '';
+    const phoneStr = b.phone ? ` · <a href="tel:${esc(String(b.phone).replace(/[^0-9+]/g,''))}">${esc(b.phone)}</a>` : '';
+    const mlsStr  = b.mls  ? ` · MLS ${esc(b.mls)}` : '';
+    const parts = [nameStr, firmStr].filter(Boolean).join(' · ');
+    const tierBadge = b.tier==='A' ? ' <span style="color:var(--acc);font-size:9px">[A]</span>' : '';
+    return `<div class="brokerline">🧑‍💼 ${parts}${dreStr}${phoneStr}${mlsStr}${tierBadge}</div>`;
+  }
+  return `<div class="card ${uc?'uc':''}" id="card-${p.id}">
+    <div class="top">
+      <div><div class="rank">#${p.rank} · score</div><div class="addr">${esc(p.address||'')}</div><div class="city">${p.city?`<span class="drill" data-cityfilter="${esc(p.city).replace(/"/g,'&quot;')}" data-filter="city" data-v="${esc(p.city).replace(/"/g,'&quot;')}" title="Filter to ${esc(p.city)}">${esc(p.city)}</span>`:''}, CA ${esc(p.zip||'')}</div></div>
+      <div style="display:flex;align-items:flex-start;gap:8px">
+        <button class="copybtn" data-copy="${p.id}" aria-label="Copy listing summary" title="Copy summary to clipboard">📋</button>
+        <button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist this listing">★</button>
+        ${vis('score')?`<div class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' · ai '+p.qwenScore:''}</small></div>`:''}
+      </div>
+    </div>
+    <div class="badges">${vis('deal')?dealBadge(p):''}${vis('firm')?firmB:''}${vis('type')?(p.type?`<span class="b type drill" data-typefilter="${esc(p.type).replace(/"/g,'&quot;')}" data-filter="type" data-v="${esc(p.type).replace(/"/g,'&quot;')}" title="Filter to ${esc(p.type)}">${esc(p.type)}</span>`:`<span class="b type">—</span>`):''}${vis('units')?`<span class="b">${p.units} unit${p.units>1?'s':''}</span>`:''}${vis('price')?`<span class="b">${fmt(p.price)}</span>`:''}${vis('ppu')&&ppu(p)&&!p.isCondo?`<span class="b" title="Price per unit">$${ppu(p).toLocaleString()}/unit</span>`:''}${vis('pricePerSf')&&ppsf(p)?`<span class="b" title="Price per building SF">$${ppsf(p).toLocaleString()}/sf</span>`:''}${vis('cap')?capB:''}${vis('verified')?verB:''}${vis('yield')&&f.grossYield?`<span class="b ok" title="Annual gross rent ÷ price">${f.grossYield}% rent yld</span>`:''}${vis('grm')&&grm(p)!=null?`<span class="b" title="Gross rent multiplier — price ÷ annual gross rent (lower = cheaper vs rent)">GRM ${grm(p)}</span>`:''}${vis('yearBuilt')&&p.year_built?`<span class="b" title="Year built">🏗 ${p.year_built}</span>`:''}${vis('dom')&&daysOnMarket(p)!=null?`<span class="b" title="Days since listed">${daysOnMarket(p)}d on mkt</span>`:''}${vis('beds')&&p.beds!=null?`<span class="b">${p.beds} bd</span>`:''}${vis('baths')&&p.baths!=null?`<span class="b">${p.baths} ba</span>`:''}${vis('sqft')&&p.sqft?`<span class="b">${(+p.sqft).toLocaleString()} sf</span>`:''}${vis('hoa')&&p.hoa?`<span class="b" title="HOA dues">$${(+p.hoa).toLocaleString()}/mo</span>`:''}${vis('warr')&&p.warrantable_status?`<span class="b ${p.warrantable_status==='fha_approved'?'ok':'warn'}" title="Condo warrantability (FHA-approval proxy)">${p.warrantable_status==='fha_approved'?'✓ warrantable':'⚑ '+p.warrantable_status}</span>`:''}${vis('broker')&&p.broker_name?`<span class="b" title="Listing broker">🧑‍💼 ${esc(p.broker_name)}</span>`:''}${vis('status')?statusB:''} ${vis('afford')?aff:''} ${vis('rec')?rec:''}</div>
+    ${vis('brokerflag')?brokerFlagBadge:''}
+    ${vis('statusline')?statusLine(p):''}
+    ${vis('txhist')?txHistHTML(bh):''}
+    ${vis('brokerline')?brokerLineHTML(bh):''}
+    ${(vis('coc')||vis('dscr')||vis('noi')||vis('debt')||vis('cashNeeded')||vis('cashFlow'))?`<div class="metrics">
+      ${vis('coc')?`<div><span class="k">Cash-on-cash</span><span class="v ${cls(f.coc,6,3)}">${pct(f.coc)}</span></div>`:''}
+      ${vis('dscr')?`<div><span class="k">DSCR</span><span class="v ${cls(f.dscr,1.25,1.0)}">${f.dscr??'—'}</span></div>`:''}
+      ${vis('noi')?`<div><span class="k">Est. NOI</span><span class="v">${fmt(f.noi)}</span></div>`:''}
+      ${vis('debt')?`<div><span class="k">${scenario==='cash'?'Debt svc':'Debt svc/yr'}</span><span class="v">${fmt(f.annualDebtService)}</span></div>`:''}
+      ${vis('cashNeeded')?`<div><span class="k">Cash needed</span><span class="v ${f.affordable?'pos':'neg'}">${fmt(f.cashNeeded)}</span></div>`:''}
+      ${vis('cashFlow')?`<div><span class="k">Cash flow/yr</span><span class="v ${cls(f.cashFlow,1,0)}">${fmt(f.cashFlow)}</span></div>`:''}
+    </div>`:''}
+    ${vis('thesis')&&p.qwen?.thesis?`<div class="thesis">${esc(p.qwen.thesis)}</div>`:''}
+    ${vis('risks')&&p.qwen?.risks?.length?`<div><span class="small">Risks</span><ul class="lst">${p.qwen.risks.map(r=>`<li>${esc(r)}</li>`).join('')}</ul></div>`:''}
+    ${vis('upside')&&p.upside_note?`<div class="small">💡 ${esc(p.upside_note)}</div>`:''}
+    ${vis('demo')?demoLine(p):''}
+    ${vis('rent')?rentLine(p):''}
+    ${anyAssessorVisible()?`<div class="assessline small" data-addr="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}">📋 assessor…</div>`:''}
+    ${cardAgentBlock(p)}
+  </div>`;
+}
+// Per-listing agent-contact block for the CARD view — every card (any asset type) gets the same
+// contact modal + mark-contacted as the list rows. Compact: name/phone/email + chips + buttons.
+function cardAgentBlock(p){
+  const a=agentInfo(p), id=p.id;
+  const nm=a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a>`:(isPocket(a)?pocketBadge:`<span class="amut">no agent on file</span>`);
+  const chips=`${a.contacted_at?`<span class="achip done" title="Contacted ${esc(a.contacted_at)}">✓ ${fmtWhen(a.contacted_at)}</span>`:''}${a.letters.length?`<span class="achip" title="${a.letters.length} saved letter(s)">✎ ${a.letters.length}</span>`:''}${a.notes?`<span class="achip" title="Has notes">🗒 notes</span>`:''}`;
+  return `<div class="cardagent" id="cardagent-${id}">`
+    +`<div class="ca-top">🧑‍💼 ${nm} ${chips}</div>`
+    +contactBits(a)
+    +window.firmIgInline(a.firm)
+    +`<div class="acts"><button class="abtn contactbtn" data-contact="${id}" title="Open the inline agent window — notes, letters, draft">✎ Open</button>`
+    +`<button class="abtn markbtn" data-mark="${id}" title="${a.contacted_at?'Update contacted date':'Mark contacted (saves the date)'}">${a.contacted_at?'↻':'✓'} Contacted</button></div>`
+    +`</div>`;
+}
+
+// ---- Non-QM target set: unwarrantable condos + small multifamily (1–9u) + mixed-use ----
+function isNonQM(p){
+  const t=(p.type||'').toLowerCase();
+  if(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved') return true;
+  if(/multi/.test(t) && p.units>=1 && p.units<=9) return true;
+  if(/mixed/.test(t)) return true;
+  return false;
+}
+// ---- lazy assessor enrichment (LA County parcel roll via /api/property-history, free) ----
+function anyAssessorVisible(){ return FIELDS.some(f=>f.assessor && vis(f.k)); }
+window.ASSESS = window.ASSESS || {};
+let assessObs = null;
+function assessLine(d){
+  const a=d&&(d.assessor||d.assessorArchive); const bits=[];
+  if(vis('assessed')){ const v=a&&(a.roll_total_value||a.total_value); if(v) bits.push('💰 assessed $'+(+v).toLocaleString()); }
+  if(vis('landimp')&&a){ const l=a.roll_land_value, im=a.roll_imp_value; if(l||im) bits.push('land $'+(+(l||0)).toLocaleString()+' / imp $'+(+(im||0)).toLocaleString()); }
+  if(vis('usecode')&&a){ const u=a.use_desc2||a.use_desc1||a.use_description; if(u) bits.push('🏷 '+u); }
+  if(vis('priorsale')&&d&&d.sales&&d.sales.length){ const s=d.sales[0]; bits.push('last sold $'+(+(s.sold_price||0)).toLocaleString()+' · '+String(s.sold_date||'').slice(0,10)); }
+  return bits.length ? ('📋 '+bits.join(' · ')) : '<span class="na">📋 no assessor record on file</span>';
+}
+function enrichAssessor(){
+  if(assessObs){ assessObs.disconnect(); assessObs=null; }
+  if(!anyAssessorVisible()) return;
+  const els=[...document.querySelectorAll('.assessline')];
+  const queue=[]; let active=0; const MAX=6;
+  function pump(){ while(active<MAX && queue.length){ fetchOne(queue.shift()); } }
+  function fetchOne(el){
+    const addr=el.dataset.addr, city=el.dataset.city, key=(addr+'|'+city).toLowerCase();
+    if(window.ASSESS[key]!==undefined){ el.innerHTML=assessLine(window.ASSESS[key]); pump(); return; }
+    active++;
+    fetch('/api/property-history?address='+encodeURIComponent(addr)+'&city='+encodeURIComponent(city||''))
+      .then(r=>r.json()).then(d=>{ window.ASSESS[key]=d; el.innerHTML=assessLine(d); })
+      .catch(()=>{ el.innerHTML='<span class="na">📋 assessor lookup failed</span>'; })
+      .finally(()=>{ active--; pump(); });
+  }
+  // As cards approach the viewport, queue them for enrichment (concurrency-capped).
+  assessObs=new IntersectionObserver((ents,obs)=>{ ents.forEach(en=>{ if(en.isIntersecting){ obs.unobserve(en.target); queue.push(en.target); pump(); } }); },{rootMargin:'400px'});
+  els.forEach(el=>assessObs.observe(el));
+  // Eager first batch so the top of the grid always resolves (no dependence on a scroll event).
+  els.slice(0,30).forEach(el=>{ assessObs.unobserve(el); queue.push(el); }); pump();
+}
+
+// ---- the rich filter pass ----
+function applyFilters(list){
+  return list.filter(p=>{
+    if(!idxSearchPass(p)) return false;
+    if(F.types.size && !F.types.has(p.type)) return false;
+    if(F.firms.size && !F.firms.has(firmLabel(p))) return false;
+    if(F.cityQuery){ const hay=((p.city||'')+' '+(p.address||'')).toLowerCase(); if(!hay.includes(F.cityQuery)) return false; }
+    if(F.priceMin!=null && p.price<F.priceMin) return false;
+    if(F.priceMax!=null && p.price>F.priceMax) return false;
+    if(F.capMin>0 && !(p.cap_rate!=null && p.cap_rate>=F.capMin)) return false;
+    if(F.unitsMin!=null && !(p.units>=F.unitsMin)) return false;
+    if(F.unitsMax!=null && !(p.units!=null && p.units<=F.unitsMax)) return false;
+    if(F.yearMin!=null && !(p.year_built && p.year_built>=F.yearMin)) return false;
+    if(F.yearMax!=null && !(p.year_built && p.year_built<=F.yearMax)) return false;
+    if(F.status==='active' && /Under Contract|Pending|OUT OF BUDGET/i.test(p.status)) return false;
+    if(F.afford && !p.finance.affordable) return false;
+    if(F.unwarrantable && !(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved')) return false;
+    if(F.nonqm && !isNonQM(p)) return false;
+    if(F.hasPhone||F.hasEmail){ const ci=agentInfo(p); if(F.hasPhone && !ci.phone) return false; if(F.hasEmail && !ci.email) return false; }
+    if(F.shortlist && !SHORT.has(p.id)) return false;
+    return true;
+  });
+}
+// Match everything EXCEPT one facet dimension — powers live facet counts (Amazon-style narrowing).
+function passExcept(p, skip){
+  if(!idxSearchPass(p)) return false;
+  if(skip!=='type' && F.types.size && !F.types.has(p.type)) return false;
+  if(skip!=='firm' && F.firms.size && !F.firms.has(firmLabel(p))) return false;
+  if(skip!=='city' && F.cityQuery){ const hay=((p.city||'')+' '+(p.address||'')).toLowerCase(); if(!hay.includes(F.cityQuery)) return false; }
+  if(F.priceMin!=null && p.price<F.priceMin) return false;
+  if(F.priceMax!=null && p.price>F.priceMax) return false;
+  if(F.capMin>0 && !(p.cap_rate!=null && p.cap_rate>=F.capMin)) return false;
+  if(F.unitsMin!=null && !(p.units>=F.unitsMin)) return false;
+  if(F.unitsMax!=null && !(p.units!=null && p.units<=F.unitsMax)) return false;
+  if(F.yearMin!=null && !(p.year_built && p.year_built>=F.yearMin)) return false;
+  if(F.yearMax!=null && !(p.year_built && p.year_built<=F.yearMax)) return false;
+  if(F.status==='active' && /Under Contract|Pending|OUT OF BUDGET/i.test(p.status)) return false;
+  if(F.afford && !p.finance.affordable) return false;
+  if(F.unwarrantable && !(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved')) return false;
+  if(F.nonqm && !isNonQM(p)) return false;
+  if(F.hasPhone||F.hasEmail){ const ci=agentInfo(p); if(F.hasPhone && !ci.phone) return false; if(F.hasEmail && !ci.email) return false; }
+  if(F.shortlist && !SHORT.has(p.id)) return false;
+  return true;
+}
+function updateFacetCounts(){
+  const tc={}, fc={}, cc={};
+  DATA.ranked.forEach(p=>{
+    if(passExcept(p,'type')) tc[p.type]=(tc[p.type]||0)+1;
+    if(passExcept(p,'firm')){ const fl=firmLabel(p); fc[fl]=(fc[fl]||0)+1; }
+    if(p.city && passExcept(p,'city')) cc[p.city]=(cc[p.city]||0)+1;
+  });
+  const paint=(sel,attr,map)=>$$(sel).forEach(c=>{ const key=c.dataset[attr]; if(key==null) return; const ct=c.querySelector('.ct'); const n=map[key]||0; if(ct) ct.textContent=n; c.classList.toggle('zero', n===0 && !c.classList.contains('active')); });
+  paint('#fType .chip','t',tc); paint('#fFirm .chip','fm',fc); paint('#fCityChips .chip','cy',cc);
+}
+function activeFilterCount(){
+  let n=0;
+  if(F.types.size) n++; if(F.firms.size) n++; if(F.cityQuery) n++;
+  if(F.priceMin!=null||F.priceMax!=null) n++; if(F.capMin>0) n++;
+  if(F.unitsMin!=null) n++; if(F.unitsMax!=null) n++; if(F.yearMin!=null) n++; if(F.yearMax!=null) n++;
+  if(F.status!=='all') n++; if(F.afford) n++; if(F.unwarrantable) n++; if(F.nonqm) n++; if(F.shortlist) n++; if(F.hasPhone) n++; if(F.hasEmail) n++;
+  return n;
+}
+
+const SORTS={ deal:(a,b)=>dealNum(b)-dealNum(a), composite:(a,b)=>b.composite-a.composite, coc:(a,b)=>num(b.finance.coc)-num(a.finance.coc),
+  yield:(a,b)=>num(b.finance.grossYield)-num(a.finance.grossYield),
+  cap:(a,b)=>num(b.cap_rate)-num(a.cap_rate), dscr:(a,b)=>num(b.finance.dscr)-num(a.finance.dscr),
+  priceAsc:(a,b)=>a.price-b.price, priceDesc:(a,b)=>b.price-a.price,
+  cashNeeded:(a,b)=>a.finance.cashNeeded-b.finance.cashNeeded, unitsDesc:(a,b)=>num(b.units)-num(a.units),
+  rentRoi:(a,b)=>rentRoiScore(b)-rentRoiScore(a) };
+const num=v=>v==null?-1e9:v;
+const dealNum=p=>(p.deal && p.deal.score!=null) ? p.deal.score : -1e9;  // unscored sink to the bottom
+
+// ---- active-filter summary bar (removable chips + reset) ----
+function renderActiveBar(){
+  const bar=$('#activebar'); if(!bar) return; const chips=[];
+  F.types.forEach(t=>chips.push(['type:'+t, t]));
+  F.firms.forEach(f=>chips.push(['firm:'+f, '🏢 '+f]));
+  if(F.cityQuery) chips.push(['city', 'city ~ “'+F.cityQuery+'”']);
+  if(F.priceMin!=null) chips.push(['priceMin', '≥ $'+(+F.priceMin).toLocaleString()]);
+  if(F.priceMax!=null) chips.push(['priceMax', '≤ $'+(+F.priceMax).toLocaleString()]);
+  if(F.capMin>0) chips.push(['capMin', 'cap ≥ '+F.capMin+'%']);
+  if(F.unitsMin!=null) chips.push(['unitsMin', '≥ '+F.unitsMin+' units']);
+  if(F.unitsMax!=null) chips.push(['unitsMax', '≤ '+F.unitsMax+' units']);
+  if(F.yearMin!=null) chips.push(['yearMin', 'built ≥ '+F.yearMin]);
+  if(F.yearMax!=null) chips.push(['yearMax', 'built ≤ '+F.yearMax]);
+  if(F.status!=='all') chips.push(['status', 'active only']);
+  if(F.afford) chips.push(['afford', 'in budget']);
+  if(F.unwarrantable) chips.push(['unwarrantable', '⚑ unwarrantable']);
+  if(F.nonqm) chips.push(['nonqm', '◆ non-QM']);
+  if(F.hasPhone) chips.push(['hasPhone', '☎ has phone']);
+  if(F.hasEmail) chips.push(['hasEmail', '✉ has email']);
+  if(F.shortlist) chips.push(['shortlist', '⭐ shortlist']);
+  bar.innerHTML = chips.length
+    ? '<span class="lbl">Filtering</span>'+chips.map(([k,l])=>`<span class="afchip" data-clear="${k.replace(/"/g,'&quot;')}">${esc(l)} <span class="xx">✕</span></span>`).join('')+'<button class="afreset" id="afreset">Reset all ✕</button>'
+    : '';
+}
+function clearFilter(spec){
+  if(spec.startsWith('type:')){ F.types.delete(spec.slice(5)); if(!F.types.has('Condo')) F.unwarrantable=false; }
+  else if(spec.startsWith('firm:')){ F.firms.delete(spec.slice(5)); }
+  else if(spec==='city'){ F.cityQuery=''; if($('#fCity')) $('#fCity').value=''; }
+  else if(spec==='priceMin'){ F.priceMin=null; if($('#fPriceMin')) $('#fPriceMin').value=''; }
+  else if(spec==='priceMax'){ F.priceMax=null; if($('#fPriceMax')) $('#fPriceMax').value=''; }
+  else if(spec==='capMin'){ F.capMin=0; if($('#fCap')){ $('#fCap').value=0; $('#capVal').textContent='any'; } }
+  else if(spec==='unitsMin'){ F.unitsMin=null; if($('#fUnits')) $('#fUnits').value=''; }
+  else if(spec==='unitsMax'){ F.unitsMax=null; if($('#fUnitsMax')) $('#fUnitsMax').value=''; }
+  else if(spec==='yearMin'){ F.yearMin=null; if($('#fYear')) $('#fYear').value=''; }
+  else if(spec==='yearMax'){ F.yearMax=null; if($('#fYearMax')) $('#fYearMax').value=''; }
+  else if(spec==='status'){ F.status='all'; }
+  else if(spec==='afford'){ F.afford=false; if($('#fAfford')) $('#fAfford').checked=false; }
+  else if(spec==='unwarrantable'){ F.unwarrantable=false; }
+  else if(spec==='nonqm'){ F.nonqm=false; }
+  else if(spec==='hasPhone'){ F.hasPhone=false; if($('#fHasPhone')) $('#fHasPhone').checked=false; }
+  else if(spec==='hasEmail'){ F.hasEmail=false; if($('#fHasEmail')) $('#fHasEmail').checked=false; }
+  else if(spec==='shortlist'){ F.shortlist=false; }
+  syncChips(); render();
+}
+// ---- shareable URL state (every filter/sort/view/scenario ↔ query string) ----
+function stateToURL(){
+  const p=new URLSearchParams();
+  if(F.types.size) p.set('t',[...F.types].join(','));
+  if(F.firms.size) p.set('fm',[...F.firms].join('~'));
+  if(F.cityQuery) p.set('city',F.cityQuery);
+  if(F.priceMin!=null) p.set('pmin',F.priceMin);
+  if(F.priceMax!=null) p.set('pmax',F.priceMax);
+  if(F.capMin>0) p.set('cap',F.capMin);
+  if(F.unitsMin!=null) p.set('u',F.unitsMin);
+  if(F.unitsMax!=null) p.set('ux',F.unitsMax);
+  if(F.yearMin!=null) p.set('yr',F.yearMin);
+  if(F.yearMax!=null) p.set('yrx',F.yearMax);
+  if(F.status!=='all') p.set('st',F.status);
+  if(F.afford) p.set('aff','1');
+  if(F.unwarrantable) p.set('unw','1');
+  if(F.nonqm) p.set('nq','1');
+  if(F.hasPhone) p.set('hp','1');
+  if(F.hasEmail) p.set('he','1');
+  if(scenario!=='25') p.set('sc',scenario);
+  const so=$('#sort')&&$('#sort').value; if(so && so!=='deal') p.set('sort',so);
+  if(colSort) p.set('cs',colSort.key+':'+(colSort.dir>0?'a':'d'));
+  if(heat!=='none') p.set('ht',heat);
+  if(view!=='grid') p.set('v',view);
+  const qs=p.toString();
+  // NOTE: the app declares a local `history` (chat log) that shadows window.history — must qualify.
+  try{ window.history.replaceState(null,'',location.pathname+(qs?('?'+qs):'')); }catch(e){}
+}
+// Snapshot the ORIGINAL query at first script-eval — an early render() can call stateToURL()
+// and rewrite location.search (it drops ?v= whenever the current view isn't grid) BEFORE
+// urlToState() runs, clobbering a shared ?v=/?t= link. Read shared state from this frozen copy.
+const INITIAL_SEARCH = location.search;
+function urlToState(){
+  const p=new URLSearchParams(INITIAL_SEARCH); let any=false;
+  if(p.get('t')){ F.types=new Set(p.get('t').split(',').filter(Boolean)); any=true; }
+  if(p.get('fm')){ F.firms=new Set(p.get('fm').split('~').filter(Boolean)); any=true; }
+  if(p.get('city')){ F.cityQuery=p.get('city'); if($('#fCity'))$('#fCity').value=F.cityQuery; any=true; }
+  if(p.get('pmin')){ F.priceMin=+p.get('pmin'); if($('#fPriceMin'))$('#fPriceMin').value=F.priceMin; any=true; }
+  if(p.get('pmax')){ F.priceMax=+p.get('pmax'); if($('#fPriceMax'))$('#fPriceMax').value=F.priceMax; any=true; }
+  if(p.get('cap')){ F.capMin=+p.get('cap'); if($('#fCap')){$('#fCap').value=F.capMin; $('#capVal').textContent=F.capMin+'%+';} any=true; }
+  if(p.get('u')){ F.unitsMin=+p.get('u'); if($('#fUnits'))$('#fUnits').value=F.unitsMin; any=true; }
+  if(p.get('ux')){ F.unitsMax=+p.get('ux'); if($('#fUnitsMax'))$('#fUnitsMax').value=F.unitsMax; any=true; }
+  if(p.get('yr')){ F.yearMin=+p.get('yr'); if($('#fYear'))$('#fYear').value=F.yearMin; any=true; }
+  if(p.get('yrx')){ F.yearMax=+p.get('yrx'); if($('#fYearMax'))$('#fYearMax').value=F.yearMax; any=true; }
+  if(p.get('st')){ F.status=p.get('st'); any=true; }
+  if(p.get('aff')==='1'){ F.afford=true; if($('#fAfford'))$('#fAfford').checked=true; any=true; }
+  if(p.get('unw')==='1'){ F.unwarrantable=true; any=true; }
+  if(p.get('nq')==='1'){ F.nonqm=true; any=true; }
+  if(p.get('hp')==='1'){ F.hasPhone=true; if($('#fHasPhone'))$('#fHasPhone').checked=true; any=true; }
+  if(p.get('he')==='1'){ F.hasEmail=true; if($('#fHasEmail'))$('#fHasEmail').checked=true; any=true; }
+  if(p.get('sc')){ scenario=p.get('sc'); }
+  if(p.get('sort')&&$('#sort')){ $('#sort').value=p.get('sort'); if($('#sortTop'))$('#sortTop').value=p.get('sort'); }
+  if(p.get('v')){ view=p.get('v'); }
+  if(p.get('cs')){ const [k,d]=p.get('cs').split(':'); colSort={key:k,dir:d==='a'?1:-1}; }
+  if(p.get('ht')){ heat=p.get('ht'); }
+  return any;
+}
+function csvEsc(v){ v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"'; }
+function exportCSV(){
+  const cols=listCols();
+  const rows=sortRows(applyFilters(DATA.ranked.slice()));
+  const head=['Address','City','ZIP'].concat(cols.map(c=>c.label));
+  const lines=[head.map(csvEsc).join(',')];
+  rows.forEach(p=>{ const row=[p.address,p.city,p.zip].concat(cols.map(c=>fieldVal(p,c.k))); lines.push(row.map(csvEsc).join(',')); });
+  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([lines.join('\n')],{type:'text/csv'})); a.download='cre-explorer-'+rows.length+'.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000);
+}
+function copyShareLink(){ const t=$('#linkbtn'); try{ navigator.clipboard.writeText(location.href); if(t){ const o=t.textContent; t.textContent='✓ Copied'; setTimeout(()=>t.textContent=o,1400); } }catch(e){ if(t) t.textContent='⌘C to copy'; } }
+// ---- side-by-side compare of shortlisted listings ----
+const CMP=[
+  {l:'Type', f:p=>esc(p.type||'—')},
+  {l:'Price', f:p=>fmt(p.price)},
+  {l:'$/unit', f:p=>{const v=ppu(p);return v?'$'+v.toLocaleString():'—';}},
+  {l:'$/SF', f:p=>{const v=ppsf(p);return v?'$'+v.toLocaleString():'—';}},
+  {l:'Units', f:p=>p.units??'—'},
+  {l:'Cap rate', f:p=>p.cap_rate!=null?p.cap_rate+'%':'—'},
+  {l:'DSCR', f:p=>p.finance&&p.finance.dscr!=null?p.finance.dscr:'—'},
+  {l:'Cash-on-cash', f:p=>p.finance&&p.finance.coc!=null?p.finance.coc+'%':'—'},
+  {l:'Cash needed', f:p=>p.finance?fmt(p.finance.cashNeeded):'—'},
+  {l:'Cash flow/yr', f:p=>p.finance?fmt(p.finance.cashFlow):'—'},
+  {l:'Est. rent', f:p=>{const e=rentEstimate(p);return e?'$'+e.std.toLocaleString()+'/mo':'—';}},
+  {l:'Beds / Baths', f:p=>(p.beds!=null||p.baths!=null)?((p.beds??'—')+' / '+(p.baths??'—')):'—'},
+  {l:'SqFt', f:p=>p.sqft?(+p.sqft).toLocaleString():'—'},
+  {l:'Year built', f:p=>p.year_built||'—'},
+  {l:'Warrantability', f:p=>esc(p.warrantable_status||'—')},
+  {l:'Deal score', f:p=>p.deal&&p.deal.score!=null?p.deal.score:'—'},
+  {l:'Invest score', f:p=>p.composite??'—'},
+];
+function openCompare(){
+  const ov=$('#cmpov'), body=$('#cmpbody');
+  const items=DATA.ranked.filter(p=>SHORT.has(p.id));
+  if(items.length<2){ body.innerHTML='<div class="cmphint">Star at least 2 listings (the ★ on any card) to compare them side by side.</div>'; ov.classList.add('on'); return; }
+  const head='<tr><th class="cmpk" scope="col"></th>'+items.map(p=>`<th scope="col">${esc(p.address)}<div class="cmpsub">${esc(p.city)}</div></th>`).join('')+'</tr>';
+  const rows=CMP.map(m=>`<tr><td class="cmpk">${m.l}</td>`+items.map(p=>`<td>${m.f(p)}</td>`).join('')+'</tr>').join('');
+  body.innerHTML=`<div class="cmptools no-print"><button id="cmpcsv">⬇ CSV</button><button id="cmpprint">🖨 Print</button></div><table class="cmptbl"><thead>${head}</thead><tbody>${rows}</tbody></table>`;
+  ov.classList.add('on');
+}
+function decodeEnt(s){ return String(s).replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&#39;/g,"'"); }
+function exportCompareCSV(){
+  const items=DATA.ranked.filter(p=>SHORT.has(p.id)); if(items.length<2) return;
+  const cols=['Address','City'].concat(CMP.map(m=>m.l));
+  const lines=[cols.map(csvEsc).join(',')];
+  items.forEach(p=>{ const row=[p.address,p.city].concat(CMP.map(m=>decodeEnt(m.f(p)))); lines.push(row.map(csvEsc).join(',')); });
+  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([lines.join('\n')],{type:'text/csv'})); a.download='cre-compare-'+items.length+'.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000);
+}
+function printCompare(){ document.body.classList.add('printing-compare'); window.print(); }
+// ---- paginated DOM (perf): render in pages, "Show more" appends ----
+let _rows=[], _shown=0;
+const PAGE=()=> view==='grid'?60:200;
+function moreBtn(){ return _shown<_rows.length ? `<div class="showmore-wrap"><button class="showmore" id="showmore">Show ${Math.min(PAGE(),_rows.length-_shown)} more · showing ${_shown} of ${_rows.length}</button></div>` : ''; }
+// Infinite scroll: auto-trigger the "show more" grow when it scrolls near view (reuses the
+// existing #showmore click handler → _shown+=PAGE → paintPage). Viewport-rooted so it works
+// whether the page or an inner pane scrolls. Re-pointed at the fresh button after each repaint.
+let _moreIO=null, _moreBusy=false;
+function wireAutoMore(){
+  const btn=document.getElementById('showmore'); if(!btn) return;
+  if(typeof IntersectionObserver==='undefined') return;
+  if(!_moreIO) _moreIO=new IntersectionObserver(es=>{ if(_moreBusy) return; if(es.some(e=>e.isIntersecting)){ const b=document.getElementById('showmore'); if(b){ _moreBusy=true; b.click(); setTimeout(()=>{_moreBusy=false;},60); } } },{rootMargin:'500px 0px'});
+  _moreIO.disconnect(); _moreIO.observe(btn);
+}
+function paintPage(){
+  const g=$('#grid'); const r=_rows; const slice=r.slice(0,_shown);
+  if(!r.length){ g.className='grid'; g.innerHTML=`<div class="empty">No properties match these filters.<br><span class="small">Loosen a filter or hit “Reset all”.</span></div>`; return; }
+  if(view==='table'){                                    // full columnar spreadsheet (sortable + drag-reorder + resizable cols)
+    g.className='listhost'; g.innerHTML=renderTableGrouped(slice)+moreBtn();
+    const csig=listCols().map(c=>c.k).join(',');
+    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
+  }
+  else if(view==='kv'){                                  // key→value block per record (deep inspection)
+    g.className='kvhost'; g.innerHTML=renderKVGrouped(slice)+moreBtn();
+  }
+  else if(view==='list'){                                // compact one-row-per-record: identity + inline summary chips
+    g.className='compacthost'; g.innerHTML=renderCompactGrouped(slice)+moreBtn();
+  }
+  else { g.className='grid'; g.innerHTML=groupWrap(slice,card,'grid')+moreBtn(); enrichAssessor(); }
+  applyHeat(slice);
+  resolveContacts(slice);   // fetch REAL phones for the visible agents, then repaint (TK-10687)
+  wireAutoMore();           // convert the show-more button into infinite scroll
+}
+// ===== nav-agent inline view renderers: List (compact) / KV / Table + group-by-any-field =====
+// The rail sort already ordered rows; sortRows() also front-sorts by the group key when groupBy is set,
+// so groups arrive contiguous here — we just detect a key change and emit a header band.
+function groupKeyLabel(p){ if(!groupBy) return ''; let v=fieldVal(p,groupBy); if(v==null||v==='') return '(none)'; return String(v); }
+// Generic grouping wrapper for non-table views: emits a heading band before each group.
+function groupWrap(rows, renderOne, kind){
+  if(!groupBy){ return rows.map(renderOne).join(''); }
+  let out='', last=null;
+  rows.forEach(p=>{ const gk=groupKeyLabel(p); if(gk!==last){ last=gk;
+    out+=`<div class="grouphead grouphead-${kind}">${esc(fieldLabel(groupBy))}: <b>${esc(gk)}</b></div>`; }
+    out+=renderOne(p); });
+  return out;
+}
+function fieldLabel(k){ if(k==='addr') return 'Property'; const f=FIELDS.find(x=>x.k===k); return f?f.label:k; }
+// ---- compact List: address identity + inline summary chips from the visible columns ----
+function compactChips(p){
+  const cols=listCols().slice(0,7);   // a handful of the visible fields as inline chips (keeps the row light)
+  return cols.map(c=>`<span class="cchip"><span class="cck">${esc(c.label)}</span> ${colCell(c.k,p)}</span>`).join('');
+}
+function renderCompactRow(p){
+  return `<div class="crow" data-id="${p.id}">`
+    +`<button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button>`
+    +`<div class="cid"><span class="cla">${esc(p.address)}</span><span class="clc">${esc(p.city)}, CA ${p.zip||''}</span></div>`
+    +`<div class="cfields">${compactChips(p)}</div></div>`;
+}
+function renderCompactGrouped(rows){ return `<div class="compactlist">${groupWrap(rows,renderCompactRow,'compact')}</div>`; }
+// ---- KV: each record a labeled key→value block, every visible field vertical ----
+function renderKVCard(p){
+  const cols=listCols();
+  const rowsHtml=[`<div class="kvrow"><span class="kvk">Property</span><span class="kvv">${esc(p.address)}, ${esc(p.city)} CA ${p.zip||''}</span></div>`]
+    .concat(cols.map(c=>`<div class="kvrow"><span class="kvk">${esc(c.label)}</span><span class="kvv">${colCell(c.k,p)}</span></div>`)).join('');
+  return `<div class="kvcard" data-id="${p.id}">`
+    +`<div class="kvhead"><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="Shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button><span class="kvaddr">${esc(p.address)}</span></div>`
+    +rowsHtml+`</div>`;
+}
+function renderKVGrouped(rows){ return `<div class="kvgrid">${groupWrap(rows,renderKVCard,'kv')}</div>`; }
+// ---- Table: reuse the full renderList() spreadsheet, injecting group header rows when grouped ----
+function renderTableGrouped(rows){
+  if(!groupBy) return renderList(rows);
+  // Build the grouped body DIRECTLY (no regex-splice of renderList's output) so col-resize sees the
+  // same thead/table shell and we never rebuild a body just to discard it.
+  const ncols=listCols().length+1;
+  let bodyRows='', last=null;
+  rows.forEach(p=>{ const gk=groupKeyLabel(p);
+    if(gk!==last){ last=gk; bodyRows+=`<tr class="grouprow"><td class="groupcell" colspan="${ncols}">${esc(fieldLabel(groupBy))}: <b>${esc(gk)}</b></td></tr>`; }
+    bodyRows+=renderListBodyRow(p); });
+  return renderListShell(bodyRows);
+}
+// The table shell (listwrap + thead), body injected. Shared by renderList() and the grouped table so
+// the <thead>/sortable/drag headers are identical in both paths (col-resize signature stays stable).
+function renderListShell(bodyHtml){
+  const cols=listCols();
+  const arrow=k=>colSort&&colSort.key===k?(colSort.dir>0?' asc':' desc'):'';
+  const head='<th scope="col" class="stick sortable'+arrow('addr')+'" data-sortk="addr" title="Sort by address">Property</th>'+cols.map(c=>`<th scope="col" class="sortable dragcol${arrow(c.k)}" draggable="true" data-sortk="${c.k}" data-k="${c.k}" title="Drag to reorder · click to sort by ${esc(c.label)}">${esc(c.label)}</th>`).join('');
+  return `<div class="listwrap"><table class="listtbl" data-no-sort><thead><tr>${head}</tr></thead><tbody>${bodyHtml}</tbody></table></div>`;
+}
+// One <tr> body row, factored out of renderList so the grouped table can interleave group headers.
+function renderListBodyRow(p){
+  const cols=listCols();
+  return `<tr class="lrow" data-id="${p.id}">`
+    +`<td class="stick"><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button> <span class="la">${esc(p.address)}</span><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div>${sourceLink(p)?`<div class="lsrc">${sourceLink(p)}</div>`:''}</td>`
+    +cols.map(c=>`<td>${colCell(c.k,p)}</td>`).join('')+`</tr>`;
+}
+// ---- saved searches (name + restore the full shareable-URL state) ----
+const SAVED=(function(){ try{ return JSON.parse(localStorage.getItem('cre_saved')||'[]'); }catch(e){ return []; } })();
+function saveSaved(){ try{ localStorage.setItem('cre_saved', JSON.stringify(SAVED)); }catch(e){} }
+function buildSavedList(){ const el=$('#savedList'); if(!el) return;
+  el.innerHTML = SAVED.length
+    ? SAVED.map((s,i)=>`<div class="savedrow"><button class="savedname" data-qs="${(s.qs||'').replace(/"/g,'&quot;')}" title="Restore this saved view">📁 ${esc(s.name)}</button><button class="saveddel" data-del="${i}" aria-label="Delete saved search">✕</button></div>`).join('')
+    : '<div class="savedempty">None yet — filter, then “Save current view”.</div>';
+}
+// ---- per-listing clipboard summary (honest fields only) ----
+function cardSummary(p){ const f=p.finance||{}; const L=[];
+  L.push(`${p.address}, ${p.city} CA ${p.zip||''}`.trim());
+  const l2=[p.type, p.units?`${p.units} unit${p.units>1?'s':''}`:null, p.price?fmt(p.price):null].filter(Boolean).join(' · '); if(l2) L.push(l2);
+  const m=[]; if(p.cap_rate!=null) m.push('Cap '+p.cap_rate+'%'); if(ppu(p)&&!p.isCondo) m.push('$'+ppu(p).toLocaleString()+'/unit'); if(ppsf(p)) m.push('$'+ppsf(p).toLocaleString()+'/SF'); if(m.length) L.push(m.join(' · '));
+  const fin=[]; if(f.dscr!=null) fin.push('DSCR '+f.dscr); if(f.coc!=null) fin.push('CoC '+f.coc+'%'); if(f.cashNeeded!=null) fin.push('Cash needed '+fmt(f.cashNeeded)); if(f.cashFlow!=null) fin.push('Cash flow/yr '+fmt(f.cashFlow)); if(fin.length) L.push(fin.join(' · '));
+  const r=rentEstimate(p); const ex=[]; if(r) ex.push('Est. rent $'+r.std.toLocaleString()+'/mo'); if(p.deal&&p.deal.score!=null) ex.push('Deal score '+p.deal.score+'/100'); if(p.warrantable_status) ex.push('Warrantability: '+p.warrantable_status); if(ex.length) L.push(ex.join(' · '));
+  if(p.source) L.push(p.source);
+  return L.join('\n');
+}
+function copySummary(id, btn){ const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
+  const done=()=>{ if(btn){ btn.textContent='✓'; setTimeout(()=>btn.textContent='📋',1200); } };
+  try{ navigator.clipboard.writeText(cardSummary(p)).then(done).catch(done); }catch(e){ done(); }
+}
+function applyQuick(k){
+  if(k==='condo'){ F.types=new Set(['Condo']); }
+  else if(k==='mf'){ F.types=new Set(['Multifamily']); }
+  else if(k==='nonqm'){ F.nonqm=true; }
+  else if(k==='unw'){ F.unwarrantable=true; F.types=new Set(['Condo']); }
+  else if(k==='afford'){ F.afford=true; if($('#fAfford')) $('#fAfford').checked=true; }
+  else if(k==='short'){ F.shortlist=true; }
+  syncChips(); render();
+}
+function median(a){ if(!a.length) return null; const s=a.slice().sort((x,y)=>x-y); const m=Math.floor(s.length/2); return s.length%2?s[m]:(s[m-1]+s[m])/2; }
+function renderStats(r){
+  const sb=$('#statsbar'); if(!sb) return;
+  if(!r||!r.length){ sb.innerHTML=''; return; }
+  const prices=r.map(p=>p.price).filter(v=>v>0);
+  const ppus=r.map(p=>ppu(p)).filter(v=>v>0);
+  const ppsfs=r.map(p=>ppsf(p)).filter(v=>v>0);
+  const caps=r.map(p=>p.cap_rate).filter(v=>v!=null);
+  const aff=r.filter(p=>p.finance&&p.finance.affordable).length;
+  const rents=r.map(p=>{ const e=rentEstimate(p); return e?e.std:null; }).filter(v=>v);
+  const pill=(l,v)=>v==null?'':`<span class="statpill"><span class="sl">${l}</span> <b>${v}</b></span>`;
+  sb.innerHTML =
+    pill('Median price', prices.length?'$'+Math.round(median(prices)).toLocaleString():null)
+    +pill('Median $/unit', ppus.length?'$'+Math.round(median(ppus)).toLocaleString():null)
+    +pill('Median $/SF', ppsfs.length?'$'+Math.round(median(ppsfs)).toLocaleString():null)
+    +pill('Avg cap', caps.length?(caps.reduce((a,b)=>a+(+b),0)/caps.length).toFixed(2)+'% ('+caps.length+' disc.)':null)
+    +pill('Median est. rent', rents.length?'$'+Math.round(median(rents)).toLocaleString()+'/mo':null)
+    +pill('In budget', aff+' of '+r.length);
+}
+function render(){
+  const g=$('#grid'); const fc=activeFilterCount();
+  $('#fcount').textContent=fc; $('#fcount').classList.toggle('on',fc>0);
+  { const sb=$('#shortbtn'); if(sb){ sb.textContent='⭐ Shortlist ('+SHORT.size+')'; sb.classList.toggle('shorton',F.shortlist); } }
+  renderActiveBar(); stateToURL(); updateFacetCounts();
+  // Wait-for-selection: on a clean load (no filter chosen) don't dump 2,134 cards — prompt for a pick.
+  // A free-text search counts as a selection, so typing in the search box shows results immediately.
+  if(fc===0 && !F.cityQuery && !idxQuery){
+    g.className='grid';
+    g.innerHTML=`<div class="empty">👈 Expand a filter tab on the left — or start with one of these:
+      <div class="quickstart">
+        <button class="qchip" data-quick="condo">🏢 Condos</button>
+        <button class="qchip" data-quick="mf">🏠 Multifamily</button>
+        <button class="qchip" data-quick="nonqm">◆ Non-QM</button>
+        <button class="qchip" data-quick="unw">⚑ Unwarrantable</button>
+        <button class="qchip" data-quick="afford">✓ In budget</button>
+        ${SHORT.size?`<button class="qchip" data-quick="short">⭐ My shortlist (${SHORT.size})</button>`:''}
+      </div>
+      <span class="small">${(DATA.ranked.length).toLocaleString()} listings loaded · ${DATA.meta.market} — results appear instantly; your active filters show as removable chips up top.</span></div>`;
+    $('#count').innerHTML=`<b>${(DATA.ranked.length).toLocaleString()}</b> loaded · choose a filter to view`;
+    $('#statsbar').innerHTML='';
+    saveState(); return;
+  }
+  let r = applyFilters(DATA.ranked.slice());
+  sortRows(r);
+  renderStats(r);
+  _rows=r; _shown=Math.min(PAGE(), r.length);   // paginate the DOM; stats/count/facets still use the full set
+  paintPage();
+  $('#count').innerHTML = `<b>${r.length}</b> of ${DATA.ranked.length} properties${fc?` · ${fc} filter${fc>1?'s':''} active`:''}`;
+  $('#foot').textContent = `${scenario==='cash'?'all-cash':SCEN[scenario].downPct+'% down @ '+SCEN[scenario].ratePct+'%'} · * →X% = projected cap after value-add (not in-place). Cap rates broker-stated — verify NOI in DD.`;
+  saveState();
+}
+
+// ---- build the filter UI from the data ----
+function buildFacets(){
+  const tCount={}, fCount={}, cCount={};
+  DATA.ranked.forEach(p=>{ tCount[p.type]=(tCount[p.type]||0)+1; const fl=firmLabel(p); fCount[fl]=(fCount[fl]||0)+1; if(p.city) cCount[p.city]=(cCount[p.city]||0)+1; });
+  const chipHTML=(obj,key)=>Object.entries(obj).sort((a,b)=>b[1]-a[1])
+    .map(([k,c])=>`<span class="chip" data-${key}="${k.replace(/"/g,'&quot;')}">${k}<span class="ct">${c}</span></span>`).join('');
+  // Condos are merged into the grid as a real 'Condo' asset type (chip comes from the data below).
+  // Prepend one special in-grid filter: ⚑ Unwarrantable = Condo AND not FHA-approved (non-QM target).
+  const nonqmChip='<span class="chip" data-warr="nonqm" style="border-color:rgba(188,140,255,.55);color:var(--purp,#bc8cff)" title="Non-QM target set: unwarrantable condos + small multifamily (1–9 units) + mixed-use">◆ Non-QM</span>';
+  const unwChip='<span class="chip" data-warr="unwarrantable" style="border-color:rgba(248,81,73,.55);color:var(--bad)" title="Non-FHA-approved condos — filters the grid to non-QM targets">⚑ Unwarrantable</span>';
+  $('#fType').innerHTML=nonqmChip+unwChip+chipHTML(tCount,'t');
+  $('#fFirm').innerHTML=chipHTML(fCount,'fm');
+  // top 14 cities as quick chips; the search box covers the rest
+  const topCities=Object.entries(cCount).sort((a,b)=>b[1]-a[1]).slice(0,14);
+  $('#fCityChips').innerHTML=topCities.map(([k,c])=>`<span class="chip" data-cy="${k.replace(/"/g,'&quot;')}">${k}<span class="ct">${c}</span></span>`).join('');
+  // sort options mirrored into the top bar
+  $('#sortTop').innerHTML=$('#sort').innerHTML;
+}
+
+// ---- chat context ----
+function getContext(){
+  const lines=[];
+  lines.push(`MARKET: ${DATA.meta.market}. Budget: $400,000 down. Active scenario: ${scenario==='cash'?'ALL CASH':SCEN[scenario].downPct+'% down, '+SCEN[scenario].ratePct+'%/'+SCEN[scenario].amortYears+'yr'}.`);
+  lines.push(`Active filters: types=[${[...F.types]}], firms=[${[...F.firms]}], city~"${F.cityQuery}", price ${F.priceMin||'any'}-${F.priceMax||'any'}, capMin=${F.capMin}, unitsMin=${F.unitsMin||'any'}, unitsMax=${F.unitsMax||'any'}, builtAfter=${F.yearMin||'any'}, builtBefore=${F.yearMax||'any'}, status=${F.status}, inBudgetOnly=${F.afford}, sort=${$('#sort').value}.`);
+  lines.push('RANKED PROPERTIES (current scenario):');
+  applyFilters(DATA.ranked).slice(0,120).forEach(p=>{ const f=p.finance;
+    lines.push(`#${p.rank} id=${p.id} | ${p.address}, ${p.city} | ${p.type} | $${p.price.toLocaleString()} | ${p.units}u | cap ${p.cap_rate!=null?p.cap_rate+'%':'n/d'}${p.verified===true?' VERIFIED':''} | firm ${firmLabel(p)} | CoC ${f.coc!=null?f.coc+'%':'n/a'} | DSCR ${f.dscr??'n/a'} | cash needed $${f.cashNeeded.toLocaleString()} ${f.affordable?'(in budget)':'(OVER)'} | composite ${p.composite} | status ${p.status}`);
+  });
+  if(DATA.comps && DATA.comps.submarket_cap_benchmarks){
+    lines.push('\nSUBMARKET CAP BENCHMARKS:');
+    Object.entries(DATA.comps.submarket_cap_benchmarks).forEach(([k,v])=>lines.push(`- ${k}: ${v}`));
+  }
+  const lc = Object.entries(window.liveComps||{}).filter(([,v])=>v&&v.length);
+  if(lc.length){ lines.push('\nLIVE COMPS THIS SESSION:'); lc.forEach(([id,arr])=>{ const p=DATA.ranked.find(x=>x.id===id); lines.push(`for ${p?p.address:id}:`); arr.forEach(c=>lines.push(`  - ${c.title} | ${[c.price,c.cap,c.units,c.sf].filter(Boolean).join(' · ')}`)); }); }
+  return lines.join('\n');
+}
+
+// ---- chat-issued actions (extended for the rich filters) ----
+const TYPE_ALIASES={multifamily:'Multifamily',retail:'Retail',office:'Office',industrial:'Industrial','mixed':'Mixed-use','mixed-use':'Mixed-use',land:'Land',hospitality:'Hospitality'};
+function applyAction(a){
+  const done=[];
+  if(a.scenario && SCEN[String(a.scenario)]){ setScenario(String(a.scenario)); done.push('scenario→'+a.scenario); }
+  if(a.sort && SORTS[a.sort]){ $('#sort').value=a.sort; $('#sortTop').value=a.sort; done.push('sort→'+a.sort); }
+  const setType=v=>{ const key=TYPE_ALIASES[String(v).toLowerCase()]; if(key){ F.types=new Set([key]); done.push('type→'+key); } };
+  if(a.type) setType(a.type);
+  if(a.filter && a.filter!=='all' && a.filter!=='active' && a.filter!=='affordable') setType(a.filter);
+  if(a.filter==='affordable'){ F.afford=true; done.push('in-budget only'); }
+  if(a.filter==='active'){ F.status='active'; done.push('active only'); }
+  if(a.filter==='all'){ F.types.clear(); F.afford=false; F.status='all'; done.push('cleared'); }
+  if(a.firm){ F.firms=new Set([a.firm]); done.push('firm→'+a.firm); }
+  if(a.city){ F.cityQuery=String(a.city).toLowerCase(); $('#fCity').value=a.city; done.push('city→'+a.city); }
+  if(a.maxPrice!=null && !isNaN(+a.maxPrice)){ F.priceMax=+a.maxPrice; $('#fPriceMax').value=+a.maxPrice; done.push('max $'+(+a.maxPrice).toLocaleString()); }
+  if(a.minPrice!=null && !isNaN(+a.minPrice)){ F.priceMin=+a.minPrice; $('#fPriceMin').value=+a.minPrice; done.push('min $'+(+a.minPrice).toLocaleString()); }
+  if(a.capMin!=null && !isNaN(+a.capMin)){ F.capMin=+a.capMin; $('#fCap').value=+a.capMin; $('#capVal').textContent=F.capMin?F.capMin+'%+':'any'; done.push('cap ≥'+a.capMin+'%'); }
+  if(a.unitsMin!=null && !isNaN(+a.unitsMin)){ F.unitsMin=+a.unitsMin; $('#fUnits').value=+a.unitsMin; done.push('units ≥'+a.unitsMin); }
+  if(a.unitsMax!=null && !isNaN(+a.unitsMax)){ F.unitsMax=+a.unitsMax; $('#fUnitsMax').value=+a.unitsMax; done.push('units ≤'+a.unitsMax); }
+  if(a.yearMin!=null && !isNaN(+a.yearMin)){ F.yearMin=+a.yearMin; $('#fYear').value=+a.yearMin; done.push('built ≥'+a.yearMin); }
+  if(a.yearMax!=null && !isNaN(+a.yearMax)){ F.yearMax=+a.yearMax; $('#fYearMax').value=+a.yearMax; done.push('built ≤'+a.yearMax); }
+  syncChips(); render();
+  if(a.highlightId){ const el=$('#card-'+a.highlightId); if(el){ el.scrollIntoView({behavior:'smooth',block:'center'}); el.classList.add('flash'); setTimeout(()=>el.classList.remove('flash'),2200); done.push('→'+a.highlightId); } }
+  return done;
+}
+
+function setScenario(s){ scenario=s; localStorage.setItem('cre_scenario',s);
+  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===s)); recompute(); }
+
+function syncChips(){
+  $$('#fType .chip').forEach(c=>{ if(c.dataset.warr==='unwarrantable') c.classList.toggle('active',F.unwarrantable); else if(c.dataset.warr==='nonqm') c.classList.toggle('active',F.nonqm); else c.classList.toggle('active',F.types.has(c.dataset.t)); });
+  $$('#fFirm .chip').forEach(c=>c.classList.toggle('active',F.firms.has(c.dataset.fm)));
+  $$('#fCityChips .chip').forEach(c=>c.classList.toggle('active',F.cityQuery===c.dataset.cy.toLowerCase()));
+  $$('#fStatus .chip').forEach(c=>c.classList.toggle('active',F.status===c.dataset.st));
+}
+
+// ---- Top-of-page natural-language chat bar (client-side; drives F via applyAction) ----
+const CHAT_TYPES=[['multifamily','Multifamily'],['multi-family','Multifamily'],['multi family','Multifamily'],['apartment','Multifamily'],['office','Office'],['industrial','Industrial'],['retail','Retail'],['mixed-use','Mixed-use'],['mixed use','Mixed-use'],['net lease','Retail'],['net-lease','Retail'],['nnn','Retail'],['land','Land'],['hotel','Hospitality'],['hospitality','Hospitality']];
+function chatMoney(s){ s=String(s).toLowerCase().replace(/[,\s$]/g,''); let n=parseFloat(s); if(isNaN(n))return null; if(/m/.test(s))n*=1e6; else if(/k/.test(s))n*=1e3; return Math.round(n); }
+function chatCities(){ try{ return [...new Set((DATA||[]).map(r=>(r.city||'').trim()).filter(Boolean))].sort((a,b)=>b.length-a.length); }catch(e){ return []; } }
+function chatClear(){
+  F.types.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.unwarrantable=F.nonqm=false;
+  ['#fCity','#fPriceMin','#fPriceMax','#fUnits','#fUnitsMax','#fYear','#fYearMax'].forEach(x=>{const e=$(x); if(e)e.value='';});
+  const c=$('#fCap'); if(c){ c.value=0; const v=$('#capVal'); if(v)v.textContent='any'; }
+  syncChips(); render();
+}
+function applyChat(raw){
+  const msg=$('#chatmsg'); const s=' '+String(raw||'').toLowerCase().replace(/\s+/g,' ').trim()+' ';
+  if(!s.trim())return;
+  if(/^\s*(clear|reset|all|everything|start over|show all)\s*$/.test(s)){ chatClear(); if(msg)msg.textContent='Cleared'; return; }
+  // fresh search — clear chat-controlled fields + their drawer inputs first
+  F.types.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.unwarrantable=F.nonqm=false;
+  ['#fCity','#fPriceMin','#fPriceMax','#fUnits','#fUnitsMax','#fYear','#fYearMax'].forEach(x=>{const e=$(x); if(e)e.value='';});
+  const a={}; const got=[]; let m;
+  // warrantability / condo
+  if(/unwarrantable|non-?warrantable|not warrantable/.test(s)){ F.unwarrantable=true; got.push('unwarrantable condos'); }
+  if(/non-?qm\b|non qm/.test(s)){ F.nonqm=true; got.push('non-QM'); }
+  if(/\bcondos?\b/.test(s) && !F.unwarrantable){ F.types.add('Condo'); got.push('Condo'); }
+  // financing scenario (the seg now lives in the left drawer)
+  if(/\ball\s*cash\b/.test(s)){ a.scenario='cash'; got.push('all cash'); }
+  else if(/\b30\s*%?\s*down\b/.test(s)){ a.scenario='30'; got.push('30% down'); }
+  else if(/\b25\s*%?\s*down\b/.test(s)){ a.scenario='25'; got.push('25% down'); }
+  // units
+  const WORDU={duplex:2,triplex:3,fourplex:4,fiveplex:5};
+  for(const w in WORDU){ if(s.includes(' '+w)){ a.unitsMin=a.unitsMax=WORDU[w]; got.push(WORDU[w]+' units'); } }
+  if(m=s.match(/(\d+)\s*(?:-|to)\s*(\d+)\s*unit/)){ a.unitsMin=+m[1]; a.unitsMax=+m[2]; got.push(m[1]+'–'+m[2]+' units'); }
+  else if(m=s.match(/(?:under|below|less than|up to|max(?:imum)?|<)\s*(\d+)\s*unit/)){ a.unitsMax=+m[1]; got.push('≤'+m[1]+' units'); }
+  else if(m=s.match(/(?:over|above|more than|at least|min(?:imum)?|\+|>)\s*(\d+)\s*unit/)){ a.unitsMin=+m[1]; got.push(m[1]+'+ units'); }
+  else if(m=s.match(/(\d+)\s*unit/)){ a.unitsMin=a.unitsMax=+m[1]; got.push(m[1]+' units'); }
+  // type (set on F directly so Condo/multi don't get clobbered by applyAction's single-type set)
+  const types=[]; for(const [k,v] of CHAT_TYPES){ if(s.includes(' '+k) && !types.includes(v)) types.push(v); }
+  types.forEach(v=>F.types.add(v)); if(types.length) got.push(types.join('+'));
+  // city (known city names from the data; longest-first to avoid substring collisions)
+  for(const c of chatCities()){ const cl=c.toLowerCase(); if(s.includes(' '+cl+' ') || s.includes(' in '+cl)){ a.city=c; got.push(c); break; } }
+  // price
+  if((m=s.match(/\$?\s*([\d.,]+\s*[mk]?)\s*(?:-|to)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b/.test(m[0])){ a.minPrice=chatMoney(m[1]); a.maxPrice=chatMoney(m[2]); got.push('$'+m[1].trim()+'–$'+m[2].trim()); }
+  else if((m=s.match(/(?:under|below|less than|up to|<)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b|\d{4,}/.test(m[0])){ a.maxPrice=chatMoney(m[1]); got.push('< $'+m[1].trim()); }
+  else if((m=s.match(/(?:over|above|more than|at least|>)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b|\d{4,}/.test(m[0])){ a.minPrice=chatMoney(m[1]); got.push('> $'+m[1].trim()); }
+  // cap rate
+  if(m=s.match(/(\d+(?:\.\d+)?)\s*%?\s*cap/) || (m=s.match(/cap\s*(?:rate)?\s*(?:over|above|min|>|of)?\s*(\d+(?:\.\d+)?)/))){ a.capMin=+m[1]; got.push(m[1]+'%+ cap'); }
+  // year
+  if(m=s.match(/(?:built\s*after|newer than|after|since)\s*(\d{4})/)){ a.yearMin=+m[1]; got.push('after '+m[1]); }
+  if(m=s.match(/(?:built\s*before|older than|before)\s*(\d{4})/)){ a.yearMax=+m[1]; got.push('before '+m[1]); }
+  applyAction(a);   // maps a.* → F + drawer inputs, then syncChips()+render()
+  if(msg) msg.textContent = got.length ? ('Showing: '+got.join(' · ')) : ('Couldn’t parse — try “4 units in Los Angeles”');
+}
+(function(){ const q=$('#chatq'), go=$('#chatgo'), cl=$('#chatclear'), ex=$('#chatex');
+  if(q) q.addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); applyChat(q.value); } });
+  if(go) go.addEventListener('click',()=>applyChat(q?q.value:''));
+  if(cl) cl.addEventListener('click',()=>{ if(q)q.value=''; chatClear(); const m=$('#chatmsg'); if(m)m.textContent='Cleared'; });
+  if(ex) ex.addEventListener('click',e=>{ const c=e.target.closest('.chatchip'); if(!c)return; if(q)q.value=c.dataset.q; applyChat(c.dataset.q); });
+})();
+
+// ---- persistence ----
+function saveState(){ try{ localStorage.setItem('cre_filters', JSON.stringify({
+  types:[...F.types],firms:[...F.firms],cityQuery:F.cityQuery,priceMin:F.priceMin,priceMax:F.priceMax,
+  capMin:F.capMin,unitsMin:F.unitsMin,unitsMax:F.unitsMax,yearMin:F.yearMin,yearMax:F.yearMax,status:F.status,afford:F.afford,hasPhone:F.hasPhone,hasEmail:F.hasEmail,sort:$('#sort').value })); }catch(e){} }
+function loadState(){ try{ const s=JSON.parse(localStorage.getItem('cre_filters')||'{}');
+  if(s.types) F.types=new Set(s.types); if(s.firms) F.firms=new Set(s.firms);
+  F.cityQuery=s.cityQuery||''; F.priceMin=s.priceMin??null; F.priceMax=s.priceMax??null;
+  F.capMin=s.capMin||0; F.unitsMin=s.unitsMin??null; F.unitsMax=s.unitsMax??null; F.yearMin=s.yearMin??null; F.yearMax=s.yearMax??null; F.status=s.status||'all'; F.afford=!!s.afford; F.hasPhone=!!s.hasPhone; F.hasEmail=!!s.hasEmail;
+  if(s.cityQuery) $('#fCity').value=s.cityQuery; if(s.priceMin) $('#fPriceMin').value=s.priceMin; if(s.priceMax) $('#fPriceMax').value=s.priceMax;
+  if(s.capMin){ $('#fCap').value=s.capMin; $('#capVal').textContent=s.capMin+'%+'; } if(s.unitsMin) $('#fUnits').value=s.unitsMin; if(s.unitsMax) $('#fUnitsMax').value=s.unitsMax; if(s.yearMin) $('#fYear').value=s.yearMin; if(s.yearMax) $('#fYearMax').value=s.yearMax;
+  $('#fAfford').checked=F.afford; if($('#fHasPhone'))$('#fHasPhone').checked=F.hasPhone; if($('#fHasEmail'))$('#fHasEmail').checked=F.hasEmail; if(s.sort){ $('#sort').value=s.sort; }
+}catch(e){} }
+function resetAll(){ F.types.clear(); F.firms.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.status='all'; F.afford=false; F.unwarrantable=false; F.nonqm=false; F.hasPhone=false; F.hasEmail=false;
+  ['fCity','fPriceMin','fPriceMax','fUnits','fUnitsMax','fYear','fYearMax'].forEach(id=>$('#'+id).value=''); $('#fCap').value=0; $('#capVal').textContent='any'; $('#fAfford').checked=false; if($('#fHasPhone'))$('#fHasPhone').checked=false; if($('#fHasEmail'))$('#fHasEmail').checked=false;
+  syncChips(); render(); }
+
+// ---- chat ----
+function addMsg(role, text, actions){
+  const d=document.createElement('div'); d.className='msg '+role;
+  d.innerHTML = (text||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/```action[\s\S]*?```/g,'').trim()
+    .replace(/\*\*(.+?)\*\*/g,'<b>$1</b>').replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\n/g,'<br>');
+  if(actions&&actions.length) d.innerHTML += `<div class="actiontag">⚡ view updated: ${actions.join(', ')}</div>`;
+  $('#log').appendChild(d); $('#log').scrollTop=$('#log').scrollHeight; return d;
+}
+function parseAction(reply){ const m=reply.match(/```action\s*([\s\S]*?)```/); if(!m) return null; try{ return JSON.parse(m[1].trim()); }catch{ return null; } }
+async function ask(text){
+  addMsg('u', text); history.push({role:'user',content:text});
+  $('#send').disabled=true; const thinking=addMsg('sys','…thinking');
+  try{
+    const r = await fetch('/api/claude-chat',{ method:'POST', headers:{'Content-Type':'application/json'},
+      body: JSON.stringify({ messages: history, context: getContext() }) });
+    const j = await r.json(); thinking.remove();
+    if(j.error){ addMsg('sys','⚠ '+j.error); }
+    else { const act=parseAction(j.reply); const applied=act?applyAction(act):null; addMsg('a', j.reply, applied); history.push({role:'assistant',content:j.reply}); }
+  }catch(e){ thinking.remove(); addMsg('sys','⚠ '+e.message); }
+  $('#send').disabled=false; $('#q').focus();
+}
+
+// ---- property history + live comps ----
+window.liveComps = {};
+async function fetchHistory(btn){
+  const id=btn.dataset.id, out=$('#hist-'+id); if(!out) return;
+  if(out.dataset.loaded){ out.style.display = out.style.display==='none'?'block':'none'; return; }
+  out.innerHTML='<span class="small">…checking WhoLivedThere archive (free)…</span>';
+  try{
+    const q=new URLSearchParams({address:btn.dataset.address||'',city:btn.dataset.city||'',zip:btn.dataset.zip||''});
+    const j=await (await fetch('/api/history?'+q)).json();
+    out.dataset.loaded='1';
+    if(j.available===false){ out.innerHTML='<span class="b warn">⚠ archive offline</span>'; return; }
+    if(!j.matched){ out.innerHTML='<span class="small">📜 No record in the WhoLivedThere archive for this address yet.</span>'; return; }
+    const P=j.parcel, money=v=>v?'$'+Number(v).toLocaleString():'—';
+    let h='<div class="small" style="margin-top:6px;border-top:1px solid #2a2a2a;padding-top:6px">📜 <b>WhoLivedThere archive</b>';
+    if(P){ h+=`<div class="badges" style="margin:4px 0">${P.year_built?`<span class="b">built ${P.year_built}</span>`:''}${P.use_type?`<span class="b type">${P.use_type}</span>`:''}${P.units?`<span class="b">${P.units} unit${P.units>1?'s':''}</span>`:''}${P.total_value?`<span class="b ok" title="LA County assessor total assessed value">assessed ${money(P.total_value)}</span>`:''}${P.land_value?`<span class="b">land ${money(P.land_value)}</span>`:''}${P.improvement_value?`<span class="b">improv ${money(P.improvement_value)}</span>`:''}</div><div class="small">${P.use_description||''} · APN ${P.apn||'—'} · ${P.situs_address||''}</div>`; }
+    if(j.permits&&j.permits.length){ h+=`<div class="small" style="margin-top:4px">🔨 Permits (${j.permits.length}): `+j.permits.slice(0,5).map(pm=>`${pm.permit_type||'permit'}${pm.valuation?' $'+Number(pm.valuation).toLocaleString():''}${pm.issue_date?' ('+String(pm.issue_date).slice(0,10)+')':''}`).join(' · ')+'</div>'; }
+    if(j.filming&&j.filming.length){ h+=`<div class="small" style="margin-top:4px">🎬 Filmed here: `+j.filming.slice(0,5).map(f=>`${f.title||'production'}${f.year?' ('+f.year+')':''}`).join(' · ')+'</div>'; }
+    if(j.events&&j.events.length){ h+=`<div class="small" style="margin-top:4px">📌 ${j.events.slice(0,4).map(ev=>ev.summary||ev.kind).join(' · ')}</div>`; }
+    out.innerHTML=h+'</div>';
+  }catch(e){ out.innerHTML='<span class="b warn">⚠ '+e.message+'</span>'; }
+}
+async function fetchLiveComps(id){
+  const p = DATA.ranked.find(x=>x.id===id); if(!p) return;
+  const out = $('#lc-'+id); if(!out) return;
+  out.innerHTML = '<span class="small">…pulling fresh comps (~$0.03)…</span>';
+  try{
+    const r = await fetch('/api/comps',{method:'POST',headers:{'Content-Type':'application/json'},
+      body:JSON.stringify({address:p.address,city:p.city,type:p.type})});
+    const j = await r.json();
+    if(j.error && !(j.comps&&j.comps.length)){ out.innerHTML = `<span class="b warn">⚠ ${j.error}</span>`; return; }
+    window.liveComps[id] = j.comps||[];
+    const none = (!j.comps||!j.comps.length)?' — none scraped (source may have blocked)':'';
+    out.innerHTML = `<div class="small">Live comps via ${j.source||'web'} (cost ${j.cost})${none}:</div>` +
+      (j.comps||[]).map(c=>`<div class="lc-item"><span>${esc(c.title||c.url||'comp')}</span>${c.url?` <button class="copyb" data-copyval="${esc(c.url)}" title="Copy comp URL">⧉</button>`:''} — ${esc([c.price,c.cap,c.units,c.sf].filter(Boolean).join(' · '))}</div>`).join('');
+  }catch(e){ out.innerHTML = `<span class="b bad">⚠ ${e.message}</span>`; }
+}
+
+// ---- drawer open/close ----
+function openDrawer(){ $('#drawer').classList.add('open'); $('#scrim').classList.add('on'); }
+function closeDrawer(){ $('#drawer').classList.remove('open'); $('#scrim').classList.remove('on'); }
+
+// ── Inline list-row expansion + Agent CRM actions ──────────────────────────────────
+function postJSON(url,body){ return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body||{})}).then(r=>r.json()); }
+function toast(msg){ let t=document.getElementById('crmToast'); if(!t){ t=document.createElement('div'); t.id='crmToast'; t.className='crmToast'; document.body.appendChild(t); } t.textContent=msg; t.classList.add('on'); clearTimeout(t._t); t._t=setTimeout(()=>t.classList.remove('on'),1900); }
+// In-place refresh after any CRM save — repaints EVERY visible surface for this listing (list
+// agent cell + open list-row panel + card block) WITHOUT a full render() (which would collapse
+// expanded rows). Works for every asset type in either view.
+function refreshContactUI(id){
+  const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
+  try{ delete p.__idxHay; }catch(_){}   // CRM changed → let the search haystack rebuild
+
+  const row=[...document.querySelectorAll('.listtbl tr.lrow')].find(r=>r.dataset.id===id);
+  if(row){ const cols=listCols(); const ai=cols.findIndex(c=>c.k==='agent');
+    if(ai>=0 && row.children[ai+1]) row.children[ai+1].innerHTML=colCell('agent',p);
+    const exp=row.nextElementSibling;
+    if(exp&&exp.classList.contains('lexp')&&exp.classList.contains('open')){
+      const body=exp.querySelector('.lexpbody'); if(body){ body.innerHTML=expandBody(p); body.dataset.loaded='1'; }
+    }
+  }
+  const cb=document.getElementById('cardagent-'+id); if(cb) cb.outerHTML=cardAgentBlock(p);
+}
+async function markContacted(id){
+  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/contacted',{}); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('✓ marked contacted '+fmtWhen(j.contact.contacted_at)); } else toast('⚠ '+(j.error||'failed')); }
+  catch(e){ toast('⚠ '+e.message); }
+}
+async function saveAgentInfo(id){
+  const scope=document.getElementById('composeM')&&document.getElementById('composeM').classList.contains('on')?document.getElementById('composeM'):document;
+  const inputs=[...scope.querySelectorAll('.crmf')].filter(i=>i.dataset.id===id);
+  const body={}; inputs.forEach(i=>body[i.dataset.info]=i.value.trim());
+  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/info',body); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); toast('💾 contact info saved'); } else toast('⚠ '+(j.error||'failed')); }
+  catch(e){ toast('⚠ '+e.message); }
+}
+async function deleteLetter(id,idx){
+  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/letter/'+encodeURIComponent(idx)+'/delete',{}); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('letter deleted'); } }
+  catch(e){ toast('⚠ '+e.message); }
+}
+function agentDraft(p,a){ return `Hi ${a.name||'there'},\n\nI'm interested in ${p.address||'your listing'}${p.city?' in '+p.city:''}${p.price?' (listed '+fmt(p.price)+')':''}. Could you share the setup — current rents / in-place NOI, any financing in place, and whether an offer near list would be entertained?\n\nThanks`; }
+// The full per-listing contact modal body (editable info + mark-contacted + compose/save + letter log).
+// The single per-listing "Agent window" — ONE modal with tabs: Agent · Letters · Find · Comps · History.
+function contactModalBody(p){
+  const a=agentInfo(p), id=p.id, firm=a.firm;
+  const tel=a.phone?`<a class="ain" href="tel:${esc(a.phone).replace(/[^0-9+]/g,'')}">📞 ${esc(a.phone)}</a>`:'';
+  const eml=a.email?`<a class="ain" href="mailto:${esc(a.email)}">✉ ${esc(a.email)}</a>`:'';
+  const letters=a.letters.length?`<div class="crm-letters"><div class="deeplbl">Saved letters (${a.letters.length}) — saved, not sent</div>`
+    +a.letters.map((l,i)=>`<div class="lett"><div class="letth"><b>${esc(l.subject||'(no subject)')}</b>${l.draft_id?`<span class="achip done" title="Gmail draft created${l.to?' to '+esc(l.to):''} — review & send in Gmail">📤 drafted</span>`:''}<span class="letts">${fmtWhen(l.ts)}</span><button class="letdel" data-letdel="${id}" data-idx="${l.id||i}" title="Delete this saved letter">✕</button></div><div class="lettb">${esc(l.body||'')}</div></div>`).join('')+`</div>`:'<div class="tabmut">No saved letters yet.</div>';
+  const find=[];
+  if(firm&&firm!=='Unknown'){
+    find.push(`<a class="findlink" href="${gsearch('"'+firm+'" commercial real estate Los Angeles')}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm)} — Google</a>`);
+    find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/companies/?keywords=${encodeURIComponent(firm)}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm)} — LinkedIn</a>`);
+    find.push(`<button class="findlink" data-firmfilter="${esc(firm).replace(/"/g,'&quot;')}" title="Show only ${esc(firm)} listings in the grid">▦ More from ${esc(firm)} (in-app)</button>`);
+  }
+  if(a.name){
+    find.push(`<a class="findlink" href="${gsearch('"'+a.name+'"'+(firm&&firm!=='Unknown'?' "'+firm+'"':'')+' real estate agent')}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — Google</a>`);
+    find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/people/?keywords=${encodeURIComponent(a.name+(firm&&firm!=='Unknown'?' '+firm:''))}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — LinkedIn</a>`);
+    find.push(`<a class="findlink" href="${gsearch('"'+a.name+'" DRE license California real estate')}" target="_blank" rel="noopener noreferrer">🪪 ${esc(a.name)} — DRE license</a>`);
+  }
+  listingLinkBits(p,firm).forEach(b=>find.push(b));
+  find.push(`<span class="findlink findloop">${loopnetLink(p)}</span>`);
+  const findHtml=find.length?`<div class="findgrid">${find.join('')}</div>`:'<div class="tabmut">Add an agent/firm in the Agent tab to enable lookups.</div>';
+  return `<div class="composeCard tabwin">
+    <div class="composeH">🧑‍💼 <b>${esc(p.address||'listing')}</b>${p.city?', '+esc(p.city):''}<span class="winmeta">${a.name?esc(a.name):'no agent yet'}${a.contacted_at?' · ✓ '+fmtWhen(a.contacted_at):''}</span><button class="composeX" title="Close">✕</button></div>
+    <div class="tabbar">
+      <button class="tabt active" data-tab="agent">Agent</button>
+      <button class="tabt" data-tab="letters">Letters${a.letters.length?' ('+a.letters.length+')':''}</button>
+      <button class="tabt" data-tab="find">Find</button>
+      <button class="tabt" data-tab="comps">Comps</button>
+      <button class="tabt" data-tab="history">History</button>
+    </div>
+    <div class="tabpane active" data-pane="agent">
+      <div class="cm-info">
+        <input class="crmf" data-info="name" data-id="${id}" placeholder="Agent name" value="${esc(a.name||'')}">
+        <input class="crmf" data-info="phone" data-id="${id}" placeholder="Phone" value="${esc(a.phone||'')}">
+        <input class="crmf" data-info="email" data-id="${id}" placeholder="Email" value="${esc(a.email||'')}">
+      </div>
+      ${(tel||eml)?`<div class="ainrow">${tel} ${eml}</div>`:''}
+      <div class="cm-notes"><div class="deeplbl">Notes</div><textarea class="crmf" data-info="notes" data-id="${id}" rows="4" placeholder="Notes on this agent / listing">${esc(a.notes||'')}</textarea></div>
+      <div class="cm-row">
+        <button class="abtn saveinfobtn" data-saveinfo="${id}">💾 Save info + notes</button>
+        <button class="abtn markbtn" data-mark="${id}">${a.contacted_at?'↻ Update contacted date':'✓ Mark contacted'}</button>
+        ${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}
+      </div>
+    </div>
+    <div class="tabpane" data-pane="letters">
+      <div class="composeGate">⚑ <b>Save letter</b> logs it. <b>Send via George</b> creates a Gmail <b>DRAFT</b> — NOT sent; you review &amp; hit send in Gmail.</div>
+      <input id="composeSubj" placeholder="Subject" value="${esc('Interest in '+(p.address||'your listing')+(p.city?', '+p.city:''))}">
+      <textarea id="composeBody" rows="7">${esc(agentDraft(p,a))}</textarea>
+      <div class="composeActs">
+        <button id="composeSave" class="primary" data-id="${id}">💾 Save letter</button>
+        <button id="composeSend" class="gated" data-id="${id}" title="Create a Gmail DRAFT to the agent (fill Email in the Agent tab) — not sent">📤 Send via George</button>
+        <button id="composeCopy">📋 Copy</button>
+      </div>
+      ${letters}
+    </div>
+    <div class="tabpane" data-pane="find">${findHtml}</div>
+    <div class="tabpane" data-pane="comps">
+      <button class="lcbtn" data-id="${id}" title="Pull fresh comps via a cloud browser (~$0.03)">🔄 Pull live comps (~$0.03)</button>
+      <div class="lcout" id="lc-${id}"></div>
+    </div>
+    <div class="tabpane" data-pane="history">
+      <button class="histbtn" data-id="${id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (free)">📜 Load property history</button>
+      <div class="histout" id="hist-${id}"></div>
+    </div>
+  </div>`;
+}
+// One modal, opens for EVERY listing from any surface. Its own click delegation (it lives on
+// document.body, outside #grid) handles save-info / mark / save-letter / delete-letter.
+function openContact(id){
+  const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
+  let m=document.getElementById('composeM');
+  if(!m){ m=document.createElement('div'); m.id='composeM'; m.className='composeM'; document.body.appendChild(m); }
+  m.dataset.id=id; m.innerHTML=contactModalBody(p); m.classList.add('on');
+  m.onclick=async(e)=>{
+    if(e.target===m || e.target.closest('.composeX,.composeX2')){ closeCompose(); return; }
+    const tb=e.target.closest('.tabt'); if(tb){ activateTab(m,tb.dataset.tab); return; }
+    const cv=e.target.closest('[data-copyval]'); if(cv){ navigator.clipboard.writeText(cv.dataset.copyval).then(()=>toast('📋 copied')).catch(()=>{}); return; }
+    const ff=e.target.closest('[data-firmfilter]'); if(ff){ closeCompose(); F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
+    const lc=e.target.closest('.lcbtn'); if(lc){ fetchLiveComps(lc.dataset.id); return; }
+    const hi=e.target.closest('.histbtn'); if(hi){ fetchHistory(hi); return; }
+    if(e.target.closest('#composeCopy')){ navigator.clipboard.writeText(m.querySelector('#composeBody').value); toast('📋 copied'); return; }
+    if(e.target.closest('#composeSave')){
+      const subject=m.querySelector('#composeSubj').value, body=m.querySelector('#composeBody').value;
+      try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/letter',{subject,body,channel:'letter'});
+        if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('💾 letter saved to contact log'); }
+        else toast('⚠ '+(j.error||'save failed')); }catch(err){ toast('⚠ '+err.message); }
+      return;
+    }
+    if(e.target.closest('#composeSend')){
+      const to=((m.querySelector('.crmf[data-info=email]')||{}).value||'').trim();
+      if(!to){ toast('add the agent email first'); return; }
+      const subject=m.querySelector('#composeSubj').value, body=m.querySelector('#composeBody').value;
+      if(!confirm('Create a Gmail DRAFT to '+to+' via George?\n\nIt will NOT be sent — it lands in your Gmail drafts (steve@designerwallcoverings.com) for you to review and send.')) return;
+      try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/send-draft',{to,subject,body});
+        if(j.ok){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('📤 Gmail draft created — review & send in Gmail'); }
+        else if(j.error==='george_not_configured'){ toast('⚠ George drafting not enabled on this box'); alert('George drafting isn’t enabled on this server yet — the letter is not drafted.\n\nTo turn it on, GEORGE_AUTH must be set in the CRCP env (one-time). The letter is unaffected; use 💾 Save letter to keep it.'); }
+        else toast('⚠ '+(j.detail||j.error||'draft failed')); }
+      catch(err){ toast('⚠ '+err.message); }
+      return;
+    }
+    if(e.target.closest('[data-saveinfo]')){ saveAgentInfo(id); return; }
+    if(e.target.closest('[data-mark]')){ markContacted(id); return; }
+    const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(id, ld.dataset.idx); return; }
+  };
+}
+// Switch the active tab within the agent window.
+function activateTab(m,tab){
+  m.querySelectorAll('.tabt').forEach(t=>t.classList.toggle('active',t.dataset.tab===tab));
+  m.querySelectorAll('.tabpane').forEach(pn=>pn.classList.toggle('active',pn.dataset.pane===tab));
+}
+// Repaint the open modal in place (after mark/letter changes) — preserves open state AND active tab.
+function repaintModal(id){ const m=document.getElementById('composeM'); const p=DATA.ranked.find(x=>x.id===id);
+  if(m && m.classList.contains('on') && m.dataset.id===id && p){
+    const cur=(m.querySelector('.tabt.active')||{}).dataset; const tab=cur?cur.tab:'agent';
+    m.innerHTML=contactModalBody(p); activateTab(m,tab||'agent');
+  } }
+function openCompose(id){ openContact(id); }   // back-compat alias
+function closeCompose(){ const m=document.getElementById('composeM'); if(m) m.classList.remove('on'); }
+
+// ---- Broker-history pilot index ----
+// Keyed by both listing id (e.g. crx2477882) and lowercase address.
+// Loaded once at boot; cards read from this map at render time.
+window.BROKER_HIST = {};   // id → enrichment object
+window.BROKER_HIST_ADDR = {};  // lc-address → enrichment object
+fetch('/api/broker-history').then(r=>r.json()).then(j=>{
+  (j.properties||[]).forEach(p=>{
+    if(p.id) window.BROKER_HIST[p.id]=p;
+    if(p.address) window.BROKER_HIST_ADDR[p.address.toLowerCase().trim()]=p;
+  });
+  // Re-render if DATA is already loaded (race: demographics loads fast too)
+  if(window.DATA_READY) render();
+}).catch(()=>{}); // graceful — broker pilot data is additive, not load-blocking
+
+// ---- boot ----
+fetch('data/comps.json').then(r=>r.json()).then(c=>{ if(DATA) DATA.comps=c; else window.__comps=c; }).catch(()=>{});
+fetch('data/demographics.json').then(r=>r.json()).then(d=>{ window.DEMO=d.byCity||{}; if(DATA) render(); }).catch(()=>{});
+// Map a condo (separate /api/condos schema) into the explorer's property shape so it flows through
+// finance, scoring, filters, sort, and the card/list renderers as a first-class 'Condo' asset type.
+function mapCondo(c){
+  return { id:c.id, address:c.address, city:c.city, zip:c.zip, type:'Condo',
+    price:+c.price||0, units:1, cap_rate:null, year_built:c.year_built||null,
+    status:'Active', source:c.source, firm:c.firm_name||null, verified:false,
+    isCondo:true, warrantable_status:c.warrantable_status,
+    beds:c.beds, baths:c.baths, sqft:c.sqft, hoa:c.hoa, broker_name:c.broker_name,
+    project_name:c.project_name, upside_note:null, qwen:null };
+}
+function loadRanked(_retry){ return fetch('data/ranked.json').then(r=>{ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); }).catch(e=>{ if(_retry>0){ return new Promise(res=>setTimeout(res,1500)).then(()=>loadRanked(_retry-1)); } throw e; }); } loadRanked(1).then(async d=>{
+  DATA=d; if(window.__comps) DATA.comps=window.__comps;
+  // Merge condo inventory in-grid (best-effort; explorer still works if the API is down).
+  let condoN=0; try{ const cd=await (await fetch('/api/condos')).json(); const cs=(cd.condos||[]).map(mapCondo); condoN=cs.length; DATA.ranked=DATA.ranked.concat(cs); }catch(e){}
+  $('#sub').innerHTML = `${d.ranked.length} listings${condoN?` · ${condoN} condos`:''} · <b>${d.meta.market}</b>${d.generated?' · sourced '+d.generated:''}`;
+
+  scenario = localStorage.getItem('cre_scenario')||'25';
+  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===scenario));
+  buildFacets();
+  loadState();
+  urlToState();  // a shared ?query overrides saved localStorage state and restores the exact view
+  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===scenario));
+
+  // drawer wiring
+  $('#burger').onclick=openDrawer; $('#dclose').onclick=closeDrawer; $('#scrim').onclick=closeDrawer;
+  $('#reset').onclick=resetAll;
+  // collapsible filter tabs — click a section header to expand/collapse; all collapsed on load
+  $('#drawer').addEventListener('click',e=>{ const h=e.target.closest('h4'); if(h && h.parentElement.classList.contains('fsec')) h.parentElement.classList.toggle('collapsed'); });
+  // Fields section starts EXPANDED and first — all other fsec sections collapse on load.
+  $$('#drawer .fsec').forEach(s=>{ if(s.id!=='fieldsSec') s.classList.add('collapsed'); });
+  // active-filter summary bar: remove one filter or reset all
+  $('#activebar').addEventListener('click',e=>{ if(e.target.closest('#afreset')){ resetAll(); return; } const c=e.target.closest('.afchip'); if(c) clearFilter(c.dataset.clear); });
+  // saved searches
+  buildSavedList();
+  $('#savedList').addEventListener('click',e=>{ const del=e.target.closest('[data-del]'); if(del){ SAVED.splice(+del.dataset.del,1); saveSaved(); buildSavedList(); return; } const n=e.target.closest('.savedname'); if(n){ location.href=location.pathname+(n.dataset.qs||''); } });
+  $('#savebtn').onclick=()=>{ const name=(prompt('Name this saved search:')||'').trim(); if(!name) return; SAVED.push({name, qs:location.search||''}); saveSaved(); buildSavedList(); };
+  $('#linkbtn').onclick=copyShareLink;
+  $('#csvbtn').onclick=exportCSV;
+  // search-all-fields box (Steve list-build rule 7) — filters across EVERY field, space = AND
+  { const si=$('#idxSearch'); if(si) si.oninput=()=>{ idxQuery=si.value.trim().toLowerCase(); if($('#railSearch'))$('#railSearch').value=si.value; render(); }; }
+  // Reset the PRIMARY List table layout back to the tight default: column order, resize widths,
+  // and visibility. Clears idxColOrder + col-resize's cr:<pathname> widths + resets VIS to defaults.
+  { const rb=$('#idxReset'); if(rb) rb.onclick=()=>{
+      COLORDER=[]; saveColOrder();
+      try{ Object.keys(localStorage).forEach(k=>{ if(k.indexOf('cr:'+location.pathname+':')===0) localStorage.removeItem(k); }); }catch(e){}
+      FIELDS.forEach(f=>VIS[f.k]=!!f.def); saveVis();
+      lastColSig=''; buildFieldToggles(); render();
+    }; }
+  // facet chip handlers (multi-select toggles)
+  $('#fType').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return;
+    if(c.dataset.warr==='nonqm'){ F.nonqm=!F.nonqm; syncChips(); render(); return; }
+    if(c.dataset.warr==='unwarrantable'){ F.unwarrantable=!F.unwarrantable; if(F.unwarrantable) F.types=new Set(['Condo']); syncChips(); render(); return; }
+    F.types.has(c.dataset.t)?F.types.delete(c.dataset.t):F.types.add(c.dataset.t);
+    if(!F.types.has('Condo')) F.unwarrantable=false;
+    syncChips(); render(); };
+  $('#fFirm').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; F.firms.has(c.dataset.fm)?F.firms.delete(c.dataset.fm):F.firms.add(c.dataset.fm); syncChips(); render(); };
+  $('#fCityChips').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; const cy=c.dataset.cy.toLowerCase(); F.cityQuery=(F.cityQuery===cy)?'':cy; $('#fCity').value=(F.cityQuery?c.dataset.cy:''); syncChips(); render(); };
+  $('#fStatus').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; F.status=c.dataset.st; syncChips(); render(); };
+  $('#fCity').oninput=()=>{ F.cityQuery=$('#fCity').value.trim().toLowerCase(); syncChips(); render(); };
+  $('#fPriceMin').oninput=()=>{ F.priceMin=$('#fPriceMin').value?+$('#fPriceMin').value:null; render(); };
+  $('#fPriceMax').oninput=()=>{ F.priceMax=$('#fPriceMax').value?+$('#fPriceMax').value:null; render(); };
+  $('#fCap').oninput=()=>{ F.capMin=+$('#fCap').value; $('#capVal').textContent=F.capMin?F.capMin+'%+':'any'; render(); };
+  $('#fUnits').oninput=()=>{ F.unitsMin=$('#fUnits').value?+$('#fUnits').value:null; render(); };
+  $('#fUnitsMax').oninput=()=>{ F.unitsMax=$('#fUnitsMax').value?+$('#fUnitsMax').value:null; render(); };
+  $('#fYear').oninput=()=>{ F.yearMin=$('#fYear').value?+$('#fYear').value:null; render(); };
+  $('#fYearMax').oninput=()=>{ F.yearMax=$('#fYearMax').value?+$('#fYearMax').value:null; render(); };
+  $('#fAfford').onchange=()=>{ F.afford=$('#fAfford').checked; render(); };
+  $('#fHasPhone').onchange=()=>{ F.hasPhone=$('#fHasPhone').checked; render(); };
+  $('#fHasEmail').onchange=()=>{ F.hasEmail=$('#fHasEmail').checked; render(); };
+  $('#scenario').onclick=e=>{ if(!e.target.dataset.s) return; setScenario(e.target.dataset.s); render(); };
+
+  // sort (drawer + top bar mirror each other)
+  $('#sort').onchange=()=>{ colSort=null; $('#sortTop').value=$('#sort').value; render(); };
+  $('#sortTop').onchange=()=>{ colSort=null; $('#sort').value=$('#sortTop').value; render(); };
+  $('#sortTop').value=$('#sort').value;
+  // density
+  const dv=String(Math.min(10,Math.max(1,+(localStorage.getItem('cre_density')||'3')||3))); $('#density').value=dv; $('#densVal').textContent=dv; document.documentElement.style.setProperty('--cols',dv); localStorage.setItem('cre_density',dv);
+  $('#density').oninput=()=>{ document.documentElement.style.setProperty('--cols',$('#density').value); $('#densVal').textContent=$('#density').value; localStorage.setItem('cre_density',$('#density').value); if($('#railDensity')){$('#railDensity').value=$('#density').value; $('#railDensVal').textContent=$('#density').value;} };
+
+  // ===== right-hand control rail (nav-agent skill surface) =====
+  wireCtrlRail(dv);
+
+  // field show/hide toggles + list/grid view
+  buildFieldToggles();
+  $('#fFields').onchange=e=>{ const cb=e.target.closest('input[data-fk]'); if(!cb) return; VIS[cb.dataset.fk]=cb.checked; saveVis(); buildFieldToggles(); render(); };
+  { const rf=$('#railFields'); if(rf) rf.onchange=e=>{ const cb=e.target.closest('input[data-fk]'); if(!cb) return; VIS[cb.dataset.fk]=cb.checked; saveVis(); buildFieldToggles(); render(); }; }
+  $('#fAll').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=true); saveVis(); buildFieldToggles(); render(); };
+  $('#fNone').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=false); saveVis(); buildFieldToggles(); render(); };
+  $('#fDef').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=!!f.def); saveVis(); buildFieldToggles(); render(); };
+  // ── panel drag-to-reorder columns (Fix 3) ──
+  // Dragging the ⠿ handle on a .ftog row reorders COLORDER then rebuilds toggles + re-renders list.
+  let pDragKey=null;
+  $('#fFields').addEventListener('dragstart',e=>{ const row=e.target.closest('.ftog'); if(!row) return;
+    if(!e.target.closest('.fdrag')){ e.preventDefault(); return; }  // only ⠿ handle starts a move
+    pDragKey=row.dataset.fk; e.dataTransfer.effectAllowed='move'; try{e.dataTransfer.setData('text/plain',pDragKey);}catch(_){}
+    row.classList.add('fdragging'); });
+  $('#fFields').addEventListener('dragover',e=>{ if(!pDragKey) return; const row=e.target.closest('.ftog'); if(!row||row.dataset.fk===pDragKey) return;
+    e.preventDefault(); e.dataTransfer.dropEffect='move';
+    const r=row.getBoundingClientRect(), after=(e.clientY-r.top)>r.height/2;
+    $('#fFields').querySelectorAll('.ftog').forEach(x=>x.classList.remove('fdropT','fdropB'));
+    row.classList.toggle('fdropB',after); row.classList.toggle('fdropT',!after); });
+  $('#fFields').addEventListener('dragleave',e=>{ const row=e.target.closest('.ftog'); if(row) row.classList.remove('fdropT','fdropB'); });
+  $('#fFields').addEventListener('drop',e=>{ if(!pDragKey) return; const row=e.target.closest('.ftog'); if(!row){ pDragKey=null; return; } e.preventDefault();
+    const tgt=row.dataset.fk;
+    if(tgt!==pDragKey){
+      const r=row.getBoundingClientRect(), after=(e.clientY-r.top)>r.height/2;
+      syncColOrder(); const from=COLORDER.indexOf(pDragKey);
+      if(from>=0){ COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,pDragKey);
+        saveColOrder(); lastColSig=''; buildFieldToggles(); render(); } }
+    pDragKey=null; });
+  $('#fFields').addEventListener('dragend',()=>{ $('#fFields').querySelectorAll('.fdragging,.fdropT,.fdropB').forEach(x=>x.classList.remove('fdragging','fdropT','fdropB')); pDragKey=null; });
+  $$('#viewseg button').forEach(b=>b.classList.toggle('active',b.dataset.view===view));
+  $('#viewseg').onclick=e=>{ const b=e.target.closest('button[data-view]'); if(!b) return; view=b.dataset.view; localStorage.setItem('cre_view',view); $$('#viewseg button').forEach(x=>x.classList.toggle('active',x.dataset.view===view)); render(); };
+
+  // chat
+  const EX=['Show only multifamily under $1M, sorted by cash-on-cash','Which deals actually cash-flow at 30% down?','Office and industrial in Pasadena','Take me to the Reseda mixed-use','Best in-budget deals'];
+  $('#examples').innerHTML = EX.map(x=>`<button class="ex">${x}</button>`).join('');
+  $('#examples').onclick=e=>{ if(e.target.classList.contains('ex')){ $('#q').value=e.target.textContent; $('#q').focus(); } };
+  $('#chatform').onsubmit=e=>{ e.preventDefault(); const t=$('#q').value.trim(); if(!t) return; $('#q').value=''; ask(t); };
+  $('#grid').onclick=e=>{ const cp=e.target.closest('[data-copy]'); if(cp){ copySummary(cp.dataset.copy, cp); return; } const qk=e.target.closest('[data-quick]'); if(qk){ applyQuick(qk.dataset.quick); return; } const more=e.target.closest('#showmore'); if(more){ _shown=Math.min(_shown+PAGE(), _rows.length); paintPage(); return; } const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); const sb=$('#shortbtn'); if(sb) sb.textContent='⭐ Shortlist ('+SHORT.size+')'; if(F.shortlist) render(); return; } const th=e.target.closest('.listtbl th[data-sortk]'); if(th){ const k=th.dataset.sortk; if(colSort&&colSort.key===k) colSort.dir*=-1; else colSort={key:k,dir:['type','firm','warr','broker','status','zip','addr','rec','phone','email','firmweb'].includes(k)?1:-1}; render(); return; } const b=e.target.closest('.lcbtn'); if(b){ fetchLiveComps(b.dataset.id); return; } const h=e.target.closest('.histbtn'); if(h){ fetchHistory(h); return; }
+    const cv=e.target.closest('[data-copyval]'); if(cv){ navigator.clipboard.writeText(cv.dataset.copyval).then(()=>toast('📋 copied')).catch(()=>toast('⚠ copy failed')); return; }
+    const ff=e.target.closest('[data-firmfilter]'); if(ff){ if(e.metaKey||e.ctrlKey||e.shiftKey) F.firms.add(ff.dataset.firmfilter); else F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
+    // drill atoms: card asset-type + city → filter the grid to that value (URL updates via render→stateToURL)
+    const tf=e.target.closest('[data-typefilter]'); if(tf){ if(e.metaKey||e.ctrlKey||e.shiftKey) F.types.add(tf.dataset.typefilter); else F.types=new Set([tf.dataset.typefilter]); syncChips(); render(); return; }
+    const cf=e.target.closest('[data-cityfilter]'); if(cf){ const cy=cf.dataset.cityfilter.toLowerCase(); F.cityQuery=(F.cityQuery===cy)?'':cy; if($('#fCity'))$('#fCity').value=(F.cityQuery?cf.dataset.cityfilter:''); syncChips(); render(); return; }
+    const ct=e.target.closest('[data-contact]'); if(ct){ openCompose(ct.dataset.contact); return; }
+    const mk=e.target.closest('[data-mark]'); if(mk){ markContacted(mk.dataset.mark); return; }
+    const si=e.target.closest('[data-saveinfo]'); if(si){ saveAgentInfo(si.dataset.saveinfo); return; }
+    const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(ld.dataset.letdel, ld.dataset.idx); return; }
+    // Row-click NO LONGER opens the agent pop-out (Steve 2026-08-18: "do not allow this to pop out, or
+    // move round. fix in place"). Contact info renders directly in the row; explicit links/buttons still work.
+  };
+  // ── drag-to-reorder PRIMARY List columns (Steve list-build rule 2) ──
+  // Delegated on #grid since the list table is re-rendered each render(). Property (addr) is a
+  // fixed anchor (no data-k) so it never participates as a drag source or drop target.
+  let idxDragKey=null;
+  $('#grid').addEventListener('dragstart',e=>{ const th=e.target.closest('.listtbl th.dragcol'); if(!th||!th.dataset.k) return;
+    if(e.target.closest && e.target.closest('.cr-handle')){ e.preventDefault(); return; }  // that grab is a resize, not a move
+    idxDragKey=th.dataset.k; e.dataTransfer.effectAllowed='move'; try{ e.dataTransfer.setData('text/plain',idxDragKey); }catch(_){}
+    th.classList.add('dragging'); });
+  $('#grid').addEventListener('dragover',e=>{ if(!idxDragKey) return; const th=e.target.closest('.listtbl th.dragcol'); if(!th||!th.dataset.k||th.dataset.k===idxDragKey) return;
+    e.preventDefault(); e.dataTransfer.dropEffect='move';
+    const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
+    th.classList.toggle('dropR',after); th.classList.toggle('dropL',!after); });
+  $('#grid').addEventListener('dragleave',e=>{ const th=e.target.closest('.listtbl th.dragcol'); if(th) th.classList.remove('dropL','dropR'); });
+  $('#grid').addEventListener('drop',e=>{ if(!idxDragKey) return; const th=e.target.closest('.listtbl th.dragcol'); if(!th||!th.dataset.k){ idxDragKey=null; return; }
+    e.preventDefault(); const tgt=th.dataset.k;
+    if(tgt!==idxDragKey){ const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
+      syncColOrder(); const from=COLORDER.indexOf(idxDragKey);
+      if(from>=0){ COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,idxDragKey);
+        saveColOrder(); lastColSig=''; render(); } }
+    idxDragKey=null; });
+  $('#grid').addEventListener('dragend',()=>{ document.querySelectorAll('.listtbl th.dragging,.listtbl th.dropL,.listtbl th.dropR').forEach(x=>x.classList.remove('dragging','dropL','dropR')); idxDragKey=null; });
+  $('#shortbtn').onclick=()=>{ F.shortlist=!F.shortlist; render(); };
+  $('#cmpbtn').onclick=openCompare;
+  $('#heat').value=heat; $('#heat').onchange=()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat',heat); render(); };
+  $('#cmpov').addEventListener('click',e=>{ if(e.target.id==='cmpov') e.currentTarget.classList.remove('on'); });
+  $('#cmpmodal').addEventListener('click',e=>{ if(e.target.closest('#cmpcsv')) exportCompareCSV(); else if(e.target.closest('#cmpprint')) printCompare(); });
+  window.addEventListener('afterprint',()=>document.body.classList.remove('printing-compare'));
+  // a11y: Escape closes any open overlay (compare / broker / property-history modals)
+  document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ ['cmpov','bkov'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('on'); }); } });
+
+  window.DATA_READY = true;
+  recompute(); syncChips(); render();
+}).catch(e=>{ $('#sub').textContent='Listings are still loading — refresh in a moment if this persists.'; console.warn('ranked.json load failed:',e); });
+</script>
+  <script src="/col-resize.js" defer></script>
+  <script src="/global-search.js" defer></script>
+<script src="/corner-nav.js" defer></script>
+<script>
+// Day/Night toggle — theme already applied before first paint by the head snippet.
+// crcp-theme key; Day (light) is the default. Only an explicit click persists.
+(function(){
+  var root=document.documentElement;
+  function label(t){
+    var b=document.getElementById('crcpThemeToggle'); if(!b) return;
+    b.innerHTML=(t==='light'?'☀️':'🌙')+' <span>'+(t==='light'?'Day':'Night')+'</span>';
+  }
+  function applyTheme(t){ root.setAttribute('data-theme',t); label(t); }
+  function mount(){
+    if(document.getElementById('crcpThemeToggle')){ label(root.getAttribute('data-theme')||'light'); return; }
+    var btn=document.createElement('button');
+    btn.id='crcpThemeToggle'; btn.type='button';
+    btn.title='Toggle day / night'; btn.setAttribute('aria-label','Toggle day / night theme');
+    btn.addEventListener('click',function(){
+      var nt=root.getAttribute('data-theme')==='light'?'dark':'light';
+      applyTheme(nt);
+      try{ localStorage.setItem('crcp-theme',nt); }catch(e){}
+    });
+    var host=document.querySelector('header .nav')||document.querySelector('header');
+    if(host){ host.appendChild(btn); } else { btn.style.cssText+=';position:fixed;top:12px;right:14px;z-index:99999'; document.body.appendChild(btn); }
+    label(root.getAttribute('data-theme')||'light');
+  }
+  if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',mount); else mount();
+})();
+</script>
+  <!-- nav-agent REMOVED (Steve 2026-08-18: "no more pop out … fix into the ui"): its floating
+       movable/removable panel overlay (.na-restorebar, position:fixed) popped out over the grid.
+       The native Fields show/hide/reorder panel (#fFields) is independent and stays. -->
+  <!-- <script src="/nav-agent/nav-agent.js" defer></script> -->
+
+  <!-- ── Controls-rail resize bar (Steve 2026-08-20): drag the #ctrlrail (Controls) panel wider ──
+       A fixed vertical grab-bar pinned to the rail's left edge via right:var(--railW), so it auto-
+       tracks the width and never scrolls with the rail content. Drag = resize, dbl-click = reset,
+       width persisted to localStorage. Hidden with the rail below 1100px. -->
+  <style>
+    .railrsz{ position:fixed; right:var(--railW,272px); top:57px; bottom:0; width:10px; margin-right:-5px; cursor:ew-resize; z-index:25; }
+    .railrsz::before{ content:''; position:absolute; left:4px; top:0; bottom:0; width:2px; background:transparent; transition:background .15s; }
+    .railrsz:hover::before, .railrsz.rsz-active::before{ background:var(--blue,#3b7dff); }
+    body.rsz-dragging{ cursor:ew-resize; user-select:none; }
+    @media (max-width:1100px){ .railrsz{ display:none; } }
+  </style>
+  <script>
+  (function(){
+    var rail=document.getElementById('ctrlrail'); if(!rail) return;
+    var KEY='crcp_railW';
+    try{ var s=parseInt(localStorage.getItem(KEY),10); if(s>=200&&s<=760) document.documentElement.style.setProperty('--railW', s+'px'); }catch(_){}
+    var h=document.createElement('div'); h.className='railrsz'; h.title='Drag to resize the Controls panel · double-click to reset'; document.body.appendChild(h);
+    var dragging=false;
+    function onMove(e){ if(!dragging) return; var w=Math.max(200, Math.min(760, window.innerWidth - e.clientX)); document.documentElement.style.setProperty('--railW', w+'px'); }
+    function onUp(){ if(!dragging) return; dragging=false; document.body.classList.remove('rsz-dragging'); h.classList.remove('rsz-active');
+      document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp);
+      try{ localStorage.setItem(KEY, Math.round(rail.getBoundingClientRect().width)); }catch(_){} }
+    h.addEventListener('mousedown',function(e){ e.preventDefault(); dragging=true; document.body.classList.add('rsz-dragging'); h.classList.add('rsz-active');
+      document.addEventListener('mousemove',onMove); document.addEventListener('mouseup',onUp); });
+    h.addEventListener('dblclick',function(){ document.documentElement.style.removeProperty('--railW'); try{ localStorage.removeItem(KEY); }catch(_){} });
+  })();
+  </script>
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
+<script src="/user-badge.js" defer></script><div id="__geo" style="position:fixed;bottom:0;left:0;z-index:2147483647;font:10px monospace;background:#000;color:#0f0;padding:2px">m</div><script>window.addEventListener("load",function(){setTimeout(function(){function r(s){var e=document.querySelector(s);if(!e)return s+":null";var b=e.getBoundingClientRect();return s+"={x:"+Math.round(b.x)+",right:"+Math.round(b.right)+",w:"+Math.round(b.width)+"}";}var burger=document.querySelector(".cnav-burger.left"),h1=document.querySelector(".htitle h1");var ov="";if(burger&&h1){var bb=burger.getBoundingClientRect(),hh=h1.getBoundingClientRect();ov="GAP="+Math.round(hh.left-bb.right)+" OVERLAP="+(bb.right>hh.left);}var padL=getComputedStyle(document.querySelector("header")).paddingLeft;document.getElementById("__geo").textContent="GEO "+r(".cnav-burger.left")+" | "+r(".htitle h1")+" | padL="+padL+" | "+ov;},1800);});</script></body>
+</html>
diff --git a/front-page-audit/screens/crcp/repro/real-index.html b/front-page-audit/screens/crcp/repro/real-index.html
new file mode 100644
index 0000000..ac12823
--- /dev/null
+++ b/front-page-audit/screens/crcp/repro/real-index.html
@@ -0,0 +1,2548 @@
+<!doctype html>
+<html lang="en">
+<head>
+<script>/* credential-safe fetch (2026-08-18): resolve EVERY relative fetch (with or without a leading slash) against a credential-free base so creds embedded in the page URL (http://user:pass@host/...) don't poison document.baseURI and throw "Request cannot be constructed from a URL that includes credentials". */(function(){var of=window.fetch,O=location.origin;window.fetch=function(u,o){try{if(typeof u==='string'){if(/^[a-z][a-z0-9+.-]*:/i.test(u)){var x=new URL(u);if(x.username||x.password){x.username='';x.password='';u=x.href;}}else{u=new URL(u,O+location.pathname+location.search).href;}}else if(u&&typeof u.url==='string'){var y=new URL(u.url,O+location.pathname+location.search);if(y.username||y.password){y.username='';y.password='';u=new Request(y.href,u);}}}catch(e){}return of.call(this,u,o);};try{if(location.href.indexOf('@')>=0){history.replaceState(null,'',location.pathname+location.search+location.hash);}}catch(e){}})();</script>
+<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='dark'?'dark':'light');}catch(e){document.documentElement.setAttribute('data-theme','light');}})();</script>
+<!-- Self-heal: nav-agent grid controls can persist a "removed"/"collapsed" state that hides the
+     whole grid with no obvious way back. Clear it before nav-agent runs so a grid can never stay hidden. -->
+<script>(function(){try{for(var i=localStorage.length-1;i>=0;i--){var k=localStorage.key(i);if(k&&k.indexOf('navagent:')===0){var v=localStorage.getItem(k);if(v&&v.indexOf('"removed":true')>=0){localStorage.removeItem(k);}}}}catch(e){}})();</script>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230e1116'/%3E%3Ctext x='8' y='12' font-size='11' text-anchor='middle' fill='%233fb950'%3E%24%3C/text%3E%3C/svg%3E">
+<title>LA County CRE — Investment Explorer</title>
+<style>
+  :root { --cols:3; --drawerW:316px; }
+  * { box-sizing:border-box; }
+  :focus-visible { outline:2px solid var(--blue); outline-offset:2px; border-radius:4px; }
+  @media (prefers-reduced-motion: reduce){ *,*::before,*::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; scroll-behavior:auto !important; } }
+  body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }
+
+  /* ---- top bar ---- */
+  header { display:flex; align-items:center; gap:14px; padding:12px 20px; border-bottom:1px solid var(--line); position:sticky; top:0; z-index:30; background:var(--headbg); backdrop-filter:blur(8px); }
+  .burger { display:inline-flex; align-items:center; gap:8px; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:9px 13px; font-size:15px; cursor:pointer; font-weight:600; }
+  .burger:hover { border-color:var(--blue); }
+  .fcount { display:none; background:var(--blue); color:#04122b; font-size:11px; font-weight:700; border-radius:20px; padding:1px 7px; }
+  .fcount.on { display:inline-block; }
+  .htitle h1 { margin:0; font-size:18px; letter-spacing:.2px; }
+  .htitle .sub { color:var(--mut); font-size:12px; }
+  header .spacer { margin-left:auto; }
+  /* nav-link cluster; on wide screens it sits inline right-aligned, on tablets/phones it becomes a swipeable strip (see @media 1024) */
+  .topnav { display:flex; align-items:center; flex-wrap:nowrap; }
+  .seg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .seg button { background:var(--card); color:var(--ink); border:0; border-right:1px solid var(--line); padding:8px 11px; font-size:12px; cursor:pointer; }
+  .seg button:last-child { border-right:0; }
+  .seg button.active { background:#1f6feb; color:#fff; }
+  #drawer .seg { display:flex; width:100%; }
+  #drawer .seg button { flex:1; text-align:center; padding:9px 6px; }
+
+  /* ---- left drawer ---- */
+  #scrim { position:fixed; inset:0; background:rgba(0,0,0,.5); opacity:0; pointer-events:none; transition:opacity .2s; z-index:38; }
+  #scrim.on { opacity:1; pointer-events:auto; }
+  #drawer { position:fixed; top:0; left:0; height:100vh; width:var(--drawerW); background:var(--bg2); border-right:1px solid var(--line); transform:translateX(-100%); transition:transform .22s ease; z-index:40; overflow-y:auto; padding:0 0 40px; }
+  #drawer.open { transform:translateX(0); box-shadow:6px 0 30px rgba(0,0,0,.4); }
+  .dhead { display:flex; align-items:center; justify-content:space-between; padding:16px 18px 10px; position:sticky; top:0; background:var(--bg2); border-bottom:1px solid var(--line); z-index:2; }
+  .dhead b { font-size:15px; } .dhead .x { background:none; border:0; color:var(--mut); font-size:18px; cursor:pointer; }
+  .fsec { padding:14px 18px; border-bottom:1px solid var(--line); }
+  .fsec h4 { margin:0 0 9px; font-size:11px; text-transform:uppercase; letter-spacing:.6px; color:var(--mut); }
+  .chips { display:flex; flex-wrap:wrap; gap:6px; }
+  .chip { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:18px; padding:5px 11px; font-size:12px; cursor:pointer; user-select:none; }
+  .chip:hover { color:var(--ink); }
+  .chip.active { border-color:var(--blue); color:var(--blue); background:rgba(88,166,255,.08); }
+  .chip .ct { color:var(--mut); font-size:10px; margin-left:4px; }
+  .chip.zero { opacity:.4; }
+  .row2 { display:flex; gap:8px; }
+  .fld { flex:1; }
+  .fld label { display:block; font-size:10px; color:var(--mut); margin-bottom:3px; }
+  input[type=text], input[type=number], select { width:100%; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:8px 10px; font-size:13px; }
+  input[type=range] { width:100%; accent-color:var(--blue); }
+  .rangeval { font-size:11px; color:var(--blue); float:right; }
+  .toggle { display:flex; align-items:center; gap:8px; font-size:13px; cursor:pointer; }
+  .resetbtn { width:calc(100% - 36px); margin:14px 18px 0; background:transparent; color:var(--bad); border:1px solid #7d2b28; border-radius:9px; padding:9px; font-size:13px; cursor:pointer; }
+
+  /* ---- results bar + grid ---- */
+  .bar { display:flex; flex-wrap:wrap; align-items:center; gap:14px; padding:11px 20px; border-bottom:1px solid var(--line); position:sticky; top:57px; background:var(--headbg); backdrop-filter:blur(6px); z-index:20; }
+  .bar .count { font-size:13px; color:var(--mut); } .bar .count b { color:var(--ink); }
+  .bar label { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.5px; }
+  .bar .right { margin-left:auto; display:flex; align-items:center; gap:12px; flex-wrap:wrap; justify-content:flex-end; min-width:0; }  /* wrap so the search/heat/sort controls can't overflow under the right-hand #ctrlrail when main is narrow */
+  .grid { display:grid; grid-template-columns:repeat(var(--cols),minmax(0,1fr)); gap:22px; padding:30px 32px 44px; }
+  @media (max-width:900px){ .grid{ grid-template-columns:repeat(2,minmax(0,1fr));} }
+  /* ---- iPad / tablet / narrow-desktop band (≤1024): keep header ONE fixed row so sticky offsets hold,
+         turn the nav-link cluster into a swipeable horizontal strip instead of clipping off-screen. ---- */
+  @media (max-width:1024px){
+    .grid{ grid-template-columns:repeat(2,minmax(0,1fr)); }  /* docked-drawer band: 2 cols so cards aren't crushed (iPad landscape) */
+    header{ flex-wrap:nowrap; gap:10px; }
+    header .spacer{ display:none; }                    /* nav no longer right-pushed; topnav flexes to fill */
+    .htitle{ min-width:0; flex:0 1 auto; }             /* let the title shrink instead of shoving nav off-screen */
+    .htitle h1{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+    .htitle .sub{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }  /* keep title block 2 lines → header stays ~57px so sticky bar/drawer offsets hold */
+    .topnav{ flex:1 1 auto; min-width:0; overflow-x:auto; overflow-y:hidden;
+             -webkit-overflow-scrolling:touch; scrollbar-width:none; }  /* iOS momentum-scroll, hidden scrollbar */
+    .topnav::-webkit-scrollbar{ display:none; }
+    .topnav > a, .topnav > .seg{ flex:0 0 auto; }      /* items keep intrinsic width; strip scrolls, nothing clips */
+    .topnav > a{ padding:8px 0; }                      /* taller touch target for finger taps */
+    html, body{ overflow-x:hidden; max-width:100%; }   /* no horizontal page scroll on iPad (html is the scroller) */
+    .bar{ max-width:100%; top:auto; }                   /* sort/density strip: no hairline overflow; un-stick so it can't tuck under the taller (burger+title) iPad header */
+  }
+  @media (max-width:620px){ .grid{ grid-template-columns:1fr;} }
+  .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:24px; display:flex; flex-direction:column; gap:10px; position:relative; transition:box-shadow .2s,border-color .2s; }
+  .card.uc { opacity:.62; }
+  .card.flash { border-color:var(--blue); box-shadow:0 0 0 3px rgba(88,166,255,.35); }
+  .top { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; }
+  .rank { font-size:12px; color:var(--mut); }
+  .score { font-size:30px; font-weight:700; line-height:1; }
+  .score small { font-size:11px; color:var(--mut); font-weight:400; display:block; text-align:right; }
+  .addr { font-weight:600; font-size:15px; }
+  .city { color:var(--mut); font-size:13px; }
+  .badges { display:flex; flex-wrap:wrap; gap:6px; }
+  .b { font-size:11px; padding:3px 8px; border-radius:20px; border:1px solid var(--line); color:var(--mut); }
+  .b.type { color:var(--blue); border-color:#284b7a; }
+  .b.deal-badge { font-weight:700; cursor:help; }
+  .b.ok { color:var(--acc); border-color:#1f6f37; }
+  .b.warn { color:var(--warn); border-color:#6b5320; }
+  .b.bad { color:var(--bad); border-color:#7d2b28; }
+  .b.rec-Buy { color:var(--acc); border-color:#1f6f37; font-weight:600; }
+  .b.rec-Watch { color:var(--warn); border-color:#6b5320; font-weight:600; }
+  .b.rec-Pass { color:var(--bad); border-color:#7d2b28; font-weight:600; }
+  .metrics { display:grid; grid-template-columns:1fr 1fr; gap:6px 14px; font-size:13px; border-top:1px solid var(--line); border-bottom:1px solid var(--line); padding:10px 0; }
+  .metrics div { display:flex; justify-content:space-between; }
+  .metrics .k { color:var(--mut); }
+  .metrics .v { font-variant-numeric:tabular-nums; font-weight:600; }
+  .pos { color:var(--acc); } .neg { color:var(--bad); } .na { color:var(--mut); }
+  .thesis { font-size:13px; color:var(--ink); }
+  .lst { font-size:12px; color:var(--mut); margin:2px 0 0; padding-left:16px; }
+  .src { font-size:12px; } .src a { color:var(--blue); text-decoration:none; }
+  .lcbtn,.histbtn { background:transparent; color:var(--warn); border:1px dashed #6b5320; border-radius:14px; padding:2px 8px; font-size:11px; cursor:pointer; }
+  .histbtn { color:var(--blue); border-color:#284b7a; }
+  .lcout,.histout { font-size:11px; color:var(--mut); margin-top:6px; }
+  .lcout a { color:var(--blue); text-decoration:none; }
+  .lcout .lc-item { padding:4px 0; border-top:1px solid var(--line); }
+  .small { font-size:11px; color:var(--mut); }
+  .statusline { font-size:12px; font-weight:600; font-variant-numeric:tabular-nums; padding:5px 9px; border-radius:8px; border:1px solid var(--line); background:rgba(255,255,255,.02); }
+  .statusline.ok { color:var(--acc); border-color:#1f6f37; }
+  .statusline.warn { color:var(--warn); border-color:#6b5320; }
+  .statusline.bad { color:var(--bad); border-color:#7d2b28; }
+  .statusline.mut { color:var(--mut); }
+  .statusline.none { color:var(--warn); border-color:#6b5320; border-style:dashed; }
+  .foot { color:var(--mut); font-size:12px; padding:0 20px 8px; }
+  .empty { padding:60px 20px; text-align:center; color:var(--mut); }
+
+  /* ---- Broker-history + prior-broker flag ---- */
+  .brokerflag { display:inline-flex; align-items:center; gap:5px; font-size:11px; padding:3px 9px; border-radius:20px; background:rgba(255,159,9,.10); color:#ff9f09; border:1px solid rgba(255,159,9,.35); font-weight:700; cursor:help; margin-top:2px; }
+  .txhist { font-size:11px; color:var(--mut); margin-top:4px; line-height:1.5; }
+  .txhist .tx-row { display:flex; gap:8px; align-items:baseline; padding:2px 0; border-bottom:1px dashed rgba(255,255,255,.05); }
+  .txhist .tx-row:last-child { border-bottom:0; }
+  .txhist .tx-date { color:var(--blue); font-variant-numeric:tabular-nums; min-width:72px; }
+  .txhist .tx-ev { flex:1; }
+  .txhist .tx-price { color:var(--acc); font-weight:600; font-variant-numeric:tabular-nums; }
+  .brokerline { font-size:11px; padding:4px 9px; background:rgba(88,166,255,.06); border-radius:7px; border:1px solid rgba(88,166,255,.15); color:#a5c8ff; margin-top:3px; }
+  .brokerline strong { color:var(--blue); }
+
+  /* ---- docked chat ---- */
+  #chatwrap { border-top:1px solid var(--line); background:var(--bg2); padding:16px 20px 40px; }
+  #chatwrap h2 { font-size:15px; margin:0 0 2px; }
+  #chatwrap .hint { color:var(--mut); font-size:12px; margin-bottom:10px; }
+  #log { display:flex; flex-direction:column; gap:10px; max-height:42vh; overflow-y:auto; padding:4px 2px; }
+  .msg { max-width:80%; padding:9px 13px; border-radius:12px; font-size:14px; line-height:1.45; white-space:pre-wrap; word-wrap:break-word; }
+  .msg.u { align-self:flex-end; background:#1f6feb; color:#fff; border-bottom-right-radius:3px; }
+  .msg.a { align-self:flex-start; background:var(--card); border:1px solid var(--line); border-bottom-left-radius:3px; }
+  .msg.a code { background:var(--bg); padding:1px 5px; border-radius:4px; }
+  .msg.sys { align-self:center; color:var(--mut); font-size:12px; background:transparent; }
+  .actiontag { display:inline-block; margin-top:6px; font-size:11px; color:var(--acc); border:1px solid #1f6f37; border-radius:6px; padding:2px 7px; }
+  #chatform { display:flex; gap:10px; margin-top:12px; }
+  #q { flex:1; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:11px 13px; font-size:14px; }
+  #send { background:#1f6feb; color:#fff; border:0; border-radius:10px; padding:0 20px; font-size:14px; font-weight:600; cursor:pointer; }
+  #send:disabled { opacity:.5; cursor:default; }
+  .examples { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
+  .ex { font-size:12px; color:var(--blue); border:1px dashed #284b7a; border-radius:16px; padding:4px 10px; cursor:pointer; background:transparent; }
+
+  /* ---- view toggle (grid/list) ---- */
+  .viewseg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .viewseg button { background:var(--card); color:var(--mut); border:0; border-right:1px solid var(--line); padding:7px 12px; font-size:12px; cursor:pointer; }
+  .viewseg button:last-child { border-right:0; }
+  .viewseg button.active { background:#1f6feb; color:#fff; }
+
+  /* ===== nav-agent inline views: compact List / KV / Table group headers ===== */
+  .compacthost, .kvhost { padding:20px 32px 44px; }
+  .compactlist { display:flex; flex-direction:column; gap:6px; }
+  .crow { display:flex; align-items:center; gap:12px; background:var(--card); border:1px solid var(--line); border-radius:9px; padding:8px 12px; }
+  .crow .starbtn { flex:0 0 auto; }
+  .crow .cid { flex:0 0 220px; min-width:180px; }
+  .crow .cla { display:block; font-weight:600; color:var(--ink); font-size:13.5px; }
+  .crow .clc { display:block; color:var(--mut); font-size:11px; }
+  .crow .cfields { flex:1; display:flex; flex-wrap:wrap; gap:6px 14px; align-items:center; min-width:0; }
+  .cchip { font-size:12px; color:var(--ink); white-space:nowrap; }
+  .cchip .cck { color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.3px; margin-right:3px; }
+  .kvgrid { display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:16px; }
+  .kvcard { background:var(--card); border:1px solid var(--line); border-radius:11px; padding:12px 14px; }
+  .kvhead { display:flex; align-items:center; gap:8px; margin-bottom:8px; padding-bottom:8px; border-bottom:1px solid var(--line); }
+  .kvaddr { font-weight:600; font-size:14px; color:var(--ink); }
+  .kvrow { display:flex; gap:10px; padding:3px 0; font-size:12.5px; border-bottom:1px solid rgba(127,127,127,.08); }
+  .kvrow:last-child { border-bottom:0; }
+  .kvk { flex:0 0 42%; color:var(--mut); text-transform:uppercase; font-size:10px; letter-spacing:.3px; padding-top:2px; }
+  .kvv { flex:1; color:var(--ink); min-width:0; word-break:break-word; }
+  .grouphead { padding:8px 12px; margin:10px 0 4px; background:rgba(88,166,255,.08); border-left:3px solid var(--blue); border-radius:4px; font-size:12px; color:var(--mut); text-transform:uppercase; letter-spacing:.4px; }
+  .grouphead b { color:var(--ink); }
+  .kvgrid .grouphead { grid-column:1/-1; }
+  tr.grouprow td.groupcell { background:rgba(88,166,255,.10); font-size:11px; text-transform:uppercase; letter-spacing:.4px; color:var(--mut); padding:7px 10px; }
+  tr.grouprow td.groupcell b { color:var(--ink); }
+
+  /* ===== right-hand control rail (in-flow, NOT floating) — nav-agent skill surface ===== */
+  #ctrlrail { flex:0 0 var(--railW,272px); width:var(--railW,272px); border-left:1px solid var(--line); background:var(--headbg); align-self:stretch; padding:12px 12px 40px; position:sticky; top:57px; max-height:calc(100vh - 57px); overflow-y:auto; }
+  #ctrlrail .railtitle { font-size:11px; text-transform:uppercase; letter-spacing:.6px; color:var(--mut); margin:2px 4px 12px; display:flex; align-items:center; gap:6px; }
+  .rpanel { border:1px solid var(--line); border-radius:9px; margin-bottom:8px; overflow:hidden; background:var(--card); }
+  .rpanel > .rphead { width:100%; display:flex; align-items:center; justify-content:space-between; gap:8px; background:transparent; color:var(--ink); border:0; padding:10px 12px; font-size:12.5px; font-weight:600; cursor:pointer; text-align:left; }
+  .rpanel > .rphead:hover { color:var(--blue); }
+  .rpanel > .rphead .caret { color:var(--mut); font-size:11px; transition:transform .15s; }
+  .rpanel.open > .rphead .caret { transform:rotate(90deg); color:var(--blue); }
+  .rpanel > .rpbody { display:none; padding:4px 12px 12px; }
+  .rpanel.open > .rpbody { display:block; }
+  .rpbody select, .rpbody input[type=text] { width:100%; background:var(--bg); color:var(--ink); border:1px solid var(--line); border-radius:7px; padding:7px 9px; font-size:12px; margin-top:6px; }
+  .rpbody .rlbl { font-size:11px; color:var(--mut); margin-top:8px; display:block; }
+  .rpbody .ftog { display:flex; width:100%; margin:0 0 4px; }
+  @media (max-width:1100px){ #ctrlrail { display:none; } }   /* on narrow screens the left drawer already carries these controls */
+  /* ---- field show/hide toggles: one CARD per field, click to toggle on/off ---- */
+  .fieldtoggles { display:flex; flex-wrap:wrap; gap:6px; max-height:360px; overflow-y:auto; padding:2px 4px 2px 0; }
+  .ftog { display:inline-flex; align-items:center; gap:5px; font-size:12px; color:var(--mut); cursor:pointer; padding:5px 9px;
+          border:1px solid var(--line); border-radius:9px; background:var(--card); user-select:none; transition:border-color .12s,color .12s,background .12s; }
+  .ftog input { display:none; }                                   /* card IS the toggle; checkbox drives state via :has() */
+  .ftog::before { content:'○'; font-size:11px; color:var(--mut); }
+  .ftog:hover { border-color:var(--blue); color:var(--ink); }
+  .ftog:has(input:checked) { border-color:var(--blue); color:var(--blue); background:rgba(88,166,255,.10); }
+  .ftog:has(input:checked)::before { content:'●'; color:var(--blue); }
+  .ftog .colbadge { color:var(--mut); font-size:10px; }
+  /* panel drag-to-reorder (Fix 3) */
+  .ftog .flbl { flex:1; }
+  .ftog .fdrag { cursor:grab; color:var(--mut); font-size:12px; opacity:.5; user-select:none; line-height:1; margin-right:2px; }
+  .ftog:hover .fdrag { opacity:1; }
+  .ftog.fdragging { opacity:.45; }
+  .ftog.fdropT { box-shadow:inset 0 3px 0 var(--gold); }
+  .ftog.fdropB { box-shadow:inset 0 -3px 0 var(--gold); }
+  .minibtn { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:7px; padding:4px 9px; font-size:11px; cursor:pointer; margin-right:5px; }
+  .minibtn:hover { border-color:var(--blue); color:var(--ink); }
+  /* ---- list view ---- */
+  .listhost { display:block; padding:20px 20px 30px; }
+  .listwrap { overflow:auto; border:1px solid var(--line); border-radius:12px; }
+  /* col-resize.js wraps the table in .cr-scroll (overflow:auto) — THAT is the th's nearest scroll
+     container, so bounding ITS height (not .listwrap's) is what makes the sticky header (thead
+     top:0) AND sticky first column (td.stick left:0) actually freeze while scrolling both axes.
+     --tbl-top = the sticky app-chrome height above the table (tune per layout). */
+  .cr-scroll { max-height:calc(100vh - var(--tbl-top, 178px)); }
+  /* max-content so columns size to their content and expand left/right (no artificial squish);
+     min-width:100% keeps it filling the pane when there are few columns; .listwrap scrolls-x. */
+  .listtbl { width:max-content; min-width:100%; border-collapse:collapse; font-size:12.5px; white-space:nowrap; }
+  .listtbl th, .listtbl td { text-align:right; padding:9px 13px; border-top:1px solid var(--line); }
+  .listtbl th { position:sticky; top:0; background:var(--bg2); color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; border-top:0; z-index:2; }
+  .listtbl td.stick, .listtbl th.stick { text-align:left; position:sticky; left:0; background:var(--card); z-index:1; }
+  .listtbl th.stick { background:var(--bg2); z-index:3; }  /* corner = header row + first col: highest */
+  .listtbl th.sortable { cursor:pointer; } .listtbl th.sortable:hover { color:var(--ink); }
+  /* sort arrow via CSS ::after (not literal text) so the header TEXT stays stable and col-resize's tableSig-keyed saved widths survive re-render (TK-10089) */
+  .listtbl th.asc::after { content:' ▲'; color:var(--gold); } .listtbl th.desc::after { content:' ▼'; color:var(--gold); }
+  /* draggable column reorder on the PRIMARY list table (Steve list-build rule) */
+  .listtbl th.dragcol { cursor:grab; }
+  .listtbl th.dragging { opacity:.45; }
+  .listtbl th.dropL { box-shadow:inset 3px 0 0 var(--blue); }
+  .listtbl th.dropR { box-shadow:inset -3px 0 0 var(--blue); }
+  .listtbl tr.lrow:hover td { background:rgba(88,166,255,.06); }
+  .listtbl tr.lrow:hover td.stick { background:var(--card); }
+  .listtbl .la { font-weight:600; color:var(--ink); text-decoration:none; }
+  .listtbl .la:hover { color:var(--blue); }
+  .listtbl .lc { color:var(--mut); font-size:11px; }
+  .listtbl .lsrc { margin-top:2px; }
+  .srclink { color:var(--blue); text-decoration:none; font-size:11px; font-weight:600; } .srclink:hover { text-decoration:underline; }
+  .listtbl .flink { color:var(--ink); text-decoration:none; border-bottom:1px dotted #3a4453; }
+  .listtbl .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  .listtbl tr.lrow { cursor:pointer; }
+  .listtbl .exptog { display:inline-block; width:11px; color:var(--mut); font-size:10px; transition:transform .15s; }
+  .listtbl tr.lrow.open td.stick { background:var(--card); }
+  .listtbl tr.lexp > td { padding:0; border-top:0; }
+  .listtbl tr.lexp .lexpbody { max-height:0; overflow:hidden; transition:max-height .25s ease; background:var(--bg); }
+  .listtbl tr.lexp.open .lexpbody { max-height:1200px; padding:12px 16px; border-top:1px solid var(--line); }
+  .lexpbody .src { text-align:left; white-space:normal; font-size:12px; }
+  .lexpbody .deep { text-align:left; white-space:normal; margin-top:8px; display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
+  .lexpbody .deeplbl { color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; margin-right:2px; }
+  .lexpbody .deep a, .lexpbody .deep button { font-size:11px; padding:3px 9px; border-radius:14px; border:1px solid var(--line); background:var(--card); color:var(--ink); text-decoration:none; cursor:pointer; }
+  .lexpbody .deep a:hover, .lexpbody .deep button:hover { border-color:var(--blue); color:var(--blue); }
+  .lexpbody .lcout, .lexpbody .histout { text-align:left; white-space:normal; }
+  /* Agent · contact column cell */
+  .listtbl td .agentcell { text-align:left; white-space:normal; min-width:170px; display:flex; flex-direction:column; gap:3px; }
+  .agentcell .amut { color:var(--mut); font-style:italic; font-size:11px; }
+  .agentcell .ainrow { display:flex; flex-wrap:wrap; gap:8px; }
+  .agentcell .ain { color:var(--mut); text-decoration:none; font-size:11px; }
+  .agentcell .ain:hover { color:var(--blue); }
+  .ain a { color:var(--blue); text-decoration:none; } .ain a:hover { text-decoration:underline; }
+  .tierb { font-size:9px; text-transform:uppercase; letter-spacing:.4px; color:var(--mut); border:1px solid var(--line); border-radius:4px; padding:0 4px; vertical-align:middle; }
+  .pocketbadge { font-size:11px; font-weight:600; color:var(--gold,#d29922); background:rgba(210,153,34,.12); border:1px solid rgba(210,153,34,.4); border-radius:5px; padding:1px 7px; white-space:nowrap; }
+  .agentcell .chiprow:empty { display:none; }
+  .agentcell .achip { display:inline-block; font-size:10px; color:var(--mut); background:var(--card); border:1px solid var(--line); border-radius:10px; padding:1px 6px; margin-right:4px; }
+  .agentcell .achip.done { color:#3fb950; border-color:#264f2e; }
+  .agentcell .acts { display:flex; flex-wrap:wrap; gap:5px; margin-top:2px; }
+  .abtn { font-size:10.5px; padding:2px 8px; border-radius:12px; border:1px solid var(--line); background:var(--card); color:var(--ink); cursor:pointer; }
+  .abtn:hover { border-color:var(--blue); color:var(--blue); }
+  .abtn.contactbtn { border-color:#2f5233; color:#8fdfa0; }
+  .abtn.markbtn { border-color:#284b7a; color:#8ec0ff; }
+  /* CRM block inside the inline panel */
+  .lexpbody .crm { margin-top:10px; border-top:1px solid var(--line); padding-top:10px; text-align:left; }
+  .crm-info { display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
+  .crm-info .crmf { background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12px; padding:5px 8px; min-width:150px; }
+  .crm-info .crmf:focus { outline:none; border-color:var(--blue); }
+  .crm-acts { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-top:8px; }
+  .crm-letters { margin-top:10px; }
+  .crm-letters .lett { border:1px solid var(--line); border-radius:8px; padding:7px 9px; margin-top:6px; background:var(--bg); }
+  .crm-letters .letth { display:flex; align-items:center; gap:8px; font-size:12px; }
+  .crm-letters .letts { color:var(--mut); font-size:10px; }
+  .crm-letters .letdel { margin-left:auto; background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:12px; }
+  .crm-letters .letdel:hover { color:var(--bad,#f85149); }
+  .crm-letters .lettb { white-space:pre-wrap; font-size:11.5px; color:var(--mut); margin-top:4px; }
+  /* Compose-letter modal */
+  .composeM { position:fixed; inset:0; background:rgba(0,0,0,.55); display:none; align-items:center; justify-content:center; z-index:9000; }
+  .composeM.on { display:flex; }
+  .composeCard { background:var(--card); border:1px solid var(--line); border-radius:14px; width:min(620px,92vw); max-height:88vh; overflow:auto; padding:16px; box-shadow:0 12px 40px rgba(0,0,0,.5); }
+  .composeH { display:flex; align-items:center; font-size:14px; margin-bottom:8px; }
+  .composeH .composeX { margin-left:auto; background:transparent; border:0; color:var(--mut); font-size:16px; cursor:pointer; }
+  .composeGate { font-size:11px; color:#e3b341; background:#241d0e; border:1px solid #5a4a1e; border-radius:8px; padding:6px 9px; margin-bottom:10px; }
+  .composeCard input, .composeCard textarea { width:100%; box-sizing:border-box; background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:13px; padding:8px 10px; margin-bottom:8px; font-family:inherit; }
+  .composeCard textarea { resize:vertical; line-height:1.5; }
+  .composeActs { display:flex; gap:8px; justify-content:flex-end; }
+  .composeActs button { font-size:12px; padding:6px 14px; border-radius:10px; border:1px solid var(--line); background:var(--card); color:var(--ink); cursor:pointer; }
+  .composeActs button.primary { border-color:#2f5233; color:#8fdfa0; }
+  .composeActs button.gated { border-color:#5a4a1e; color:#e3b341; }
+  .composeActs button.gated:hover:not([disabled]) { border-color:#e3b341; }
+  .composeActs button[disabled] { opacity:.4; cursor:not-allowed; }
+  /* Tabbed agent window */
+  .composeCard.tabwin { width:min(680px,94vw); }
+  .composeH .winmeta { color:var(--mut); font-size:11px; font-weight:400; margin-left:10px; }
+  .tabbar { display:flex; gap:2px; border-bottom:1px solid var(--line); margin:4px 0 12px; flex-wrap:wrap; }
+  .tabt { background:transparent; border:0; border-bottom:2px solid transparent; color:var(--mut); font-size:12.5px; padding:7px 12px; cursor:pointer; }
+  .tabt:hover { color:var(--ink); }
+  .tabt.active { color:var(--ink); border-bottom-color:var(--blue); font-weight:600; }
+  .tabpane { display:none; }
+  .tabpane.active { display:block; }
+  .tabmut { color:var(--mut); font-size:12px; font-style:italic; padding:8px 2px; }
+  .cm-row { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:10px; }
+  .tabpane .ainrow { display:flex; flex-wrap:wrap; gap:14px; margin:8px 0; }
+  .tabpane .ain { color:var(--mut); text-decoration:none; font-size:12px; }
+  .tabpane .ain:hover { color:var(--blue); }
+  .findgrid { display:flex; flex-direction:column; gap:7px; }
+  .findlink { display:inline-block; background:var(--card); border:1px solid var(--line); border-radius:8px; color:var(--ink); text-decoration:none; font-size:12.5px; padding:8px 11px; cursor:pointer; text-align:left; }
+  .findlink:hover { border-color:var(--blue); color:var(--blue); }
+  .findlink.findloop { padding:8px 11px; }
+  .findlink.findloop a { color:inherit; text-decoration:none; }
+  .crmToast { position:fixed; bottom:22px; left:50%; transform:translateX(-50%) translateY(12px); background:var(--bg2); border:1px solid var(--line); color:var(--ink); font-size:12.5px; padding:9px 16px; border-radius:20px; opacity:0; transition:opacity .2s,transform .2s; z-index:9500; pointer-events:none; }
+  .crmToast.on { opacity:1; transform:translateX(-50%) translateY(0); }
+  /* Card-view agent-contact block (every card, every asset type) */
+  .card .cardagent { margin-top:8px; border-top:1px solid var(--line); padding-top:8px; font-size:12px; }
+  .card .cardagent .ca-top { display:flex; flex-wrap:wrap; align-items:center; gap:6px; }
+  .card .cardagent .ainrow { display:flex; flex-wrap:wrap; gap:10px; margin-top:3px; }
+  .card .cardagent .ain { color:var(--mut); text-decoration:none; font-size:11px; }
+  .card .cardagent .ain:hover { color:var(--blue); }
+  .card .cardagent .acts { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
+  /* Full contact modal info row */
+  .composeCard .cm-info { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-bottom:10px; }
+  .composeCard .cm-info .crmf { background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12px; padding:6px 8px; min-width:140px; flex:1 1 140px; margin:0; }
+  .composeCard .cm-info .crmf:focus { outline:none; border-color:var(--blue); }
+  .composeH .flink { font-size:11px; font-weight:400; margin-left:8px; }
+  /* Inline (no-outside-link) agent bits */
+  .aname { font-weight:600; color:var(--ink); }
+  a.aname { text-decoration:none; cursor:pointer; } a.aname:hover { color:var(--blue); text-decoration:underline; }
+  .flink { cursor:pointer; }
+  .listtbl td .flink { border-bottom:1px dotted #3a4453; }
+  .listtbl td .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  /* ── drill atom: every displayed data point filters to its deeper view ── */
+  .drill { cursor:pointer; border-bottom:1px dotted transparent; }
+  .drill:hover { border-bottom-color:currentColor; filter:brightness(1.15); }
+  .badges .b.drill:hover { border-bottom-color:currentColor; }
+  .city .drill { color:inherit; }
+  .city .drill:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  .copyb { background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:11px; padding:0 3px; border-radius:4px; }
+  .copyb:hover { color:var(--blue); }
+  .crm-note, .lexpbody .crm-note { margin-top:6px; font-size:12px; color:var(--mut); white-space:pre-wrap; background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:6px 9px; text-align:left; }
+  .composeCard .cm-notes { margin-bottom:10px; }
+  .composeCard .cm-notes .deeplbl { margin-bottom:4px; }
+  .composeCard .cm-notes textarea { width:100%; box-sizing:border-box; background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12.5px; padding:7px 9px; font-family:inherit; resize:vertical; line-height:1.5; }
+  .composeCard .cm-notes textarea:focus { outline:none; border-color:var(--blue); }
+  /* ---- Amazon-style docked left filter rail (desktop) ---- */
+  .shell { display:flex; align-items:flex-start; }
+  main { flex:1 1 auto; min-width:0; }
+  @media (min-width:901px){
+    #burger { display:none; }
+    #scrim { display:none !important; }
+    #drawer { position:sticky; top:57px; height:calc(100vh - 57px); transform:none; flex:0 0 var(--drawerW); box-shadow:none; }
+    #drawer .dhead .x { display:none; }
+  }
+  /* collapsible filter tabs */
+  .fsec > h4 { cursor:pointer; display:flex; align-items:center; justify-content:space-between; user-select:none; }
+  .fsec > h4::after { content:'▾'; color:var(--mut); font-size:11px; transition:transform .15s; margin-left:8px; }
+  .fsec.collapsed > h4::after { transform:rotate(-90deg); }
+  .fsec.collapsed > *:not(h4) { display:none !important; }
+  .assessline { border-top:1px dashed rgba(255,255,255,.06); padding-top:6px; margin-top:2px; }
+  /* active-filter summary bar */
+  .activebar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:10px 20px; border-bottom:1px solid var(--line); background:rgba(88,166,255,.03); }
+  .activebar:empty { display:none; }
+  .activebar .lbl { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.5px; }
+  .afchip { display:inline-flex; align-items:center; gap:6px; background:rgba(88,166,255,.10); border:1px solid rgba(88,166,255,.4); color:var(--blue); border-radius:16px; padding:4px 10px; font-size:12px; cursor:pointer; }
+  .afchip:hover { background:rgba(88,166,255,.2); } .afchip .xx { opacity:.65; font-weight:700; }
+  .afreset { background:transparent; border:1px solid #7d2b28; color:var(--bad); border-radius:14px; padding:4px 11px; font-size:12px; cursor:pointer; margin-left:auto; }
+  .afreset:hover { background:rgba(248,81,73,.1); }
+  /* shortlist stars */
+  .starbtn { background:none; border:0; color:var(--mut); font-size:18px; line-height:1; cursor:pointer; padding:0; }
+  .starbtn:hover, .starbtn.on { color:var(--warn); }
+  .copybtn { background:none; border:0; color:var(--mut); font-size:14px; line-height:1; cursor:pointer; padding:0; }
+  .copybtn:hover { color:var(--blue); }
+  .listtbl .starbtn { font-size:14px; margin-right:5px; vertical-align:middle; }
+  #shortbtn.shorton { background:rgba(210,153,34,.14); border-color:var(--warn); color:var(--warn); }
+  /* aggregate stats strip */
+  .statsbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:9px 20px; border-bottom:1px solid var(--line); }
+  .statsbar:empty { display:none; }
+  .statpill { font-size:12px; color:var(--ink); background:var(--card); border:1px solid var(--line); border-radius:8px; padding:4px 10px; }
+  .statpill .sl { color:var(--mut); } .statpill b { font-variant-numeric:tabular-nums; }
+  .heatlegend { display:none; align-items:center; gap:5px; font-size:11px; color:var(--mut); }
+  .heatlegend.on { display:inline-flex; }
+  .heatlegend .bar { width:56px; height:8px; border-radius:4px; background:linear-gradient(90deg,hsl(0,70%,45%),hsl(60,70%,45%),hsl(120,70%,45%)); }
+  .showmore-wrap { grid-column:1/-1; text-align:center; padding:20px 0 6px; }
+  .showmore { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:10px; padding:9px 20px; font-size:13px; cursor:pointer; }
+  .showmore:hover { border-color:var(--blue); }
+  .quickstart { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin:16px 0; }
+  .qchip { background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:18px; padding:7px 14px; font-size:13px; cursor:pointer; }
+  .qchip:hover { border-color:var(--blue); color:var(--blue); }
+  .savedrow { display:flex; gap:6px; align-items:center; margin-bottom:5px; }
+  .savedname { flex:1; text-align:left; background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:8px; padding:6px 9px; font-size:12px; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+  .savedname:hover { border-color:var(--blue); color:var(--blue); }
+  .saveddel { background:none; border:0; color:var(--mut); cursor:pointer; font-size:13px; }
+  .saveddel:hover { color:var(--bad); }
+  #savebtn { width:100%; background:transparent; border:1px dashed #284b7a; color:var(--blue); border-radius:8px; padding:7px; font-size:12px; cursor:pointer; margin-top:4px; }
+  .savedempty { color:var(--mut); font-size:11px; }
+  /* compare overlay */
+  #cmpov { display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:60; align-items:flex-start; justify-content:center; padding:40px 14px; overflow:auto; }
+  #cmpov.on { display:flex; }
+  #cmpmodal { width:min(1100px,97vw); background:var(--card); border:1px solid var(--line); border-radius:15px; padding:20px 22px; position:relative; }
+  .cmpx { position:absolute; top:12px; right:14px; background:none; border:0; color:var(--mut); font-size:22px; cursor:pointer; }
+  .cmptbl { width:100%; border-collapse:collapse; font-size:13px; }
+  .cmptbl th, .cmptbl td { padding:8px 11px; border-top:1px solid var(--line); text-align:right; white-space:nowrap; }
+  .cmptbl thead th { position:sticky; top:0; background:var(--card); text-align:right; vertical-align:bottom; color:var(--ink); }
+  .cmptbl .cmpk, .cmptbl thead th.cmpk { text-align:left; color:var(--mut); position:sticky; left:0; background:var(--card); }
+  .cmptbl thead th .cmpsub { color:var(--mut); font-size:11px; font-weight:400; }
+  .cmphint { color:var(--mut); padding:24px 4px; }
+  .cmptools { display:flex; gap:8px; justify-content:flex-end; margin-bottom:10px; }
+  .cmptools button { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:8px; padding:6px 12px; font-size:12px; cursor:pointer; }
+  .cmptools button:hover { border-color:var(--blue); }
+  @media print {
+    body.printing-compare * { visibility:hidden; }
+    body.printing-compare #cmpov, body.printing-compare #cmpov * { visibility:visible; }
+    body.printing-compare #cmpov { position:absolute; inset:0; background:#fff; display:block; padding:0; overflow:visible; }
+    body.printing-compare #cmpmodal { width:100%; box-shadow:none; border:0; background:#fff; color:#000; }
+    body.printing-compare #cmpmodal h3 { color:#000; }
+    body.printing-compare .cmpx, body.printing-compare .no-print { display:none !important; }
+    body.printing-compare .cmptbl th, body.printing-compare .cmptbl td { color:#000; border-color:#999; }
+    body.printing-compare .cmptbl thead th, body.printing-compare .cmptbl .cmpk { background:#fff; color:#000; }
+    body.printing-compare .cmpsub { color:#444; }
+  }
+</style>
+<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 removed (no floating pop-out) -->
+  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
+<script src="/crcp-cities.js"></script><script src="/crcp-scope.js"></script></head>
+<body>
+<header>
+  <button class="burger" id="burger" aria-label="Open filters">☰ Filters <span class="fcount" id="fcount">0</span></button>
+  <div class="htitle">
+    <h1>LA County CRE — Investment Explorer</h1>
+    <div class="sub" id="sub">loading…</div>
+  </div>
+  <div class="spacer"></div>
+  <div class="topnav">
+  <a href="/crcp.html" style="color:var(--warn);text-decoration:none;font-size:13px;margin-right:12px" title="Commercial Real Estate Control Panel (live)">🛰️ CRCP</a>
+  <a href="/map.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All properties on a map, colored by type, with overhead aerial photos">🗺️ Map</a>
+  <a href="/mls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Spreadsheet-style listings + grid — all property data, sortable & searchable">📋 Listings</a>
+  <a href="/valley-pools.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="San Fernando Valley single-family pool homes under $1.2M — deal-colored map + $/sqft + price-cut signals + payment estimate">🏊 Valley Pools</a>
+  <a href="/deals.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Liquid-glass deal-intelligence charts">◆ Deals</a>
+  <a href="/deals-flow.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Closed deal flow — public-record sales + FHA + SEC REIT, source-badged">💰 Deal Flow</a>
+  <a href="/rent-rolls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Rent-roll → DSCR underwriting">📑 Rent Rolls</a>
+  <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All brokers/agents — grid + list, sortable by every field">👥 Broker grid</a>
+  <a href="/brokers.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent relationship mind-map">🕸️ Broker map</a>
+  <a href="/graphics.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / firm / warrantability charts">📊 Graphics</a>
+  <a href="/condos.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LA condos — FHA warrantability">🏢 Condos</a>
+  <a href="/fha-loans.html" style="color:var(--gold,#e9c46a);text-decoration:none;font-size:13px;margin-right:12px" title="HUD/FHA multifamily loans originally ≤3.5% — historic-low-rate flagged">◆ FHA Loans</a>
+  <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent contacts — name, company, phone, email">👤 Brokers</a>
+  <a href="/linkedin.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LinkedIn follow-list">🔗 LinkedIn</a>
+  <a href="/licensed.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Licensed directory — RE agents, firms, title & escrow + firm IG">📋 Licensed</a>
+  </div><!-- /.topnav -->
+</header>
+
+<!-- Real natural-language chat bar at the TOP — "4 units in los angeles" → drives filters + re-renders (client-side, no server) -->
+<div id="chatbar" style="padding:9px 18px 10px;border-bottom:1px solid var(--line);background:var(--bg2,var(--card));position:sticky;top:57px;z-index:20;">
+  <div style="display:flex;align-items:center;gap:9px;max-width:860px;margin:0 auto;width:100%;">
+    <span style="font-size:16px" title="Ask for properties in plain English">💬</span>
+    <input id="chatq" type="text" autocomplete="off" spellcheck="false"
+      placeholder="Ask for what you want — e.g.  “4 units in Los Angeles”  ·  “multifamily under $2M”  ·  “retail 6% cap”  ·  “all cash”  (Enter)"
+      style="flex:1;min-width:0;background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:9px 13px;font-size:14px;">
+    <button id="chatgo" class="ex" title="Apply this request">Ask ▸</button>
+    <button id="chatclear" class="ex" title="Clear the chat filters">Clear</button>
+    <span id="chatmsg" style="font-size:12px;color:var(--mut);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:22%"></span>
+  </div>
+  <div id="chatex" style="display:flex;flex-wrap:wrap;gap:6px;margin:7px auto 0;max-width:860px;justify-content:center;font-size:11px;color:var(--mut);align-items:center;">
+    <span style="opacity:.7">Try:</span>
+    <button class="chatchip" data-q="4 units in Los Angeles">4 units in Los Angeles</button>
+    <button class="chatchip" data-q="unwarrantable condos">unwarrantable condos</button>
+    <button class="chatchip" data-q="multifamily under $2M">multifamily under $2M</button>
+    <button class="chatchip" data-q="retail 6% cap">retail 6% cap</button>
+    <button class="chatchip" data-q="office in Pasadena">office in Pasadena</button>
+    <button class="chatchip" data-q="non-QM condos">non-QM condos</button>
+    <button class="chatchip" data-q="built after 2000">built after 2000</button>
+    <button class="chatchip" data-q="all cash">all cash</button>
+  </div>
+</div>
+<style>.chatchip{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:20px;padding:3px 11px;font-size:11px;cursor:pointer;transition:all .12s;} .chatchip:hover{border-color:var(--blue,#3b82f6);color:var(--blue,#3b82f6);}</style>
+
+<div id="scrim"></div>
+<div class="shell">
+<aside id="drawer">
+  <div class="dhead"><b>Filters</b><button class="x" id="dclose" aria-label="Close">✕</button></div>
+
+  <div class="fsec"><h4>Financing scenario</h4>
+    <span class="seg" id="scenario">
+      <button data-s="25" class="active">25% down</button>
+      <button data-s="30">30% down</button>
+      <button data-s="cash">All cash</button>
+    </span>
+    <div class="small" style="margin-top:6px">Underwriting lens — drives cash-on-cash, DSCR, cash-needed &amp; Deal Score across the view.</div>
+  </div>
+  <div class="fsec" id="fieldsSec"><h4>Fields — show / hide · reorder</h4>
+    <div class="fieldtoggles" id="fFields"></div>
+    <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fDef">Default</button><button class="minibtn" id="idxReset" title="Reset List columns to the tight default — order, widths &amp; visibility">↺ Reset layout</button></div>
+    <div class="small" style="margin-top:6px">Drag a row by its ⠿ handle to reorder columns. ▤ = also a column in List view · drag a List header to reorder · drag its right edge to resize</div>
+  </div>
+  <div class="fsec"><h4>Saved searches</h4><div id="savedList"></div><button id="savebtn">💾 Save current view</button></div>
+  <div class="fsec"><h4>Asset type</h4><div class="chips" id="fType"></div></div>
+  <div class="fsec"><h4>Listing firm</h4><div class="chips" id="fFirm"></div></div>
+  <div class="fsec"><h4>Submarket / city</h4>
+    <input type="text" id="fCity" placeholder="search city or street… e.g. Encino, Ventura Blvd">
+    <div class="chips" id="fCityChips" style="margin-top:8px"></div>
+  </div>
+  <div class="fsec"><h4>Price</h4>
+    <div class="row2"><div class="fld"><label>Min $</label><input type="number" id="fPriceMin" placeholder="any" step="50000"></div>
+    <div class="fld"><label>Max $</label><input type="number" id="fPriceMax" placeholder="any" step="50000"></div></div>
+  </div>
+  <div class="fsec"><h4>Cap rate <span class="rangeval" id="capVal">any</span></h4>
+    <input type="range" id="fCap" min="0" max="10" step="0.25" value="0">
+    <div class="small">minimum broker cap rate (0 = any)</div>
+  </div>
+  <div class="fsec"><h4>Units / size</h4>
+    <div class="row2"><div class="fld"><label>Min units</label><input type="number" id="fUnits" placeholder="any" min="0" step="1"></div>
+    <div class="fld"><label>Max units</label><input type="number" id="fUnitsMax" placeholder="any" min="0" step="1"></div></div>
+    <div class="row2" style="margin-top:8px"><div class="fld"><label>Built after</label><input type="number" id="fYear" placeholder="any" min="1900" max="2026" step="1"></div>
+    <div class="fld"><label>Built before</label><input type="number" id="fYearMax" placeholder="any" min="1900" max="2026" step="1"></div></div>
+  </div>
+  <div class="fsec"><h4>Status &amp; budget</h4>
+    <div class="chips" id="fStatus">
+      <span class="chip" data-st="all">All</span>
+      <span class="chip" data-st="active">Active only</span>
+    </div>
+    <label class="toggle" style="margin-top:10px"><input type="checkbox" id="fAfford"> In budget ($400k down) only</label>
+    <!-- Broker/firm contactability filters (Steve 2026-08-18): narrow to listings whose resolved broker/firm contact has a phone / email — so a reachable contact exists before working the lead. -->
+    <label class="toggle" style="margin-top:10px"><input type="checkbox" id="fHasPhone"> Has broker phone</label>
+    <label class="toggle" style="margin-top:6px"><input type="checkbox" id="fHasEmail"> Has broker email</label>
+  </div>
+  <div class="fsec"><h4>Sort &amp; density</h4>
+    <select id="sort">
+      <option value="deal">Deal Score ↓</option>
+      <option value="composite">Best Investment (blended)</option>
+      <option value="coc">Cash-on-Cash ↓</option>
+      <option value="yield">Rent Yield (rent÷price) ↓</option>
+      <option value="cap">Cap Rate ↓</option>
+      <option value="dscr">DSCR ↓</option>
+      <option value="priceAsc">Price ↑</option>
+      <option value="priceDesc">Price ↓</option>
+      <option value="cashNeeded">Cash Needed ↑</option>
+      <option value="unitsDesc">Units ↓</option>
+      <option value="rentRoi">Pro-forma Rent ROI ↓</option>
+    </select>
+    <div style="margin-top:10px"><label class="small">Cards per row (grid) <span class="rangeval" id="densVal">3</span></label>
+      <input type="range" id="density" min="1" max="10" step="1" value="3"></div>
+  </div>
+  <button class="resetbtn" id="reset">Reset all filters</button>
+</aside>
+
+<main>
+  <!-- ===== Brokers & agents — front-page contacts (name / company / phone / email) ===== -->
+  <style>
+    #brokers{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:14px 16px;margin-bottom:18px;}
+    #brokers h2{margin:0 0 2px;font-size:14px;text-transform:uppercase;letter-spacing:.6px;color:var(--warn);}
+    #brokers .bksub{color:var(--mut);font-size:12px;margin-bottom:12px;}
+    #bktbl{width:100%;border-collapse:collapse;font-size:13px;}
+    #bktbl td,#bktbl th{text-align:left;padding:7px 8px;border-top:1px solid var(--line);}
+    #bktbl th{color:var(--mut);font-size:11px;text-transform:uppercase;letter-spacing:.4px;border-top:0;}
+    #bktbl tr.bkrow{cursor:pointer;} #bktbl tr.bkrow:hover td{background:rgba(88,166,255,.06);}
+    #bktbl a{color:var(--blue);text-decoration:none;} #bktbl .miss{color:#6e7681;}
+    .bkatype{display:inline-block;font-size:10px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;padding:1px 6px;border-radius:9px;margin-left:6px;}
+    .bkatype.res{background:rgba(88,166,255,.16);color:var(--blue);} .bkatype.com{background:rgba(210,153,34,.16);color:var(--warn);}
+    #bkov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:60;align-items:flex-start;justify-content:center;padding:40px 14px;overflow-y:auto;}
+    #bkov.on{display:flex;}
+    #bkmodal{width:min(720px,96vw);background:var(--card);border:1px solid var(--line);border-radius:15px;padding:20px 22px;position:relative;}
+    #bkmodal h3{margin:0 0 2px;font-size:20px;} #bkmodal .mfirm{color:var(--warn);font-size:13px;}
+    #bkmodal .x{position:absolute;top:12px;right:14px;background:none;border:0;color:var(--mut);font-size:22px;cursor:pointer;}
+    #bkmodal .ci{display:flex;gap:16px;flex-wrap:wrap;margin:10px 0;font-size:14px;} #bkmodal .ci a{color:var(--blue);text-decoration:none;}
+    #bkmodal .sec{margin-top:14px;} #bkmodal .sec h4{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut);}
+    #bkmodal .litem{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid var(--line);font-size:13px;} #bkmodal .litem .m{color:var(--mut);}
+    .bkbtn{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:7px 12px;font-size:13px;cursor:pointer;margin-right:8px;} .bkbtn:hover{border-color:var(--blue);}
+    .gate{font-size:12px;color:var(--mut);margin-top:8px;}
+    .lead{border:1px solid var(--line);border-radius:11px;margin-bottom:8px;overflow:hidden;background:rgba(255,255,255,.012);}
+    .lead .lhead{display:flex;align-items:center;gap:12px;padding:11px 14px;cursor:pointer;}
+    .lead .lhead:hover{background:rgba(88,166,255,.06);}
+    .lead .rk{font-weight:700;color:var(--warn);width:24px;text-align:center;font-variant-numeric:tabular-nums;}
+    .lead .who{flex:1;min-width:0;} .lead .who .nm{font-weight:600;} .lead .who .fm{color:var(--mut);font-size:12px;}
+    .lead .fit{font-size:11px;font-weight:700;color:var(--acc);background:rgba(63,185,80,.13);border:1px solid rgba(63,185,80,.35);border-radius:8px;padding:2px 8px;white-space:nowrap;}
+    .lead .meta{color:var(--mut);font-size:12px;white-space:nowrap;}
+    .lead .car{color:var(--mut);transition:transform .15s;} .lead.open .car{transform:rotate(90deg);}
+    .lead .body{display:none;padding:4px 14px 12px;border-top:1px solid var(--line);}
+    .lead.open .body{display:block;}
+    .lead .acts{display:flex;gap:8px;margin:10px 0 6px;flex-wrap:wrap;}
+    .figwrap{margin:8px 0 4px;display:flex;flex-direction:column;gap:4px;}
+    .fighd{font-size:11px;color:var(--mut);letter-spacing:.02em;margin-bottom:1px;}
+    .figli{display:flex;gap:9px;align-items:center;padding:5px 7px;border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.02);text-decoration:none;color:inherit;}
+    .figli:hover{border-color:var(--blue);}
+    .figli-static{cursor:default;} .figli-static:hover{border-color:var(--line);}
+    .figt{width:38px;height:38px;flex:0 0 38px;border-radius:6px;object-fit:cover;background:var(--line);display:inline-block;}
+    .figb{display:flex;flex-direction:column;min-width:0;gap:1px;}
+    .figc{font-size:12px;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:min(70vw,560px);}
+    .figm{font-size:10.5px;color:var(--mut);font-variant-numeric:tabular-nums;}
+    .mls{width:100%;border-collapse:collapse;font-size:12.5px;}
+    .mls th,.mls td{text-align:left;padding:5px 7px;border-top:1px solid var(--line);} .mls th{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.4px;border-top:0;}
+    .mls td.n{text-align:right;font-variant-numeric:tabular-nums;} .mls tr.mrow{cursor:pointer;} .mls tr.mrow:hover td{background:rgba(210,153,34,.07);}
+    .mls .hist{color:var(--blue);font-size:11px;}
+    .phitem{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px solid var(--line);font-size:13px;} .phitem .m{color:var(--mut);}
+  </style>
+  <!-- "Top brokers" people panel removed from the root onload (Steve 2026-08-18: show properties, not people). Broker directory lives at /broker-grid.html. The #bkov modal below is retained — it's shared by the property-grid ✉ contact popup. -->
+  <div id="bkov"><div id="bkmodal"><button class="x" aria-label="Close" onclick="document.getElementById('bkov').classList.remove('on')">✕</button><div id="bkmbody"></div></div></div>
+  <script>
+  (function(){
+    if(!document.getElementById('bkleads')) return;   // "Top brokers" panel removed from root onload — this populate script no-ops (property-grid ✉ uses the global openContact, not this closure).
+    const bq=s=>document.querySelector(s);
+    const money=n=>n?'$'+Number(n).toLocaleString():'—';
+    const esc=s=>(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+    const atype=t=>t==='residential'?'<span class="bkatype res">Residential</span>':'<span class="bkatype com">Commercial</span>';
+    const fdate=s=>{ if(!s) return ''; try{ return new Date(s).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }catch(e){ return ''; } };
+    async function openContact(name,id){
+      const ov=bq('#bkov'), mb=bq('#bkmbody'); ov.classList.add('on'); mb.innerHTML='<div class="gate">loading '+esc(name)+'…</div>';
+      const qs=id?'id='+encodeURIComponent(id):'name='+encodeURIComponent(name);
+      let d; try{ d=await (await fetch('/api/broker?'+qs)).json(); }catch(e){ mb.innerHTML='<div class="gate">error</div>'; return; }
+      if(d.error){ mb.innerHTML='<div class="gate">'+esc(d.error)+'</div>'; return; }
+      const b=d.broker, w=b.website?(/^https?:/.test(b.website)?b.website:'https://'+b.website):null;
+      mb.innerHTML='<h3>'+esc(b.name)+atype(b.agent_type)+'</h3><div class="mfirm">'+esc(b.firm||'—')+(b.title?' · '+esc(b.title):'')+'</div>'
+        +'<div class="ci">'
+        +(b.phone?'<a href="tel:'+esc(b.phone)+'">☎ '+esc(b.phone)+'</a>':'<span class="gate">☎ —</span>')
+        +(b.email?'<a href="mailto:'+esc(b.email)+'">✉ '+esc(b.email)+'</a>':'<span class="gate">✉ —</span>')
+        +(w?'<a href="'+esc(w)+'" target="_blank" rel="noopener noreferrer">🌐 website</a>':'')+'</div>'
+        +'<div class="sec"><h4>Current listings ('+d.current.length+')</h4>'+(d.current.length?d.current.map(l=>'<div class="litem"><span>'+esc(l.address)+', '+esc(l.city)+(l.zip?' '+esc(l.zip):'')+'</span><span class="m">'+esc(l.type)+' · '+money(l.price)+(l.units?' · '+l.units+'u':'')+(l.cap_rate?' · '+l.cap_rate+'% cap':'')+(l.listed_at?' · listed '+fdate(l.listed_at):'')+'</span></div>').join(''):'<div class="gate">none in our set</div>')+'</div>'
+        +'<div class="sec"><h4>Expired / off-market ('+((d.expired&&d.expired.length)||0)+')</h4>'+((d.expired&&d.expired.length)?d.expired.map(x=>'<div class="litem"><span>'+esc(x.address)+', '+esc(x.city)+'</span><span class="m">'+money(x.list_price)+' → sold '+money(x.sold_price)+' · '+fdate(x.sold_date)+(x.mls?' · MLS '+esc(x.mls):'')+((x.url&&!_isAgg(x.url))?' · <a href="'+esc(x.url)+'" target="_blank" rel="noopener noreferrer">↗</a>':'')+'</span></div>').join(''):'<div class="gate">'+esc(d.expiredNote||'none')+'</div>')+'</div>'
+        +'<div class="sec"><h4>Sales — area sold ('+((d.comps&&d.comps.length)||0)+')</h4>'+((d.comps&&d.comps.length)?d.comps.map(c=>'<div class="litem"><span>'+esc(c.address)+', '+esc(c.city)+(c.beds?' · '+c.beds+'bd':'')+(c.baths?'/'+c.baths+'ba':'')+(c.sqft?' · '+Number(c.sqft).toLocaleString()+'sf':'')+'</span><span class="m">'+money(c.sold_price)+' · '+fdate(c.sold_date)+(c.mls?' · MLS '+esc(c.mls):'')+((c.url&&!_isAgg(c.url))?' · <a href="'+esc(c.url)+'" target="_blank" rel="noopener noreferrer">↗</a>':'')+'</span></div>').join(''):'<div class="gate">no area sales</div>')+(d.compsNote?'<div class="gate">'+esc(d.compsNote)+'</div>':'')+'</div>';
+    }
+    bq('#bkov').addEventListener('click',e=>{ if(e.target.id==='bkov') e.currentTarget.classList.remove('on'); });
+    const moneyK=n=>{ if(n==null) return '—'; n=+n; return n>=1e6?'$'+(n/1e6).toFixed(2)+'M':'$'+Math.round(n/1e3)+'k'; };
+    // 100-year property history popup — address-only, NO owner/resident names (by design).
+    async function propHistory(address,city){
+      const ov=bq('#bkov'), mb=bq('#bkmbody'); ov.classList.add('on'); mb.innerHTML='<div class="gate">loading history for '+esc(address)+'…</div>';
+      let d; try{ d=await (await fetch('/api/property-history?address='+encodeURIComponent(address)+'&city='+encodeURIComponent(city||''))).json(); }catch(e){ mb.innerHTML='<div class="gate">error</div>'; return; }
+      const a=d.assessor||d.assessorArchive;
+      let h='<h3>'+esc(address)+'</h3><div class="mfirm">'+esc(city||'')+' · property history (address only — no names)</div>';
+      if(a){ h+='<div class="sec"><h4>Assessor parcel'+(a.roll_year?' ('+esc(a.roll_year)+' roll)':'')+'</h4><div class="ci">'
+        +(a.year_built?'<span>🏗 Built '+a.year_built+'</span>':'')
+        +(a.bedrooms!=null?'<span>🛏 '+a.bedrooms+' bd</span>':'')+(a.bathrooms!=null?'<span>🛁 '+a.bathrooms+' ba</span>':'')
+        +(a.sqft_main?'<span>📐 '+Number(a.sqft_main).toLocaleString()+' sqft</span>':'')+(a.units?'<span>🏠 '+a.units+' units</span>':'')
+        +((a.use_desc2||a.use_desc1||a.use_description)?'<span>🏷 '+esc(a.use_desc2||a.use_desc1||a.use_description)+'</span>':'')
+        +((a.roll_total_value||a.total_value)?'<span>💰 assessed '+money(a.roll_total_value||a.total_value)+'</span>':'')+'</div></div>'; }
+      h+='<div class="sec"><h4>Sold history ('+d.sales.length+')</h4>'+(d.sales.length?d.sales.map(x=>'<div class="phitem"><span>'+esc(x.address)+(x.city?', '+esc(x.city):'')+'</span><span class="m">'+money(x.sold_price)+' · '+String(x.sold_date||'').slice(0,10)+'</span></div>').join(''):'<div class="gate">no sold records on file for this address</div>')+'</div>';
+      if(d.permits&&d.permits.length) h+='<div class="sec"><h4>Permits ('+d.permits.length+')</h4>'+d.permits.map(p=>'<div class="phitem"><span>'+esc(p.permit_type||p.use_description||'permit')+'</span><span class="m">'+esc(String(p.issue_date||'').slice(0,10))+(p.valuation?' · '+money(p.valuation):'')+'</span></div>').join('')+'</div>';
+      if(d.events&&d.events.length) h+='<div class="sec"><h4>Place history</h4>'+d.events.map(ev=>'<div class="phitem"><span>'+esc(ev.summary||ev.kind)+'</span><span class="m">'+esc(String(ev.valid_from||'').slice(0,10))+'</span></div>').join('')+'</div>';
+      if(!a && !d.sales.length && !(d.permits||[]).length) h+='<div class="gate">No assessor record matched this address in the LA County parcel roll (2.4M parcels), and no sold or permit history is on file. Try the street number plus street name exactly as it appears on the deed.</div>';
+      mb.innerHTML=h;
+    }
+    // Sold / area history for a lead (per-broker closings aren't in our data; show area sold comps).
+    async function brokerSold(id,btn){
+      const box=btn.closest('.body').querySelector('.soldbox'); box.innerHTML='<div class="gate">loading sold history…</div>';
+      try{ const d=await (await fetch('/api/broker?id='+id)).json(); const sold=d.comps||[];
+        box.innerHTML='<div class="gate">Per-broker closings need a gated detail-scrape; showing recent <b>area sold comps</b> in this broker’s cities ('+sold.length+'):</div>'
+          +(sold.length?sold.map(c=>'<div class="phitem"><span>'+esc(c.address)+', '+esc(c.city)+'</span><span class="m">'+money(c.sold_price)+' · '+String(c.sold_date||'').slice(0,10)+'</span></div>').join(''):'<div class="gate">none</div>');
+      }catch(e){ box.innerHTML='<div class="gate">'+esc(e.message)+'</div>'; }
+    }
+    // Firm Instagram (Business Discovery — public firm business accounts only).
+    // Loads the last-3-posts cache + firm->handle map, builds a normalized index.
+    window.FIRM_IG={}; window.FIRM_IG_IDX={};
+    const firmIgReady=Promise.all([
+      fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
+      fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}}))
+    ]).then(([cache,map])=>{ window.FIRM_IG=cache||{}; const idx={};
+      for(const [h,al] of Object.entries((map.handles)||{})) for(const a of al) idx[a]=h; window.FIRM_IG_IDX=idx; }).catch(()=>{});
+    const normFirm=s=>String(s||'').toLowerCase().replace(/[^a-z0-9]/g,'');
+    function firmHandle(firm){ const n=normFirm(firm); if(!n) return null;
+      if(window.FIRM_IG_IDX[n]) return window.FIRM_IG_IDX[n];
+      for(const [a,h] of Object.entries(window.FIRM_IG_IDX)){ if(n===a || (a.length>=5 && n.includes(a)) || (a.length>=3 && n.startsWith(a))) return h; } return null; }
+    function firmIgStrip(firm){ const h=firmHandle(firm); if(!h) return '';
+      const c=window.FIRM_IG[h]; if(!c||!c.posts||!c.posts.length) return '';
+      const items=c.posts.slice(0,3).map(p=>{ const cap=esc((p.caption||'').replace(/\s+/g,' ').slice(0,90))||'(no caption)';
+        const when=p.timestamp?String(p.timestamp).slice(0,10):''; const vid=p.media_type==='VIDEO'?'▶ ':'';
+        const thumb=p.image?'<img class="figt" loading="lazy" src="'+esc(p.image)+'" alt="">':'<span class="figt"></span>';
+        return '<a class="figli" href="'+esc(p.permalink)+'" target="_blank" rel="noopener noreferrer">'+thumb+'<span class="figb"><span class="figc">'+vid+cap+' ↗</span><span class="figm">'+(when?when+' · ':'')+'♥ '+(p.likes||0)+' · 💬 '+(p.comments||0)+'</span></span></a>'; }).join('');
+      return '<div class="figwrap"><div class="fighd">📷 @'+esc(h)+' · firm’s latest 3</div>'+items+'</div>'; }
+    // Ranked top brokers — expandable to spreadsheet-style current listings + Sold + per-address history.
+    (function loadBkLeads(_retry){ fetch('/api/leads/top?limit=40').then(r=>{ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); }).then(async d=>{
+      await firmIgReady;
+      const leads=d.leads||[];
+      if(!leads.length){ bq('#bkleads').innerHTML='<div class="gate">No brokers to show yet.</div>'; return; }
+      bq('#bkleads').innerHTML=leads.map((l,i)=>{
+        const rows=(l.listings_json||[]).map(p=>'<tr class="mrow" data-addr="'+esc(p.address)+'" data-city="'+esc(p.city||'')+'">'
+          +'<td>'+esc(p.address)+'<div style="color:var(--mut);font-size:11px">'+esc(p.city||'')+(p.zip?' '+esc(p.zip):'')+'</div>'+(p.created_at?'<div style="color:var(--mut);font-size:10px" title="captured '+esc(String(p.created_at))+'">🕓 '+new Date(p.created_at).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})+'</div>':'')+'</td>'
+          +'<td>'+esc(p.type||'—')+'</td><td class="n">'+money(p.price)+'</td><td class="n">'+(p.units||'—')+'</td>'
+          +'<td class="n">'+(p.cap_rate!=null?p.cap_rate+'%':'—')+'</td><td class="hist">📜 history</td></tr>').join('');
+        return '<div class="lead" data-id="'+l.broker_id+'" data-name="'+esc(l.name)+'">'
+          +'<div class="lhead"><span class="rk">'+(i+1)+'</span>'
+          +'<div class="who"><span class="nm">'+esc(l.name)+'</span>'+atype(l.agent_type)+'<div class="fm">'+esc(l.firm||'—')+'</div></div>'
+          +'<span class="fit">fit '+l.fit_score+'</span><span class="meta">'+l.listings+' listings · '+moneyK(l.total_value)+'</span><span class="car">▸</span></div>'
+          +'<div class="body"><div class="acts"><button class="bkbtn" data-act="contact">✉ Contact</button>'
+          +'<button class="bkbtn" data-act="sold">🔴 Sold / area history</button></div>'
+          +window.firmIgInline(l.firm)
+          +'<table class="mls"><thead><tr><th>Address</th><th>Type</th><th>Price</th><th>Units</th><th>Cap</th><th></th></tr></thead><tbody>'+rows+'</tbody></table>'
+          +'<div class="soldbox"></div></div></div>';
+      }).join('');
+      bq('#bkleads').onclick=e=>{
+        const lead=e.target.closest('.lead'); if(!lead) return;
+        const mrow=e.target.closest('.mrow'); if(mrow){ propHistory(mrow.dataset.addr, mrow.dataset.city); return; }
+        const act=e.target.closest('[data-act]'); if(act){ if(act.dataset.act==='contact') openContact(lead.dataset.name, lead.dataset.id); else brokerSold(lead.dataset.id, act); return; }
+        if(e.target.closest('.lhead')) lead.classList.toggle('open');
+      };
+    }).catch(e=>{ if(_retry>0){ setTimeout(()=>loadBkLeads(_retry-1),1500); return; } bq('#bkleads').innerHTML='<div class="gate">Broker data is temporarily unavailable — please refresh.</div>'; console.warn('leads/top load failed:',e); }); })(1);
+  })();
+  </script>
+  <div class="bar">
+    <span class="count" id="count">…</span>
+    <span class="viewseg" id="viewseg">
+      <button data-view="grid">▦ Grid</button>
+      <button data-view="list">☰ List</button>
+      <button data-view="table">▤ Table</button>
+      <button data-view="kv">🗂 KV</button>
+    </span>
+    <div class="right">
+      <input id="idxSearch" type="text" placeholder="🔎 search all fields — address, firm, agent, type… (space = AND)" style="background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 11px;font-size:12.5px;min-width:230px" title="Filters across EVERY field on each listing (nested-safe). Space = AND.">
+      <button id="shortbtn" class="ex" title="Show only your shortlisted (starred) listings">⭐ Shortlist (0)</button>
+      <button id="cmpbtn" class="ex" title="Compare your shortlisted listings side by side">⚖ Compare</button>
+      <button id="csvbtn" class="ex" title="Download the current filtered + sorted view (visible columns) as CSV">⬇ CSV</button>
+      <button id="linkbtn" class="ex" title="Copy a shareable link to this exact filtered view">🔗 Copy link</button>
+      <select id="heat" title="Color cards/rows by a metric — red (low) → green (high) over the filtered set" style="width:auto"><option value="none">Heat: off</option><option value="deal">Heat: Deal score</option><option value="cap">Heat: Cap rate</option><option value="coc">Heat: Cash-on-cash</option></select>
+      <span id="heatlegend" class="heatlegend">low <span class="bar"></span> high · <span class="hl"></span></span>
+      <label>Sort</label>
+      <select id="sortTop" style="width:auto;min-width:180px"></select>
+    </div>
+  </div>
+  <div class="activebar" id="activebar"></div>
+  <div class="statsbar" id="statsbar"></div>
+  <div class="grid" id="grid"></div>
+  <div class="foot" id="foot"></div>
+</main>
+<aside id="ctrlrail" aria-label="View controls">
+  <div class="railtitle">🧭 Controls</div>
+  <div class="rpanel" data-rp="fields"><button class="rphead" type="button">Fields <span class="caret">▸</span></button><div class="rpbody"><div id="railFields"></div></div></div>
+  <div class="rpanel" data-rp="sort"><button class="rphead" type="button">Sort <span class="caret">▸</span></button><div class="rpbody">
+    <select id="railSort"></select>
+    <label class="rlbl">Direction (any field)</label>
+    <select id="railColSort"><option value="">— sort by column instead —</option></select>
+    <select id="railColDir"><option value="1">Ascending ↑</option><option value="-1">Descending ↓</option></select>
+  </div></div>
+  <div class="rpanel" data-rp="group"><button class="rphead" type="button">Group <span class="caret">▸</span></button><div class="rpbody">
+    <label class="rlbl">Group by field</label>
+    <select id="railGroup"></select>
+  </div></div>
+  <div class="rpanel" data-rp="density"><button class="rphead" type="button">Density <span class="caret">▸</span></button><div class="rpbody">
+    <label class="rlbl">Cards per row (grid) <span class="rangeval" id="railDensVal">3</span></label>
+    <input type="range" id="railDensity" min="1" max="10" step="1" value="3" style="width:100%">
+  </div></div>
+  <div class="rpanel" data-rp="search"><button class="rphead" type="button">Search <span class="caret">▸</span></button><div class="rpbody">
+    <input id="railSearch" type="text" placeholder="🔎 search all fields…">
+  </div></div>
+</aside>
+</div><!-- /shell -->
+
+<div id="chatwrap">
+  <h2>💬 Ask the analyst &nbsp;<span class="small">— grounded in this dataset · free (Claude CLI)</span></h2>
+  <div class="hint">Ask questions <b>or tell it to drive the view</b>: “show only multifamily under $1M at 30% down sorted by cash-on-cash”, “take me to the Reseda mixed-use”.</div>
+  <div id="log"></div>
+  <div class="examples" id="examples"></div>
+  <form id="chatform" autocomplete="off">
+    <input id="q" placeholder="Ask about the deals, or tell me to filter / sort / change financing…">
+    <button id="send" type="submit">Send</button>
+  </form>
+</div>
+
+<div id="cmpov"><div id="cmpmodal"><button class="cmpx" aria-label="Close" onclick="document.getElementById('cmpov').classList.remove('on')">✕</button><h3 style="margin:0 0 10px">⚖ Compare shortlist</h3><div id="cmpbody"></div></div></div>
+<script src="finance.js"></script>
+<script src="deal-score.js"></script>
+<!-- href-to-deeper-data primitives (shared atoms). CRCP drives drilling through its
+     own native Set-based facet model (data-firmfilter / data-typefilter / data-cityfilter
+     → F.firms/F.types/F.cityQuery → render()→stateToURL()); this shared module is loaded
+     so the atom contract + URL-addressable filter state are the single source of truth. -->
+<script src="/drill.js"></script>
+<!-- click a list row's open space → expand consolidated detail + PRIVATE per-user notes (localStorage, not shared) -->
+<script src="/row-notes.js" defer></script>
+<script>
+const $ = s => document.querySelector(s);
+const $$ = s => Array.from(document.querySelectorAll(s));
+const fmt = n => n==null ? '—' : '$'+Math.round(n).toLocaleString();
+const pct = n => n==null ? '—' : n+'%';
+const cls = (v, good, ok) => v==null?'na':(v>=good?'pos':(v>=ok?'':'neg'));
+
+// ── Dated status line (HARD RULE: every property shows EITHER a current listing
+// with its date OR historical data with dates — never a blank/undated status) ──
+const MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
+function fmtMonYear(d){ return d ? MON[d.getMonth()]+' '+d.getFullYear() : null; }
+function fmtFullDate(d){ return d ? MON[d.getMonth()]+' '+d.getDate()+', '+d.getFullYear() : null; }
+// Pull the most reliable known date for a listing: the refresh/source stamp in the
+// note (ISO YYYY-MM-DD or "Mon D, YYYY" or MM/DD/YYYY), else the dataset's sourced date.
+function listingDate(p){
+  const t = p.upside_note || '';
+  let m = t.match(/(20\d\d)-(\d\d)-(\d\d)/);                                 // 2026-06-27
+  if (m) return { d:new Date(+m[1], +m[2]-1, +m[3]), exact:true };
+  m = t.match(/\b([A-Z][a-z]{2,8})\s+(\d{1,2}),?\s+(20\d\d)/);              // June 21, 2026
+  if (m){ const mi=MON.findIndex(x=>m[1].slice(0,3)===x); if(mi>=0) return { d:new Date(+m[3], mi, +m[2]), exact:true }; }
+  m = t.match(/\b(\d{1,2})\/(\d{1,2})\/(20\d\d)/);                          // 06/27/2026
+  if (m) return { d:new Date(+m[3], +m[1]-1, +m[2]), exact:true };
+  // Fallback: the whole dataset's sourcing date (meta.sourced begins YYYY-MM-DD).
+  const src = (DATA && DATA.meta && DATA.meta.sourced || '').match(/(20\d\d)-(\d\d)-(\d\d)/);
+  if (src) return { d:new Date(+src[1], +src[2]-1, +src[3]), exact:false };
+  return null;
+}
+// The guaranteed status line. Current listing → "Listed <date>". Off-market states
+// (Under Contract / Pending / Sold) → labeled with date as historical. No date at
+// all → an explicit "no recorded history" label, never a blank.
+function statusLine(p){
+  const ld = listingDate(p);
+  const dateTxt = ld ? (ld.exact ? fmtFullDate(ld.d) : fmtMonYear(ld.d)+' (dataset)') : null;
+  const title = ld ? ' title="'+(ld.exact?'Listing data as of '+ld.d.toISOString().slice(0,10):'No per-listing date; using dataset source date '+ld.d.toISOString().slice(0,10))+'"' : '';
+  let verb, klass;
+  if (/Under Contract|Pending|Escrow/i.test(p.status)) { verb='In escrow / '+p.status; klass='warn'; }
+  else if (/Sold|Closed/i.test(p.status))             { verb='Last sold'; klass='mut'; }
+  else if (/OUT OF BUDGET/i.test(p.status))           { verb='Listed (over budget)'; klass='bad'; }
+  else                                                { verb='Listed'; klass='ok'; }
+  if (!dateTxt) return `<div class="statusline none" title="No current listing date and no recorded sale history in this dataset">🕓 ${verb} · no recorded date · no sale history on file</div>`;
+  const priced = p.price ? ' '+fmt(p.price) : '';
+  return `<div class="statusline ${klass}"${title}>🕓 ${verb}${priced} · as of ${dateTxt}</div>`;
+}
+
+const SCEN = { '25':{budget:400000,downPct:25,ratePct:6.75,amortYears:30,closingPct:2.5},
+               '30':{budget:400000,downPct:30,ratePct:6.75,amortYears:30,closingPct:2.5},
+               'cash':{budget:400000,downPct:100,ratePct:0,amortYears:30,closingPct:2.5} };
+
+let DATA=null, scenario='25', history=[];
+window.DEMO = {};  // US Census ACS demographics by lowercased city (data/demographics.json)
+function demoLine(p){
+  const d = window.DEMO[(p.city||'').toLowerCase()]; if(!d) return '';
+  const bits=[];
+  if(d.renter_pct!=null) bits.push(`${d.renter_pct}% renters`);
+  if(d.median_gross_rent) bits.push(`$${d.median_gross_rent.toLocaleString()} med rent`);
+  if(d.median_income) bits.push(`$${Math.round(d.median_income/1000)}k income`);
+  if(d.population) bits.push(`${Math.round(d.population/1000)}k pop`);
+  return bits.length ? `<div class="small" title="US Census ACS 2022 — ${p.city} (ZCTA ${d.zcta})">📊 ${bits.join(' · ')}</div>` : '';
+}
+// Neighborhood-based rent estimate from Census median gross rent. std = market median × units
+// (conservative in-place proxy); proforma = +12% market upside. Honest estimate, NOT an appraisal/1007.
+function rentEstimate(p){
+  const d = window.DEMO[(p.city||'').toLowerCase()]; if(!d || !d.median_gross_rent) return null;
+  const base = d.median_gross_rent, units = p.units || 1;
+  const std = Math.round(base*units), pro = Math.round(std*1.12);
+  const annualPro = pro*12;
+  return { std, pro, base, units, zcta:d.zcta, grossYieldPro: p.price? +(100*annualPro/p.price).toFixed(2):null };
+}
+function rentLine(p){
+  const r = rentEstimate(p); if(!r) return '';
+  // Plausibility guard: LA gross yields run ~5-10%. >18% almost always means a bad units count, not a deal.
+  const fishy = r.grossYieldPro && r.grossYieldPro > 18;
+  const yld = r.grossYieldPro ? ` · ${r.grossYieldPro}% gross yld (pro)${fishy?` <span class="b warn" title="Implausibly high — likely a bad unit count on the listing; verify.">⚠ verify units</span>`:''}` : '';
+  return `<div class="small" title="Estimate from Census median gross rent ($${r.base.toLocaleString()}/unit, ${p.city} ZCTA ${r.zcta}) × ${r.units} unit${r.units>1?'s':''}. Not an appraisal — DSCR screen only.">💵 Est. rent: <b style="color:var(--ink)">$${r.std.toLocaleString()}/mo</b> std · <b style="color:var(--acc)">$${r.pro.toLocaleString()}/mo</b> proforma${yld}</div>`;
+}
+// For ranking, treat implausible (>18%) pro-forma yields as low-confidence so bad-units outliers don't top the list.
+function rentRoiScore(p){ const r=rentEstimate(p); if(!r||!r.grossYieldPro) return -1e9; return r.grossYieldPro>18 ? -1 : r.grossYieldPro; }
+
+// ===== Field show/hide + list/grid view + extra data points =====
+const esc = s => String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+// Every data field the UI can render. card=shows on the grid card · col=available as a List column · def=default-on.
+const FIELDS = [
+  {k:'score',      label:'Investment score',  card:1, col:1, def:1},
+  {k:'deal',       label:'Deal score',        card:1, col:1, def:1},
+  {k:'agent',      label:'Agent · contact',   card:1, col:1, def:1},
+  {k:'phone',      label:'Agent phone',       card:1, col:1, def:1},
+  {k:'email',      label:'Email',             card:1, col:1, def:0},
+  {k:'firmweb',    label:'Firm website',      card:1, col:1, def:0},
+  {k:'firm',       label:'Listing firm',      card:1, col:1, def:1},
+  {k:'type',       label:'Asset type',        card:1, col:1, def:1},
+  {k:'units',      label:'Units',             card:1, col:1, def:1},
+  {k:'price',      label:'Price',             card:1, col:1, def:1},
+  {k:'ppu',        label:'Price / unit',      card:1, col:1, def:1},
+  {k:'pricePerSf', label:'Price / SF',        card:1, col:1, def:0},
+  {k:'cap',        label:'Cap rate',          card:1, col:1, def:1},
+  {k:'verified',   label:'Cap-verified flag', card:1, col:0, def:1},
+  {k:'yield',      label:'Rent yield',        card:1, col:1, def:1},
+  {k:'grm',        label:'GRM (rent mult.)',  card:1, col:1, def:0},
+  {k:'yearBuilt',  label:'Year built',        card:1, col:1, def:0},
+  {k:'dom',        label:'Days on market',    card:1, col:1, def:0},
+  {k:'zip',        label:'ZIP',               card:0, col:1, def:0},
+  {k:'status',     label:'Status badge',      card:1, col:1, def:1},
+  {k:'afford',     label:'Affordability',     card:1, col:1, def:1},
+  {k:'rec',        label:'AI recommendation', card:1, col:1, def:0},
+  {k:'statusline', label:'Listing-date line', card:1, col:0, def:1},
+  {k:'brokerflag', label:'Prior-broker flag', card:1, col:0, def:1},
+  {k:'txhist',     label:'Transaction history',card:1,col:0, def:0},
+  {k:'brokerline', label:'Current broker',    card:1, col:1, def:1},
+  {k:'brokerPhone',label:'Broker phone',      card:0, col:1, def:1},
+  {k:'coc',        label:'Cash-on-cash',      card:1, col:1, def:1},
+  {k:'dscr',       label:'DSCR',              card:1, col:1, def:1},
+  {k:'noi',        label:'Est. NOI',          card:1, col:1, def:1},
+  {k:'debt',       label:'Debt service /yr',  card:1, col:1, def:1},
+  {k:'cashNeeded', label:'Cash needed',       card:1, col:1, def:1},
+  {k:'cashFlow',   label:'Cash flow /yr',     card:1, col:1, def:1},
+  {k:'thesis',     label:'AI thesis',         card:1, col:0, def:1},
+  {k:'risks',      label:'Risks',             card:1, col:0, def:0},
+  {k:'upside',     label:'Upside note',       card:1, col:0, def:1},
+  {k:'demo',       label:'Demographics',      card:1, col:0, def:1},
+  {k:'rent',       label:'Rent estimate',     card:1, col:1, def:1},
+  {k:'links',      label:'Listing links',     card:1, col:0, def:1},
+  {k:'beds',       label:'Beds (condo)',       card:1, col:1, def:0},
+  {k:'baths',      label:'Baths (condo)',      card:1, col:1, def:0},
+  {k:'sqft',       label:'Sq Ft',              card:1, col:1, def:0},
+  {k:'hoa',        label:'HOA / mo (condo)',   card:1, col:1, def:0},
+  {k:'warr',       label:'Warrantability',     card:1, col:1, def:0},
+  {k:'broker',     label:'Broker (condo)',     card:1, col:1, def:1},
+  {k:'assessed',   label:'Assessed value ✦',   card:1, col:0, def:0, assessor:1},
+  {k:'landimp',    label:'Land / Improvement ✦',card:1, col:0, def:0, assessor:1},
+  {k:'usecode',    label:'Assessor use code ✦', card:1, col:0, def:0, assessor:1},
+  {k:'priorsale',  label:'Prior sale ✦',        card:1, col:0, def:0, assessor:1},
+];
+// Steve (2026-08-20): ALWAYS show agent · contact, phone, listing firm, current broker — never hideable,
+// override any persisted localStorage off-state (a prior toggle-off used to hide them forever despite def:1).
+const ALWAYS_ON = ['agent','phone','brokerPhone','firm','brokerline','broker'];
+const VIS = (function(){ let s={}; try{ s=JSON.parse(localStorage.getItem('cre_fields')||'{}'); }catch(e){} const o={}; FIELDS.forEach(f=>o[f.k]=ALWAYS_ON.includes(f.k)?true:((f.k in s)?!!s[f.k]:!!f.def)); return o; })();
+function vis(k){ return !!VIS[k]; }
+function saveVis(){ ALWAYS_ON.forEach(k=>VIS[k]=true); try{ localStorage.setItem('cre_fields', JSON.stringify(VIS)); }catch(e){} }
+let view = localStorage.getItem('cre_view') || 'list';
+let groupBy = localStorage.getItem('cre_group') || '';   // group-by-any-field; '' = off
+let colSort = null;  // {key, dir} — click-to-sort on a List column; overrides the sort dropdown when set
+const SHORT = new Set((function(){ try{ return JSON.parse(localStorage.getItem('cre_shortlist')||'[]'); }catch(e){ return []; } })());
+function saveShort(){ try{ localStorage.setItem('cre_shortlist', JSON.stringify([...SHORT])); }catch(e){} }
+function toggleStar(id){ SHORT.has(id)?SHORT.delete(id):SHORT.add(id); saveShort(); }
+let heat = localStorage.getItem('cre_heat') || 'none';
+function heatVal(p){ const f=p.finance||{}; return heat==='deal'?(p.deal&&p.deal.score):heat==='cap'?p.cap_rate:heat==='coc'?f.coc:null; }
+function heatColor(t){ t=Math.max(0,Math.min(1,t)); return `hsl(${Math.round(t*120)},70%,45%)`; }
+function applyHeat(rows){
+  const lg=$('#heatlegend'); if(lg){ lg.classList.toggle('on',heat!=='none'); if(heat!=='none'){ lg.querySelector('.hl').textContent={deal:'Deal score',cap:'Cap rate',coc:'Cash-on-cash'}[heat]; } }
+  if(heat==='none') return;
+  const vals=rows.map(heatVal).filter(v=>v!=null&&isFinite(v)); if(!vals.length) return;
+  const min=Math.min(...vals), max=Math.max(...vals), span=(max-min)||1;
+  const gc=$('#grid').classList;
+  // Per-view heat target: Table stripes the sticky cell; List/KV stripe the row/card via a left inset;
+  // Grid tints the card left border. (Regression fix TK-10747: List + KV were previously un-heated.)
+  const mode = gc.contains('listhost') ? 'table' : gc.contains('compacthost') ? 'compact' : gc.contains('kvhost') ? 'kv' : 'grid';
+  const targetFor=id => mode==='table' ? document.querySelector('.listtbl tr[data-id="'+id+'"] td.stick')
+    : mode==='compact' ? document.querySelector('#grid .crow[data-id="'+id+'"]')
+    : mode==='kv' ? document.querySelector('#grid .kvcard[data-id="'+id+'"]')
+    : document.getElementById('card-'+id);
+  const usesBoxShadow = mode==='table'||mode==='compact'||mode==='kv';
+  rows.forEach(p=>{ const v=heatVal(p);
+    const el=targetFor(p.id); if(!el) return;
+    if(v==null||!isFinite(v)){ if(usesBoxShadow) el.style.boxShadow=''; else el.style.borderLeft=''; return; }
+    const col=heatColor((v-min)/span);
+    if(usesBoxShadow) el.style.boxShadow='inset 5px 0 0 '+col; else el.style.borderLeft='6px solid '+col;
+  });
+}
+// ===== right-hand control rail: disclosure panels (collapsed on load) + nav-agent controls =====
+function wireCtrlRail(densVal){
+  const rail=$('#ctrlrail'); if(!rail) return;
+  // Disclosure panels — collapsed by default; open only if a saved cre_rail_<name>==='1'.
+  rail.querySelectorAll('.rpanel').forEach(p=>{
+    const name=p.dataset.rp; const saved=localStorage.getItem('cre_rail_'+name);
+    if(saved==='1') p.classList.add('open');   // default (null) = collapsed
+    const head=p.querySelector('.rphead');
+    head.setAttribute('aria-expanded', p.classList.contains('open')?'true':'false');
+    head.onclick=()=>{ const open=p.classList.toggle('open'); head.setAttribute('aria-expanded',open?'true':'false'); localStorage.setItem('cre_rail_'+name, open?'1':'0'); };
+  });
+  // Sort — mirror the existing named SORTS (#sort/#sortTop).
+  const rs=$('#railSort'); if(rs){ rs.innerHTML=$('#sort').innerHTML; rs.value=$('#sort').value;
+    rs.onchange=()=>{ colSort=null; $('#sort').value=rs.value; $('#sortTop').value=rs.value; if($('#railColSort'))$('#railColSort').value=''; render(); }; }
+  // Sort-by-ANY-field asc/desc via colSort (uses fieldVal).
+  const groupable=[{k:'addr',label:'Property'}].concat(FIELDS.filter(f=>f.col||['type','firm','status','city'].includes(f.k)));
+  const rcs=$('#railColSort'); if(rcs){ rcs.innerHTML='<option value="">— sort by column instead —</option>'+groupable.map(f=>`<option value="${f.k}">${esc(f.label)}</option>`).join('');
+    if(colSort) rcs.value=colSort.key;
+    const applyColSort=()=>{ const k=rcs.value; if(!k){ colSort=null; } else { colSort={key:k, dir:+($('#railColDir').value)||1}; } render(); };
+    rcs.onchange=applyColSort; $('#railColDir').onchange=()=>{ if(rcs.value) applyColSort(); };
+    if(colSort&&$('#railColDir')) $('#railColDir').value=String(colSort.dir); }
+  // Group-by-ANY-field.
+  const rg=$('#railGroup'); if(rg){ rg.innerHTML='<option value="">— no grouping —</option>'+groupable.map(f=>`<option value="${f.k}">${esc(f.label)}</option>`).join('');
+    rg.value=groupBy; rg.onchange=()=>{ groupBy=rg.value; localStorage.setItem('cre_group',groupBy); render(); }; }
+  // Density — mirror #density.
+  const rd=$('#railDensity'); if(rd){ rd.value=densVal; $('#railDensVal').textContent=densVal;
+    rd.oninput=()=>{ document.documentElement.style.setProperty('--cols',rd.value); $('#railDensVal').textContent=rd.value; if($('#density')){$('#density').value=rd.value; $('#densVal').textContent=rd.value;} localStorage.setItem('cre_density',rd.value); }; }
+  // Search — synced to the top #idxSearch.
+  const rse=$('#railSearch'); if(rse){ const top=$('#idxSearch'); if(top) rse.value=top.value;
+    rse.oninput=()=>{ idxQuery=rse.value.trim().toLowerCase(); if(top) top.value=rse.value; render(); }; }
+}
+function buildFieldToggles(){
+  const host=$('#fFields'); if(!host) return;
+  // emit rows in COLORDER so panel order matches list column order; non-col fields appended after
+  const ordered=(function(){
+    syncColOrder();
+    const colFs=COLORDER.map(k=>FIELDS.find(f=>f.k===k)).filter(Boolean);
+    const rest=FIELDS.filter(f=>!f.col);
+    return colFs.concat(rest);
+  })();
+  host.innerHTML = ordered.map(f=>`<label class="ftog" draggable="true" data-fk="${f.k}"><span class="fdrag" title="Drag to reorder column">⠿</span><input type="checkbox" data-fk="${f.k}" ${vis(f.k)?'checked':''}><span class="flbl">${f.label}</span>${f.col?'<span class="colbadge" title="Also shows as a column in List view">▤</span>':''}</label>`).join('');
+  // Mirror into the right-rail Fields panel (checkbox-only, shares the same VIS state; no drag here).
+  const rail=$('#railFields'); if(rail){ rail.innerHTML=ordered.map(f=>`<label class="ftog" data-fk="${f.k}"><input type="checkbox" data-fk="${f.k}" ${vis(f.k)?'checked':''}><span class="flbl">${f.label}</span></label>`).join(''); }
+}
+// ---- extra computed data points ----
+function ppu(p){ return (p.price&&p.units)?Math.round(p.price/p.units):null; }
+function ppsf(p){ return (p.price&&p.sqft)?Math.round(p.price/p.sqft):null; }
+function grm(p){ const r=rentEstimate(p); if(!r||!r.std) return null; const annual=r.std*12; return annual?+(p.price/annual).toFixed(1):null; }
+function daysOnMarket(p){ const ld=listingDate(p); if(!ld||!ld.exact) return null; const d=Math.round((Date.now()-ld.d.getTime())/86400000); return d>=0?d:null; }
+function shortStatus(p){ if(/Under Contract|Pending|Escrow/i.test(p.status)) return p.status; if(/Sold|Closed/i.test(p.status)) return 'Sold'; if(/OUT OF BUDGET/i.test(p.status)) return 'Over budget'; return 'Active'; }
+// ---- list-view renderer ----
+// ── PRIMARY-TABLE column ORDER (Steve list-build rule: draggable + persisted per-browser) ──
+// COLORDER is the display order over the col-eligible FIELDS keys; new/removed keys are
+// reconciled on every read. Persisted as idxColOrder. The always-first "Property" (addr)
+// stick column is NOT part of COLORDER — it's a fixed anchor, reordering happens among the rest.
+let COLORDER=(function(){ let a=[]; try{ a=JSON.parse(localStorage.getItem('idxColOrder')||'[]'); }catch(e){} return Array.isArray(a)?a:[]; })();
+function colKeys(){ return FIELDS.filter(f=>f.col).map(f=>f.k); }               // every col-eligible field key
+function syncColOrder(){ const keys=colKeys();
+  COLORDER=COLORDER.filter(k=>keys.includes(k));                                // prune removed
+  // Insert any NEW col key next to its canonical FIELDS neighbor (its preceding col-field), NOT at the
+  // end — so newly-added fields (Phone/Email/Firm website) surface beside Agent·contact where they
+  // belong, instead of stranded at the bottom of a persisted column order (Steve 2026-08-18).
+  keys.forEach(k=>{ if(COLORDER.includes(k)) return;
+    const ci=keys.indexOf(k); let anchor=null;
+    for(let j=ci-1;j>=0;j--){ if(COLORDER.includes(keys[j])){ anchor=keys[j]; break; } }
+    if(anchor==null){ COLORDER.unshift(k); }
+    else { COLORDER.splice(COLORDER.indexOf(anchor)+1,0,k); }
+  });
+}
+function saveColOrder(){ try{ localStorage.setItem('idxColOrder',JSON.stringify(COLORDER)); }catch(e){} }
+// Ordered, col-eligible FIELDS in the user's chosen order.
+function orderedCols(){ syncColOrder(); return COLORDER.map(k=>FIELDS.find(f=>f.k===k)).filter(Boolean); }
+// The visible List columns, in order — drives the header + body + resize signature.
+function listCols(){ return orderedCols().filter(f=>vis(f.k)); }
+// Column-set/order signature guard: when it changes, hand the table back to col-resize.js so
+// it rebuilds its <colgroup> + drag handles for the current columns (Steve list-build rule 1).
+let lastColSig='';
+function reinitResize(){
+  const tbl=document.querySelector('#grid .listtbl'); if(!tbl) return;
+  const cg=tbl.querySelector('colgroup[data-cr]'); if(cg) cg.remove();
+  delete tbl.dataset.crDone; tbl.style.tableLayout=''; tbl.style.width='';
+  if(window.ColResize && window.ColResize.refresh) window.ColResize.refresh();
+}
+function colCell(k,p){ const f=p.finance||{};
+  switch(k){
+    case 'score': return p.composite??'—';
+    case 'deal': return (p.deal&&p.deal.score!=null)?p.deal.score:'—';
+    case 'agent': return agentContactCell(p);
+    case 'phone': { const a=agentInfo(p); if(!a.phone) return '—';
+      const tier=a.phoneTier==='firm'?' <span class="tierb" title="Firm branch line'+(a.phoneCity?' — '+esc(a.phoneCity)+' office':'')+'">firm</span>':'';
+      return `<span class="ain"><a href="${telHref(a.phone)}" title="Call ${esc(a.phone)}">📞 ${esc(a.phone)}</a>${tier} <button class="copyb" data-copyval="${esc(a.phone)}" title="Copy phone">⧉</button></span>`; }
+    case 'email': { const a=agentInfo(p); return a.email?`<span class="ain"><a href="mailto:${esc(a.email)}" title="Email ${esc(a.email)}">✉ ${esc(a.email)}</a> <button class="copyb" data-copyval="${esc(a.email)}" title="Copy email">⧉</button></span>`:'—'; }
+    case 'brokerPhone': { const bp=brokerPhoneOf(p); if(!bp) return '—';
+      return `<span class="ain"><a href="${telHref(bp)}" title="Call broker ${esc(bp)}">📞 ${esc(bp)}</a> <button class="copyb" data-copyval="${esc(bp)}" title="Copy broker phone">⧉</button></span>`; }
+    case 'firmweb': { const a=agentInfo(p); const w=a.firmWebsite; if(!w) return '—';
+      const disp=String(w).replace(/^https?:\/\//,'').replace(/\/$/,''); const href=/^https?:\/\//.test(w)?w:'https://'+w;
+      return `<a class="ain" href="${esc(href)}" target="_blank" rel="noopener noreferrer" title="Open the listing firm’s website">🔗 ${esc(disp)}</a>`; }
+    case 'firm': return firmCell(p);
+    case 'type': return esc(p.type||'—');
+    case 'units': return p.units||'—';
+    case 'price': return fmt(p.price);
+    case 'ppu': { const v=ppu(p); return v?'$'+v.toLocaleString():'—'; }
+    case 'pricePerSf': { const v=ppsf(p); return v?'$'+v.toLocaleString():'—'; }
+    case 'cap': return p.cap_rate!=null?p.cap_rate+'%':'—';
+    case 'yield': return f.grossYield?f.grossYield+'%':'—';
+    case 'grm': { const v=grm(p); return v!=null?v:'—'; }
+    case 'yearBuilt': return p.year_built||'—';
+    case 'dom': { const v=daysOnMarket(p); return v!=null?v+'d':'—'; }
+    case 'zip': return p.zip||'—';
+    case 'status': return esc(shortStatus(p));
+    case 'afford': return f.affordable?'✓ in budget':'✗ over';
+    case 'rec': return (p.qwen&&p.qwen.recommendation)||'—';
+    case 'brokerline': { const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()]; const nm=(bh&&bh.current_broker)?(bh.current_broker.name||bh.current_broker.firm):null; return nm?agentCell(nm,(bh.current_broker&&bh.current_broker.firm)||firmLabel(p)):'—'; }
+    case 'coc': return f.coc!=null?f.coc+'%':'—';
+    case 'dscr': return f.dscr??'—';
+    case 'noi': return fmt(f.noi);
+    case 'debt': return fmt(f.annualDebtService);
+    case 'cashNeeded': return fmt(f.cashNeeded);
+    case 'cashFlow': return fmt(f.cashFlow);
+    case 'rent': { const r=rentEstimate(p); return r?'$'+r.std.toLocaleString():'—'; }
+    case 'beds': return p.beds??'—';
+    case 'baths': return p.baths??'—';
+    case 'sqft': return p.sqft?(+p.sqft).toLocaleString():'—';
+    case 'hoa': return p.hoa?'$'+(+p.hoa).toLocaleString():'—';
+    case 'warr': return esc(p.warrantable_status||'—');
+    case 'broker': return p.broker_name?agentCell(p.broker_name,firmLabel(p)):'—';
+    default: return '—';
+  }
+}
+// arrow as a CSS CLASS (asc/desc), NOT literal text — keeps the <th> text stable so col-resize's
+// tableSig doesn't change on sort and saved column widths survive the re-render (TK-10089).
+// "Property" (addr) stays a fixed anchor; the rest are draggable to reorder + click to sort.
+// renderListShell (defined above) builds the identical thead for both renderList and renderTableGrouped.
+function renderList(rows){ return renderListShell(rows.map(renderListBodyRow).join('')); }
+// The inline panel that opens under a list row: deep property/agent searches + the agent CRM
+// (editable contact info, mark-contacted, saved-letter history, compose). Reuses the same
+// #lc-/#hist- containers the card view uses so fetchLiveComps/fetchHistory work unchanged.
+function expandBody(p){
+  const a=agentInfo(p), firm=a.firm, id=p.id;
+  // In-app actions ONLY — no external links. Live comps + Property history render inline in the
+  // panel below; "More from this firm" filters the grid in place.
+  const deep=[];
+  deep.push(`<button class="lcbtn" data-id="${id}" title="Pull fresh comps inline (~$0.03)">🔄 Live comps</button>`);
+  deep.push(`<button class="histbtn" data-id="${id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (inline, free)">📜 Property history</button>`);
+  if(firm&&firm!=='Unknown') deep.push(`<button data-firmfilter="${esc(firm).replace(/"/g,'&quot;')}" title="Show only ${esc(firm)} listings">▦ More from ${esc(firm)}</button>`);
+  // Agent summary + notes preview. Full editing (info, notes, letters, draft) lives in the inline
+  // agent window that ✎ Open launches — no external search, no new tabs.
+  const summary=`<div class="crm-summary">${a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a> `:(isPocket(a)?pocketBadge+' ':'<span class="amut">no agent on file — add via ✎ Open</span> ')}${contactBits(a)}`
+    +`${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}${a.letters.length?`<span class="achip">✎ ${a.letters.length} saved</span>`:''}</div>`
+    +(a.notes?`<div class="crm-note">🗒 ${esc(a.notes)}</div>`:'');
+  return `<div class="src">${deep.join(' ')}</div>`
+    +`<div class="crm">${summary}
+        <div class="crm-acts">
+          <button class="abtn contactbtn" data-contact="${id}">✎ Open agent window</button>
+          <button class="abtn markbtn" data-mark="${id}">${a.contacted_at?'↻ Update contacted date':'✓ Mark contacted'}</button>
+        </div>
+      </div>`
+    +`<div class="lcout" id="lc-${id}"></div><div class="histout" id="hist-${id}"></div>`;
+}
+// raw (unformatted) value for a field, for generic column sorting
+function fieldVal(p,k){ const f=p.finance||{};
+  switch(k){
+    case 'addr': return p.address; case 'score': return p.composite; case 'deal': return p.deal&&p.deal.score;
+    case 'agent': { const a=agentInfo(p); return a.name||('~'); } // contactless rows sort last (~ > letters)
+    case 'phone': { const a=agentInfo(p); return a.phone||'~'; }   // rows without a resolved phone sort last
+    case 'brokerPhone': { const bp=brokerPhoneOf(p); return bp||'~'; }
+    case 'email': { const a=agentInfo(p); return a.email||'~'; }
+    case 'firmweb': { const a=agentInfo(p); return a.firmWebsite||'~'; }
+    case 'firm': return firmLabel(p); case 'type': return p.type; case 'units': return p.units; case 'price': return p.price;
+    case 'ppu': return ppu(p); case 'pricePerSf': return ppsf(p); case 'cap': return p.cap_rate; case 'yield': return f.grossYield;
+    case 'grm': return grm(p); case 'yearBuilt': return p.year_built; case 'dom': return daysOnMarket(p); case 'zip': return p.zip;
+    case 'status': return shortStatus(p); case 'afford': return f.affordable?1:0; case 'rec': return p.qwen&&p.qwen.recommendation;
+    case 'coc': return f.coc; case 'dscr': return f.dscr; case 'noi': return f.noi; case 'debt': return f.annualDebtService;
+    case 'cashNeeded': return f.cashNeeded; case 'cashFlow': return f.cashFlow;
+    case 'rent': { const r=rentEstimate(p); return r?r.std:null; }
+    case 'beds': return p.beds; case 'baths': return p.baths; case 'sqft': return p.sqft; case 'hoa': return p.hoa;
+    case 'warr': return p.warrantable_status; case 'broker': return p.broker_name;
+    default: return null;
+  }
+}
+function sortRows(r){
+  if(colSort){ const k=colSort.key, d=colSort.dir;
+    r.sort((a,b)=>{ let x=fieldVal(a,k), y=fieldVal(b,k);
+      const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
+      if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
+      if(x<y) return -d; if(x>y) return d; return 0; });
+  } else {
+    r.sort(SORTS[$('#sort').value]||SORTS.composite);
+  }
+  // group-by: keep the within-group order, but bucket rows so each group is contiguous (stable partition).
+  if(groupBy){
+    const buckets=new Map();
+    r.forEach(p=>{ const gk=groupKeyLabel(p); if(!buckets.has(gk)) buckets.set(gk,[]); buckets.get(gk).push(p); });
+    const keys=[...buckets.keys()].sort((a,b)=>{ const an=!isNaN(+a),bn=!isNaN(+b); if(an&&bn) return +a-+b; return a.localeCompare(b); });
+    const out=[]; keys.forEach(k=>out.push(...buckets.get(k)));
+    r.length=0; r.push(...out);
+  }
+  return r;
+}
+
+// ---- Unified Deal Score (0-100) — DealScore.compute() over the signals we already have. ----
+// Gathers: pro-forma gross yield (Census rent estimate), cap rate, in-place DSCR (finance.js),
+// a PROXY DSCR when no cap is disclosed (est NOI from pro-forma rent vs the active-scenario debt
+// service), and the assumable-FHA/VA bonus. Transparent + labeled; never NaN. Cached on the row.
+function dealScore(p){
+  const r = rentEstimate(p);
+  const grossYieldPro = (r && r.grossYieldPro != null && isFinite(r.grossYieldPro)) ? r.grossYieldPro : null;
+  // Proxy DSCR only when there is NO in-place cap (else the real DSCR carries it). Estimate in-place
+  // NOI as pro-forma annual gross × 0.6 (a 40% commercial expense ratio) and compare to debt service.
+  let proxyDscr = null;
+  const f = p.finance;
+  if (p.cap_rate == null && r && r.pro && f && f.annualDebtService > 0){
+    const estNoi = r.pro * 12 * 0.6;
+    proxyDscr = +(estNoi / f.annualDebtService).toFixed(2);
+  }
+  return DealScore.compute({
+    grossYieldPro,
+    cap_rate: p.cap_rate,
+    verified: p.verified,
+    dscr: f ? f.dscr : null,
+    proxyDscr,
+    assum: p.assum,           // present only if the geocode pipeline tagged this listing (heuristic)
+    fhaApproved: p.fhaApproved
+  });
+}
+// LoopNet cross-reference for EVERY listing — free deep-links ($0, no scrape). Mirrors scripts/sources/loopnet.js.
+const LN_TYPE={'Multifamily':'multifamily','Office':'office','Retail':'retail','Industrial':'industrial','Mixed-use':'mixed-use','Mixed Use':'mixed-use','Land':'land'};
+function loopnetLink(p){
+  const sl=s=>String(s||'').toLowerCase().trim().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
+  const city=p.city?`${sl(p.city)}-ca`:'los-angeles-ca';
+  const ptype=LN_TYPE[p.type]||'';
+  // exact: site-search resolves the specific LoopNet listing for this address if it exists.
+  const exact=`https://www.google.com/search?q=${encodeURIComponent(`site:loopnet.com "${[p.address,p.city].filter(Boolean).join(' ')}"`)}`;
+  const compsQ=`site:loopnet.com ${ptype||'commercial'} for sale ${p.city||'Los Angeles'} CA`;
+  const comps=`https://www.google.com/search?q=${encodeURIComponent(compsQ)}`;
+  return `<a href="${exact}" target="_blank" rel="noopener noreferrer" title="Find this exact property on LoopNet (CoStar) — free search, opens in a new tab">LoopNet ↗</a> <a href="${comps}" target="_blank" rel="noopener noreferrer" class="small" title="Browse comparable ${p.type||'commercial'} for-sale on LoopNet in ${p.city||'LA'}" style="opacity:.7">(comps)</a>`;
+}
+// Rich filter state
+const F = { types:new Set(), firms:new Set(), cityQuery:'', priceMin:null, priceMax:null,
+            capMin:0, unitsMin:null, unitsMax:null, yearMin:null, yearMax:null, status:'all', afford:false, unwarrantable:false, nonqm:false, shortlist:false, hasPhone:false, hasEmail:false };
+
+// ── SEARCH ALL DATA (Steve list-build rule 7) ──────────────────────────────────
+// Free-text search across EVERY field on a listing — not just the visible/defined columns,
+// nested objects included (finance/qwen/deal…) — via JSON.stringify. Space = AND.
+// A per-row haystack is memoized on `p.__idxHay` and invalidated when the row identity changes.
+let idxQuery='';
+function idxHay(p){
+  if(p.__idxHay!=null) return p.__idxHay;
+  let s;
+  try{ s=JSON.stringify(p, (k,v)=> k==='__idxHay'?undefined:v); }
+  catch(e){ s=Object.keys(p).map(k=>{ const v=p[k]; return v==null?'':(typeof v==='object'?'':String(v)); }).join(' '); }
+  // fold in the CRM-saved contact (agent name/phone/email) which lives outside the row object
+  const c=window.CONTACTS&&window.CONTACTS[p.id]; if(c) s+=' '+JSON.stringify(c);
+  s=s.toLowerCase();
+  try{ Object.defineProperty(p,'__idxHay',{value:s,enumerable:false,configurable:true,writable:true}); }catch(_){ p.__idxHay=s; }
+  return s;
+}
+function idxSearchPass(p){
+  if(!idxQuery) return true;
+  const hay=idxHay(p);
+  return idxQuery.split(/\s+/).filter(Boolean).every(t=>hay.includes(t));
+}
+
+function recompute(){
+  const A = SCEN[scenario];
+  DATA.ranked.forEach(p=>{
+    try{ delete p.__idxHay; }catch(_){}   // finance/deal recomputed → refresh the search haystack
+    p.finance = CREFin.finance(p, A);
+    const fsc = CREFin.financeScore(p, p.finance);
+    p.financeScore = fsc.score; p.financeConf = fsc.conf;
+    p.composite = CREFin.composite(p, p.finance, fsc.score, p.qwenScore);
+    p.deal = dealScore(p);   // {score, parts, confidence, coverage} — cached for badge + sort + tooltip
+  });
+  DATA.ranked.sort((a,b)=>b.composite-a.composite);
+  DATA.ranked.forEach((p,i)=>p.rank=i+1);
+}
+
+function firmLabel(p){
+  if(p.firm) return p.firm;
+  const d=(p.source||'').replace(/^https?:\/\/(www\.)?/,'').split('/')[0]||'';
+  const map={'crexi.com':'Crexi','loopnet.com':'LoopNet','compass.com':'Compass','remax.com':'RE/MAX',
+    'coldwellbankerhomes.com':'Coldwell Banker','marcusmillichap.com':'Marcus & Millichap',
+    'lee-associates.com':'Lee & Associates','colliers.com':'Colliers','cbre.com':'CBRE','jll.com':'JLL'};
+  if(map[d]) return map[d];
+  return d ? d.split('.')[0].replace(/^./,c=>c.toUpperCase()) : 'Unknown';
+}
+
+// ── Agent-contact CRM (front-end) ──────────────────────────────────────────────────
+// CONTACTS[listingId] = {name,phone,email,firm,contacted_at,letters:[]}. Loaded once at
+// boot from /api/contacts; every save re-writes the record here so the grid re-renders
+// without a reload. Sending a letter is NOT done here — letters are SAVED only.
+window.CONTACTS = window.CONTACTS || {};
+fetch('/api/contacts').then(r=>r.json()).then(j=>{ window.CONTACTS=j.contacts||{}; if(window.DATA_READY) render(); }).catch(()=>{});
+
+// ── Real phone reveal (TK-10687) ── each listing carries a broker/agent NAME but no phone; the real
+// numbers live in the usre DRE registry. resolveContacts() batches the visible page's agents to the
+// /api/broker-contacts proxy, which returns a REAL city-scoped contact (see serve.js). National
+// toll-free numbers arrive flagged phone_national and are SUPPRESSED below (Steve: "not a national
+// number — closest to the listing"); the firm website link stands in so the local branch is reachable.
+window.BROKER_RESOLVED = window.BROKER_RESOLVED || {};
+const _TOLLFREE=new Set(['800','833','844','855','866','877','888','822','880','887','889']);
+// CRCP is LA-County-only, so the "closest to the listing" phone (Steve's rule) must be a CALIFORNIA
+// number. Every CA area code — a firm phone outside this set (e.g. a Compass 212 NY HQ line) is a
+// national/HQ leak and is SUPPRESSED (the firm website stands in) exactly like a toll-free number.
+const _CA_AREA=new Set(['209','213','279','310','323','341','350','408','415','424','442','510','530','559','562','619','626','628','650','657','661','669','707','714','747','760','805','818','820','831','858','909','916','925','935','949','951']);
+const _area=ph=>{ const d=String(ph||'').replace(/\D/g,'').replace(/^1(?=\d{10}$)/,''); return d.slice(0,3); };
+const isNat=ph=>{ const a=_area(ph); return _TOLLFREE.has(a) || (a.length===3 && !_CA_AREA.has(a)); };
+const _resolveDone=new Set();
+async function resolveContacts(rows){
+  const items=[];
+  for(const p of (rows||[])){ if(p&&p.id!=null&&!_resolveDone.has(p.id)){ _resolveDone.add(p.id);
+    const a=agentInfo(p); if(a.name) items.push({id:p.id,name:a.name,firm:a.firm||'',city:p.city||''}); } }
+  if(!items.length) return;
+  try{ const j=await postJSON('/api/broker-contacts',{items});
+    if(j&&j.contacts&&Object.keys(j.contacts).length){ Object.assign(window.BROKER_RESOLVED,j.contacts); paintPage(); } }catch(_){}
+}
+
+// ── Firm Instagram (global, link-free) ── each brokerage firm's last 3 IG posts,
+// rendered INLINE under the agent-contact card (no outside links — Business
+// Discovery, thumbnails cached same-origin). Loads once at boot, re-renders on ready.
+window.FIRM_IG = window.FIRM_IG || {}; window.FIRM_IG_IDX = window.FIRM_IG_IDX || {};
+Promise.all([
+  fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
+  fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}}))
+]).then(([cache,map])=>{ window.FIRM_IG=cache||{}; const idx={};
+  for(const [h,al] of Object.entries((map.handles)||{})) for(const a of al) idx[a]=h; window.FIRM_IG_IDX=idx;
+  if(window.DATA_READY) render(); }).catch(()=>{});
+window.firmHandleOf=function(firm){ const n=String(firm||'').toLowerCase().replace(/[^a-z0-9]/g,''); if(!n) return null;
+  if(window.FIRM_IG_IDX[n]) return window.FIRM_IG_IDX[n];
+  for(const [a,h] of Object.entries(window.FIRM_IG_IDX)){ if(n===a||(a.length>=5&&n.includes(a))||(a.length>=3&&n.startsWith(a))) return h; } return null; };
+window.firmIgInline=function(firm){ const h=window.firmHandleOf(firm); if(!h) return '';
+  const c=window.FIRM_IG[h]; if(!c||!c.posts||!c.posts.length) return '';
+  const e=s=>String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]));
+  const items=c.posts.slice(0,3).map(p=>{ const cap=e((p.caption||'').replace(/\s+/g,' ').slice(0,90))||'(no caption)';
+    const when=p.timestamp?String(p.timestamp).slice(0,10):''; const vid=p.media_type==='VIDEO'?'▶ ':'';
+    const thumb=p.image?'<img class="figt" loading="lazy" src="'+e(p.image)+'" alt="">':'<span class="figt"></span>';
+    return '<div class="figli figli-static">'+thumb+'<span class="figb"><span class="figc">'+vid+cap+'</span><span class="figm">'+(when?when+' · ':'')+'♥ '+(p.likes||0)+' · 💬 '+(p.comments||0)+'</span></span></div>'; }).join('');
+  return '<div class="figwrap"><div class="fighd">📷 @'+e(h)+' · firm’s latest 3</div>'+items+'</div>'; };
+// Best-known contact info for a listing: the saved CRM record wins, else what the scrape gave us.
+function agentInfo(p){
+  const c=window.CONTACTS[p.id]||{};
+  const rv=window.BROKER_RESOLVED[p.id]||{};                       // real contact resolved from usre (TK-10687)
+  const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
+  const dName=p.broker_name||p.broker_agent||(Array.isArray(p.broker_agents)&&p.broker_agents[0])||(bh&&bh.current_broker&&(bh.current_broker.name||bh.current_broker.firm))||'';
+  // Phone precedence: hand-saved CRM > scraped row > resolved usre line — but a NATIONAL/toll-free
+  // number is never shown as the phone (Steve: "not a national number"); the firm website stands in.
+  const rvPhone=(rv.phone && !rv.phone_national && !isNat(rv.phone))?rv.phone:'';   // isNat now also drops non-CA area codes (out-of-region HQ lines)
+  return { name:c.name||dName||'', phone:c.phone||p.broker_phone||rvPhone||'', email:c.email||p.broker_email||rv.email||'',
+           firm:c.firm||(rv.firm)||firmLabel(p),
+           phoneTier:rv.phone_tier||null, phoneCity:rv.phone_city||null, phoneNational:!!rv.phone_national,
+           firmWebsite:rv.firm_website||'', firmPhone:(rv.firm_phone && !isNat(rv.firm_phone))?rv.firm_phone:'', firmAddress:rv.firm_address||'',
+           contacted_at:c.contacted_at||null, notes:c.notes||'', letters:(c.letters||[]) };
+}
+// Broker (listing broker of record) phone — the enriched current-broker line, distinct from the
+// agent/contact phone in agentInfo (which folds in the scraped row's broker_phone). Steve 2026-08-25:
+// "add broker and agent phone numbers to the fields."
+function brokerPhoneOf(p){
+  const a=agentInfo(p);
+  const bh=window.BROKER_HIST[p.id]||window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
+  const chist=(bh&&bh.current_broker&&bh.current_broker.phone)||'';
+  // Prefer a DISTINCT firm/broker-of-record line (resolved usre firm_phone or broker-history), then the
+  // raw row, then — if the agent's own resolved line IS a firm/branch line — use that (it's the broker
+  // office number). Keeps "Broker phone" the firm-of-record line, distinct from the agent-direct phone.
+  return a.firmPhone || chist || p.broker_phone || (a.phoneTier==='firm' ? a.phone : '') || '';
+}
+function gsearch(q){ return 'https://www.google.com/search?q='+encodeURIComponent(q); } // retained; no longer linked out
+function safeUrl(u){ u=String(u==null?'':u); return /^https?:\/\//i.test(u)?u:'#'; }
+// "↗ View property" — a link OUT to the real listing on whichever service it's on (Crexi / LoopNet /
+// the broker's own site). Uses p.source, the actual listing URL. Steve 2026-08-18: "need to see the property."
+function sourceLink(p){ const u=p.source; if(!u||!/^https?:\/\//i.test(u)) return '';
+  const host=u.replace(/^https?:\/\/(www\.)?/,'').split('/')[0];
+  const map={'crexi.com':'Crexi','loopnet.com':'LoopNet','compass.com':'Compass','remax.com':'RE/MAX','lyonstahl.com':'Lyon Stahl','coldwellbankerhomes.com':'Coldwell Banker'};
+  const name=map[host]||host.replace(/\.(com|net|org)$/,'').replace(/^./,c=>c.toUpperCase());
+  return `<a class="srclink" href="${safeUrl(u)}" target="_blank" rel="noopener noreferrer" title="See this property on ${esc(name)}">↗ View on ${esc(name)}</a>`;
+}
+// Aggregator doctrine: never surface a clickable href to crexi/redfin/zillow/costar/loopnet/realtor/myelisting.
+// Mirror of mls.html brokerListing() — resolve canonical click-through from listing data.
+const _AGG=['crexi','redfin','zillow','costar','loopnet','realtor','myelisting'];
+const _host=u=>{try{return new URL(u).hostname.replace(/^www\./,'');}catch{return null;}};
+const _isAgg=u=>{const h=_host(u);return !!(h&&_AGG.some(a=>h.includes(a)));};
+// Refined listing-link doctrine (2026-07-31):
+// 1. Non-agg source → firm-direct listing link.
+// 2. Agg/absent source + non-agg broker_url → honest "firm site" label (NOT "the listing").
+// 3. Always append a Google ground-truth finder (never crexi/aggregator).
+// Returns array of HTML strings to push() into the find[] block.
+function listingLinkBits(p, firm){
+  const bits=[];
+  const listingQ='"'+(p.address||'')+'" '+(p.city||'')+' '+(firm&&firm!=='Unknown'?firm:'')+' commercial listing';
+  if(p.firm_direct_url&&!_isAgg(p.firm_direct_url)){   // harvested broker's-own-page deep link — highest confidence
+    bits.push(`<a class="findlink" href="${safeUrl(p.firm_direct_url)}" target="_blank" rel="noopener noreferrer">↗ View firm listing</a>`);
+  } else if(p.source&&!_isAgg(p.source)){
+    bits.push(`<a class="findlink" href="${safeUrl(p.source)}" target="_blank" rel="noopener noreferrer">↗ View firm listing</a>`);
+  } else {
+    if(p.broker_url&&!_isAgg(p.broker_url)){
+      bits.push(`<a class="findlink" href="${safeUrl(p.broker_url)}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm&&firm!=='Unknown'?firm:'Broker')} site</a>`);
+    }
+    bits.push(`<a class="findlink" href="https://www.google.com/search?q=${encodeURIComponent(listingQ)}" target="_blank" rel="noopener noreferrer">🔎 Find this listing</a>`);
+  }
+  return bits;
+}
+// A listing with NO contact of any kind (no agent name, phone, email, or firm site) is an off-market /
+// POCKET listing — the broker deliberately isn't published. Show that honestly instead of "no agent on
+// file" (which reads like missing data). Steve 2026-08-19: "show the word pocket listing if appropriate."
+function isPocket(a){ return !a.name && !a.phone && !a.email && !a.firmWebsite && !a.firm; }
+const pocketBadge = `<span class="pocketbadge" title="Off-market / pocket listing — the broker isn't publicly listed for this property">🔒 Pocket listing</span>`;
+// Firm cell — clicking filters the grid to that firm IN-APP (no external link).
+function firmCell(p){ const n=firmLabel(p); if(!n||n==='Unknown') return esc(n||'—');
+  return `<span class="flink" data-firmfilter="${esc(n).replace(/"/g,'&quot;')}" title="Show only ${esc(n)} listings">${esc(n)}</span>`; }
+// Agent name — plain in-app text (no external search).
+function agentCell(name){ return name?esc(name):'—'; }
+// Inline phone/email. The phone is a REAL tel: link (Steve 2026-08-18 "make phone icon actually call
+// out") so clicking 📞 or the number places the call; email is a mailto:. Copy buttons stay for both.
+const telHref=ph=>'tel:'+String(ph||'').replace(/[^0-9+]/g,'');
+function contactBits(a){
+  const tier=a.phone&&a.phoneTier==='firm'?`<span class="tierb" title="Firm branch line${a.phoneCity?' — '+esc(a.phoneCity)+' office':''}">firm</span>`:'';
+  const tel=a.phone?`<span class="ain"><a href="${telHref(a.phone)}" title="Call ${esc(a.phone)}">📞 ${esc(a.phone)}</a> ${tier} <button class="copyb" data-copyval="${esc(a.phone)}" title="Copy phone">⧉</button></span>`:'';
+  const eml=a.email?`<span class="ain"><a href="mailto:${esc(a.email)}" title="Email ${esc(a.email)}">✉ ${esc(a.email)}</a> <button class="copyb" data-copyval="${esc(a.email)}" title="Copy email">⧉</button></span>`:'';
+  // Firm website — "open listing firm website" (and the way to reach the LOCAL branch when we only
+  // have a national/missing number). Opens in a new tab; noopener for safety.
+  const w=a.firmWebsite?String(a.firmWebsite).replace(/^https?:\/\//,'').replace(/\/$/,''):'';
+  const web=w?`<span class="ain">🔗 <a href="${esc(a.firmWebsite.match(/^https?:\/\//)?a.firmWebsite:'https://'+a.firmWebsite)}" target="_blank" rel="noopener noreferrer" title="Open the listing firm’s website — find the local office">${esc(w)}</a></span>`:'';
+  const inner=[tel,eml,web].filter(Boolean).join(' ');
+  return inner?`<div class="ainrow">${inner}</div>`:'';
+}
+// The Agent · contact column cell: name + copyable phone/email + Open/Mark buttons + chips.
+function agentContactCell(p){
+  const a=agentInfo(p);
+  const nm=a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a>`:(isPocket(a)?pocketBadge:`<span class="amut">no agent on file</span>`);
+  const contacted=a.contacted_at?`<span class="achip done" title="Marked contacted ${esc(a.contacted_at)}">✓ ${fmtWhen(a.contacted_at)}</span>`:'';
+  const lc=a.letters.length?`<span class="achip" title="${a.letters.length} saved letter(s)">✎ ${a.letters.length}</span>`:'';
+  const nt=a.notes?`<span class="achip" title="Has notes">🗒 notes</span>`:'';
+  // Steve 2026-08-18 "phone on front page. no OPEN": the contact info (phone/email/firm website) shows
+  // DIRECTLY in the cell — no "Open" button gating it. Only a compact Contacted mark remains (click the
+  // ROW to open the full agent window if needed; the row click handler already does that).
+  const btns=`<span class="acts">`
+    +`<button class="abtn markbtn" data-mark="${p.id}" title="${a.contacted_at?'Update the contacted date':'Mark contacted (saves the date)'}">${a.contacted_at?'↻':'✓'} Contacted</button>`
+    +`</span>`;
+  return `<div class="agentcell">${nm}${contactBits(a)}<div class="chiprow">${contacted}${lc}${nt}</div>${btns}</div>`;
+}
+function fmtWhen(iso){ try{ return new Date(iso).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}); }catch(_){ return String(iso||'').slice(0,10); } }
+
+// Deal-Score badge + plain-text breakdown tooltip (explains WHY the score is what it is).
+function dealBadge(p){
+  const d = p.deal; if(!d || d.score==null) return `<span class="b" title="Not enough signal to score this deal (no cap rate, rent estimate, or DSCR available).">⬡ Deal —</span>`;
+  const col = DealScore.scoreColor(d.score);
+  const lines = d.parts.map(pt=>`• ${pt.label}: ${Math.round(pt.score)}/100 (${pt.conf}, weight ${Math.round(pt.weight)})`).join('\n');
+  const tip = `Deal Score ${d.score}/100 — weighted blend of the signals below, re-normalized over what's present (missing signals drop out, never NaN).\nConfidence: ${d.confidence}${d.coverage!=null?` (core coverage ${Math.round(d.coverage*100)}%)`:''}.\n\n${lines}\n\nA screen, not an appraisal. Verify NOI + units in DD.`;
+  return `<span class="b deal-badge" style="color:${col};border-color:${col}" title="${tip.replace(/"/g,'&quot;')}">⬡ Deal ${d.score}${d.confidence==='low'||d.confidence==='thin'?' ?':''}</span>`;
+}
+function card(p){
+  const f=p.finance, uc=/Under Contract|Pending/i.test(p.status);
+  // firm badge is now a DRILL atom → filters the grid to this firm (reuses data-firmfilter handler)
+  const _fn=firmLabel(p);
+  const firmB = (_fn && _fn!=='Unknown')
+    ? `<span class="b drill" data-firmfilter="${esc(_fn).replace(/"/g,'&quot;')}" data-filter="firm" data-v="${esc(_fn).replace(/"/g,'&quot;')}" title="Filter to ${esc(_fn)} listings">🏢 ${esc(_fn)}</span>`
+    : `<span class="b" title="Listing source / brokerage firm">🏢 ${esc(_fn||'—')}</span>`;
+  const aff = f.affordable ? `<span class="b ok">✓ $${(f.cashNeeded/1000).toFixed(0)}k in budget</span>`
+                           : `<span class="b bad">✗ needs $${(f.cashNeeded/1000).toFixed(0)}k</span>`;
+  const rec = p.qwen && p.qwen.recommendation ? `<span class="b rec-${p.qwen.recommendation}">${p.qwen.recommendation}</span>` : '';
+  const statusB = uc ? `<span class="b warn">${p.status}</span>` : (/OUT OF BUDGET/i.test(p.status)?`<span class="b bad">over budget</span>`:`<span class="b ok">Active</span>`);
+  const capB = p.cap_rate!=null
+     ? `<span class="b">${p.cap_rate}% cap${p.cap_rate_projected?` →${p.cap_rate_projected}%*`:''}</span>`
+     : `<span class="b warn">cap n/d</span>`;
+  const verB = p.verified===true ? `<span class="b ok" title="In-place NOI confirmed against MLS">✓ cap verified</span>`
+     : (p.cap_rate!=null ? `<span class="b" title="Broker-stated cap, not independently verified">broker cap</span>`
+     : `<span class="b warn" title="No in-place income disclosed">⚠ no income data</span>`);
+  // Broker-history enrichment — keyed by listing id then by address fallback
+  const bh = window.BROKER_HIST[p.id] || window.BROKER_HIST_ADDR[(p.address||'').toLowerCase().trim()];
+  const brokerFlagBadge = bh && bh.broker_flag
+    ? `<span class="brokerflag" title="${esc(bh.flag_reason||'')}">⚑ Prior broker on file</span>`
+    : '';
+  function txHistHTML(bh){
+    if(!bh || !bh.tx_history || !bh.tx_history.length) return '';
+    const rows = bh.tx_history.map(t=>{
+      const price = t.price ? `<span class="tx-price">$${Number(t.price).toLocaleString()}</span>` : '';
+      const src = t.tier==='A' ? ' <span style="color:var(--acc);font-size:9px">[A]</span>' : ' <span style="color:var(--mut);font-size:9px">[B]</span>';
+      return `<div class="tx-row"><span class="tx-date">${esc(String(t.date||'').slice(0,10))}</span><span class="tx-ev">${esc(t.event||'')}${src}</span>${price}</div>`;
+    }).join('');
+    // Also show the last deed recording from assessor if we have AIN
+    const ain = bh.ain ? `<div style="font-size:10px;color:var(--mut);margin-top:2px">AIN ${bh.ain} · built ${bh.assessor_year_built||'—'} · ${bh.assessor_units||'?'} units (assessor)</div>` : '';
+    return `<div class="txhist">${rows}${ain}</div>`;
+  }
+  function brokerLineHTML(bh){
+    if(!bh || !bh.current_broker) return '';
+    const b = bh.current_broker;
+    if(!b.name && !b.firm) return '';
+    const nameStr = b.name ? `<strong>${esc(b.name)}</strong>` : '';
+    const firmStr = b.firm ? esc(b.firm) : '';
+    const dreStr  = b.dre ? (function(){const L=String(b.dre).trim();return /^\d{8}$/.test(L)?` · DRE# <a href="https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=${encodeURIComponent(L)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record ↗">${esc(L)} ↗</a>`:` · DRE ${esc(L)}`;})() : '';
+    const phoneStr = b.phone ? ` · <a href="tel:${esc(String(b.phone).replace(/[^0-9+]/g,''))}">${esc(b.phone)}</a>` : '';
+    const mlsStr  = b.mls  ? ` · MLS ${esc(b.mls)}` : '';
+    const parts = [nameStr, firmStr].filter(Boolean).join(' · ');
+    const tierBadge = b.tier==='A' ? ' <span style="color:var(--acc);font-size:9px">[A]</span>' : '';
+    return `<div class="brokerline">🧑‍💼 ${parts}${dreStr}${phoneStr}${mlsStr}${tierBadge}</div>`;
+  }
+  return `<div class="card ${uc?'uc':''}" id="card-${p.id}">
+    <div class="top">
+      <div><div class="rank">#${p.rank} · score</div><div class="addr">${esc(p.address||'')}</div><div class="city">${p.city?`<span class="drill" data-cityfilter="${esc(p.city).replace(/"/g,'&quot;')}" data-filter="city" data-v="${esc(p.city).replace(/"/g,'&quot;')}" title="Filter to ${esc(p.city)}">${esc(p.city)}</span>`:''}, CA ${esc(p.zip||'')}</div></div>
+      <div style="display:flex;align-items:flex-start;gap:8px">
+        <button class="copybtn" data-copy="${p.id}" aria-label="Copy listing summary" title="Copy summary to clipboard">📋</button>
+        <button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist this listing">★</button>
+        ${vis('score')?`<div class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' · ai '+p.qwenScore:''}</small></div>`:''}
+      </div>
+    </div>
+    <div class="badges">${vis('deal')?dealBadge(p):''}${vis('firm')?firmB:''}${vis('type')?(p.type?`<span class="b type drill" data-typefilter="${esc(p.type).replace(/"/g,'&quot;')}" data-filter="type" data-v="${esc(p.type).replace(/"/g,'&quot;')}" title="Filter to ${esc(p.type)}">${esc(p.type)}</span>`:`<span class="b type">—</span>`):''}${vis('units')?`<span class="b">${p.units} unit${p.units>1?'s':''}</span>`:''}${vis('price')?`<span class="b">${fmt(p.price)}</span>`:''}${vis('ppu')&&ppu(p)&&!p.isCondo?`<span class="b" title="Price per unit">$${ppu(p).toLocaleString()}/unit</span>`:''}${vis('pricePerSf')&&ppsf(p)?`<span class="b" title="Price per building SF">$${ppsf(p).toLocaleString()}/sf</span>`:''}${vis('cap')?capB:''}${vis('verified')?verB:''}${vis('yield')&&f.grossYield?`<span class="b ok" title="Annual gross rent ÷ price">${f.grossYield}% rent yld</span>`:''}${vis('grm')&&grm(p)!=null?`<span class="b" title="Gross rent multiplier — price ÷ annual gross rent (lower = cheaper vs rent)">GRM ${grm(p)}</span>`:''}${vis('yearBuilt')&&p.year_built?`<span class="b" title="Year built">🏗 ${p.year_built}</span>`:''}${vis('dom')&&daysOnMarket(p)!=null?`<span class="b" title="Days since listed">${daysOnMarket(p)}d on mkt</span>`:''}${vis('beds')&&p.beds!=null?`<span class="b">${p.beds} bd</span>`:''}${vis('baths')&&p.baths!=null?`<span class="b">${p.baths} ba</span>`:''}${vis('sqft')&&p.sqft?`<span class="b">${(+p.sqft).toLocaleString()} sf</span>`:''}${vis('hoa')&&p.hoa?`<span class="b" title="HOA dues">$${(+p.hoa).toLocaleString()}/mo</span>`:''}${vis('warr')&&p.warrantable_status?`<span class="b ${p.warrantable_status==='fha_approved'?'ok':'warn'}" title="Condo warrantability (FHA-approval proxy)">${p.warrantable_status==='fha_approved'?'✓ warrantable':'⚑ '+p.warrantable_status}</span>`:''}${vis('broker')&&p.broker_name?`<span class="b" title="Listing broker">🧑‍💼 ${esc(p.broker_name)}</span>`:''}${vis('status')?statusB:''} ${vis('afford')?aff:''} ${vis('rec')?rec:''}</div>
+    ${vis('brokerflag')?brokerFlagBadge:''}
+    ${vis('statusline')?statusLine(p):''}
+    ${vis('txhist')?txHistHTML(bh):''}
+    ${vis('brokerline')?brokerLineHTML(bh):''}
+    ${(vis('coc')||vis('dscr')||vis('noi')||vis('debt')||vis('cashNeeded')||vis('cashFlow'))?`<div class="metrics">
+      ${vis('coc')?`<div><span class="k">Cash-on-cash</span><span class="v ${cls(f.coc,6,3)}">${pct(f.coc)}</span></div>`:''}
+      ${vis('dscr')?`<div><span class="k">DSCR</span><span class="v ${cls(f.dscr,1.25,1.0)}">${f.dscr??'—'}</span></div>`:''}
+      ${vis('noi')?`<div><span class="k">Est. NOI</span><span class="v">${fmt(f.noi)}</span></div>`:''}
+      ${vis('debt')?`<div><span class="k">${scenario==='cash'?'Debt svc':'Debt svc/yr'}</span><span class="v">${fmt(f.annualDebtService)}</span></div>`:''}
+      ${vis('cashNeeded')?`<div><span class="k">Cash needed</span><span class="v ${f.affordable?'pos':'neg'}">${fmt(f.cashNeeded)}</span></div>`:''}
+      ${vis('cashFlow')?`<div><span class="k">Cash flow/yr</span><span class="v ${cls(f.cashFlow,1,0)}">${fmt(f.cashFlow)}</span></div>`:''}
+    </div>`:''}
+    ${vis('thesis')&&p.qwen?.thesis?`<div class="thesis">${esc(p.qwen.thesis)}</div>`:''}
+    ${vis('risks')&&p.qwen?.risks?.length?`<div><span class="small">Risks</span><ul class="lst">${p.qwen.risks.map(r=>`<li>${esc(r)}</li>`).join('')}</ul></div>`:''}
+    ${vis('upside')&&p.upside_note?`<div class="small">💡 ${esc(p.upside_note)}</div>`:''}
+    ${vis('demo')?demoLine(p):''}
+    ${vis('rent')?rentLine(p):''}
+    ${anyAssessorVisible()?`<div class="assessline small" data-addr="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}">📋 assessor…</div>`:''}
+    ${cardAgentBlock(p)}
+  </div>`;
+}
+// Per-listing agent-contact block for the CARD view — every card (any asset type) gets the same
+// contact modal + mark-contacted as the list rows. Compact: name/phone/email + chips + buttons.
+function cardAgentBlock(p){
+  const a=agentInfo(p), id=p.id;
+  const nm=a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a>`:(isPocket(a)?pocketBadge:`<span class="amut">no agent on file</span>`);
+  const chips=`${a.contacted_at?`<span class="achip done" title="Contacted ${esc(a.contacted_at)}">✓ ${fmtWhen(a.contacted_at)}</span>`:''}${a.letters.length?`<span class="achip" title="${a.letters.length} saved letter(s)">✎ ${a.letters.length}</span>`:''}${a.notes?`<span class="achip" title="Has notes">🗒 notes</span>`:''}`;
+  return `<div class="cardagent" id="cardagent-${id}">`
+    +`<div class="ca-top">🧑‍💼 ${nm} ${chips}</div>`
+    +contactBits(a)
+    +window.firmIgInline(a.firm)
+    +`<div class="acts"><button class="abtn contactbtn" data-contact="${id}" title="Open the inline agent window — notes, letters, draft">✎ Open</button>`
+    +`<button class="abtn markbtn" data-mark="${id}" title="${a.contacted_at?'Update contacted date':'Mark contacted (saves the date)'}">${a.contacted_at?'↻':'✓'} Contacted</button></div>`
+    +`</div>`;
+}
+
+// ---- Non-QM target set: unwarrantable condos + small multifamily (1–9u) + mixed-use ----
+function isNonQM(p){
+  const t=(p.type||'').toLowerCase();
+  if(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved') return true;
+  if(/multi/.test(t) && p.units>=1 && p.units<=9) return true;
+  if(/mixed/.test(t)) return true;
+  return false;
+}
+// ---- lazy assessor enrichment (LA County parcel roll via /api/property-history, free) ----
+function anyAssessorVisible(){ return FIELDS.some(f=>f.assessor && vis(f.k)); }
+window.ASSESS = window.ASSESS || {};
+let assessObs = null;
+function assessLine(d){
+  const a=d&&(d.assessor||d.assessorArchive); const bits=[];
+  if(vis('assessed')){ const v=a&&(a.roll_total_value||a.total_value); if(v) bits.push('💰 assessed $'+(+v).toLocaleString()); }
+  if(vis('landimp')&&a){ const l=a.roll_land_value, im=a.roll_imp_value; if(l||im) bits.push('land $'+(+(l||0)).toLocaleString()+' / imp $'+(+(im||0)).toLocaleString()); }
+  if(vis('usecode')&&a){ const u=a.use_desc2||a.use_desc1||a.use_description; if(u) bits.push('🏷 '+u); }
+  if(vis('priorsale')&&d&&d.sales&&d.sales.length){ const s=d.sales[0]; bits.push('last sold $'+(+(s.sold_price||0)).toLocaleString()+' · '+String(s.sold_date||'').slice(0,10)); }
+  return bits.length ? ('📋 '+bits.join(' · ')) : '<span class="na">📋 no assessor record on file</span>';
+}
+function enrichAssessor(){
+  if(assessObs){ assessObs.disconnect(); assessObs=null; }
+  if(!anyAssessorVisible()) return;
+  const els=[...document.querySelectorAll('.assessline')];
+  const queue=[]; let active=0; const MAX=6;
+  function pump(){ while(active<MAX && queue.length){ fetchOne(queue.shift()); } }
+  function fetchOne(el){
+    const addr=el.dataset.addr, city=el.dataset.city, key=(addr+'|'+city).toLowerCase();
+    if(window.ASSESS[key]!==undefined){ el.innerHTML=assessLine(window.ASSESS[key]); pump(); return; }
+    active++;
+    fetch('/api/property-history?address='+encodeURIComponent(addr)+'&city='+encodeURIComponent(city||''))
+      .then(r=>r.json()).then(d=>{ window.ASSESS[key]=d; el.innerHTML=assessLine(d); })
+      .catch(()=>{ el.innerHTML='<span class="na">📋 assessor lookup failed</span>'; })
+      .finally(()=>{ active--; pump(); });
+  }
+  // As cards approach the viewport, queue them for enrichment (concurrency-capped).
+  assessObs=new IntersectionObserver((ents,obs)=>{ ents.forEach(en=>{ if(en.isIntersecting){ obs.unobserve(en.target); queue.push(en.target); pump(); } }); },{rootMargin:'400px'});
+  els.forEach(el=>assessObs.observe(el));
+  // Eager first batch so the top of the grid always resolves (no dependence on a scroll event).
+  els.slice(0,30).forEach(el=>{ assessObs.unobserve(el); queue.push(el); }); pump();
+}
+
+// ---- the rich filter pass ----
+function applyFilters(list){
+  return list.filter(p=>{
+    if(!idxSearchPass(p)) return false;
+    if(F.types.size && !F.types.has(p.type)) return false;
+    if(F.firms.size && !F.firms.has(firmLabel(p))) return false;
+    if(F.cityQuery){ const hay=((p.city||'')+' '+(p.address||'')).toLowerCase(); if(!hay.includes(F.cityQuery)) return false; }
+    if(F.priceMin!=null && p.price<F.priceMin) return false;
+    if(F.priceMax!=null && p.price>F.priceMax) return false;
+    if(F.capMin>0 && !(p.cap_rate!=null && p.cap_rate>=F.capMin)) return false;
+    if(F.unitsMin!=null && !(p.units>=F.unitsMin)) return false;
+    if(F.unitsMax!=null && !(p.units!=null && p.units<=F.unitsMax)) return false;
+    if(F.yearMin!=null && !(p.year_built && p.year_built>=F.yearMin)) return false;
+    if(F.yearMax!=null && !(p.year_built && p.year_built<=F.yearMax)) return false;
+    if(F.status==='active' && /Under Contract|Pending|OUT OF BUDGET/i.test(p.status)) return false;
+    if(F.afford && !p.finance.affordable) return false;
+    if(F.unwarrantable && !(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved')) return false;
+    if(F.nonqm && !isNonQM(p)) return false;
+    if(F.hasPhone||F.hasEmail){ const ci=agentInfo(p); if(F.hasPhone && !ci.phone) return false; if(F.hasEmail && !ci.email) return false; }
+    if(F.shortlist && !SHORT.has(p.id)) return false;
+    return true;
+  });
+}
+// Match everything EXCEPT one facet dimension — powers live facet counts (Amazon-style narrowing).
+function passExcept(p, skip){
+  if(!idxSearchPass(p)) return false;
+  if(skip!=='type' && F.types.size && !F.types.has(p.type)) return false;
+  if(skip!=='firm' && F.firms.size && !F.firms.has(firmLabel(p))) return false;
+  if(skip!=='city' && F.cityQuery){ const hay=((p.city||'')+' '+(p.address||'')).toLowerCase(); if(!hay.includes(F.cityQuery)) return false; }
+  if(F.priceMin!=null && p.price<F.priceMin) return false;
+  if(F.priceMax!=null && p.price>F.priceMax) return false;
+  if(F.capMin>0 && !(p.cap_rate!=null && p.cap_rate>=F.capMin)) return false;
+  if(F.unitsMin!=null && !(p.units>=F.unitsMin)) return false;
+  if(F.unitsMax!=null && !(p.units!=null && p.units<=F.unitsMax)) return false;
+  if(F.yearMin!=null && !(p.year_built && p.year_built>=F.yearMin)) return false;
+  if(F.yearMax!=null && !(p.year_built && p.year_built<=F.yearMax)) return false;
+  if(F.status==='active' && /Under Contract|Pending|OUT OF BUDGET/i.test(p.status)) return false;
+  if(F.afford && !p.finance.affordable) return false;
+  if(F.unwarrantable && !(p.isCondo && p.warrantable_status && p.warrantable_status!=='fha_approved')) return false;
+  if(F.nonqm && !isNonQM(p)) return false;
+  if(F.hasPhone||F.hasEmail){ const ci=agentInfo(p); if(F.hasPhone && !ci.phone) return false; if(F.hasEmail && !ci.email) return false; }
+  if(F.shortlist && !SHORT.has(p.id)) return false;
+  return true;
+}
+function updateFacetCounts(){
+  const tc={}, fc={}, cc={};
+  DATA.ranked.forEach(p=>{
+    if(passExcept(p,'type')) tc[p.type]=(tc[p.type]||0)+1;
+    if(passExcept(p,'firm')){ const fl=firmLabel(p); fc[fl]=(fc[fl]||0)+1; }
+    if(p.city && passExcept(p,'city')) cc[p.city]=(cc[p.city]||0)+1;
+  });
+  const paint=(sel,attr,map)=>$$(sel).forEach(c=>{ const key=c.dataset[attr]; if(key==null) return; const ct=c.querySelector('.ct'); const n=map[key]||0; if(ct) ct.textContent=n; c.classList.toggle('zero', n===0 && !c.classList.contains('active')); });
+  paint('#fType .chip','t',tc); paint('#fFirm .chip','fm',fc); paint('#fCityChips .chip','cy',cc);
+}
+function activeFilterCount(){
+  let n=0;
+  if(F.types.size) n++; if(F.firms.size) n++; if(F.cityQuery) n++;
+  if(F.priceMin!=null||F.priceMax!=null) n++; if(F.capMin>0) n++;
+  if(F.unitsMin!=null) n++; if(F.unitsMax!=null) n++; if(F.yearMin!=null) n++; if(F.yearMax!=null) n++;
+  if(F.status!=='all') n++; if(F.afford) n++; if(F.unwarrantable) n++; if(F.nonqm) n++; if(F.shortlist) n++; if(F.hasPhone) n++; if(F.hasEmail) n++;
+  return n;
+}
+
+const SORTS={ deal:(a,b)=>dealNum(b)-dealNum(a), composite:(a,b)=>b.composite-a.composite, coc:(a,b)=>num(b.finance.coc)-num(a.finance.coc),
+  yield:(a,b)=>num(b.finance.grossYield)-num(a.finance.grossYield),
+  cap:(a,b)=>num(b.cap_rate)-num(a.cap_rate), dscr:(a,b)=>num(b.finance.dscr)-num(a.finance.dscr),
+  priceAsc:(a,b)=>a.price-b.price, priceDesc:(a,b)=>b.price-a.price,
+  cashNeeded:(a,b)=>a.finance.cashNeeded-b.finance.cashNeeded, unitsDesc:(a,b)=>num(b.units)-num(a.units),
+  rentRoi:(a,b)=>rentRoiScore(b)-rentRoiScore(a) };
+const num=v=>v==null?-1e9:v;
+const dealNum=p=>(p.deal && p.deal.score!=null) ? p.deal.score : -1e9;  // unscored sink to the bottom
+
+// ---- active-filter summary bar (removable chips + reset) ----
+function renderActiveBar(){
+  const bar=$('#activebar'); if(!bar) return; const chips=[];
+  F.types.forEach(t=>chips.push(['type:'+t, t]));
+  F.firms.forEach(f=>chips.push(['firm:'+f, '🏢 '+f]));
+  if(F.cityQuery) chips.push(['city', 'city ~ “'+F.cityQuery+'”']);
+  if(F.priceMin!=null) chips.push(['priceMin', '≥ $'+(+F.priceMin).toLocaleString()]);
+  if(F.priceMax!=null) chips.push(['priceMax', '≤ $'+(+F.priceMax).toLocaleString()]);
+  if(F.capMin>0) chips.push(['capMin', 'cap ≥ '+F.capMin+'%']);
+  if(F.unitsMin!=null) chips.push(['unitsMin', '≥ '+F.unitsMin+' units']);
+  if(F.unitsMax!=null) chips.push(['unitsMax', '≤ '+F.unitsMax+' units']);
+  if(F.yearMin!=null) chips.push(['yearMin', 'built ≥ '+F.yearMin]);
+  if(F.yearMax!=null) chips.push(['yearMax', 'built ≤ '+F.yearMax]);
+  if(F.status!=='all') chips.push(['status', 'active only']);
+  if(F.afford) chips.push(['afford', 'in budget']);
+  if(F.unwarrantable) chips.push(['unwarrantable', '⚑ unwarrantable']);
+  if(F.nonqm) chips.push(['nonqm', '◆ non-QM']);
+  if(F.hasPhone) chips.push(['hasPhone', '☎ has phone']);
+  if(F.hasEmail) chips.push(['hasEmail', '✉ has email']);
+  if(F.shortlist) chips.push(['shortlist', '⭐ shortlist']);
+  bar.innerHTML = chips.length
+    ? '<span class="lbl">Filtering</span>'+chips.map(([k,l])=>`<span class="afchip" data-clear="${k.replace(/"/g,'&quot;')}">${esc(l)} <span class="xx">✕</span></span>`).join('')+'<button class="afreset" id="afreset">Reset all ✕</button>'
+    : '';
+}
+function clearFilter(spec){
+  if(spec.startsWith('type:')){ F.types.delete(spec.slice(5)); if(!F.types.has('Condo')) F.unwarrantable=false; }
+  else if(spec.startsWith('firm:')){ F.firms.delete(spec.slice(5)); }
+  else if(spec==='city'){ F.cityQuery=''; if($('#fCity')) $('#fCity').value=''; }
+  else if(spec==='priceMin'){ F.priceMin=null; if($('#fPriceMin')) $('#fPriceMin').value=''; }
+  else if(spec==='priceMax'){ F.priceMax=null; if($('#fPriceMax')) $('#fPriceMax').value=''; }
+  else if(spec==='capMin'){ F.capMin=0; if($('#fCap')){ $('#fCap').value=0; $('#capVal').textContent='any'; } }
+  else if(spec==='unitsMin'){ F.unitsMin=null; if($('#fUnits')) $('#fUnits').value=''; }
+  else if(spec==='unitsMax'){ F.unitsMax=null; if($('#fUnitsMax')) $('#fUnitsMax').value=''; }
+  else if(spec==='yearMin'){ F.yearMin=null; if($('#fYear')) $('#fYear').value=''; }
+  else if(spec==='yearMax'){ F.yearMax=null; if($('#fYearMax')) $('#fYearMax').value=''; }
+  else if(spec==='status'){ F.status='all'; }
+  else if(spec==='afford'){ F.afford=false; if($('#fAfford')) $('#fAfford').checked=false; }
+  else if(spec==='unwarrantable'){ F.unwarrantable=false; }
+  else if(spec==='nonqm'){ F.nonqm=false; }
+  else if(spec==='hasPhone'){ F.hasPhone=false; if($('#fHasPhone')) $('#fHasPhone').checked=false; }
+  else if(spec==='hasEmail'){ F.hasEmail=false; if($('#fHasEmail')) $('#fHasEmail').checked=false; }
+  else if(spec==='shortlist'){ F.shortlist=false; }
+  syncChips(); render();
+}
+// ---- shareable URL state (every filter/sort/view/scenario ↔ query string) ----
+function stateToURL(){
+  const p=new URLSearchParams();
+  if(F.types.size) p.set('t',[...F.types].join(','));
+  if(F.firms.size) p.set('fm',[...F.firms].join('~'));
+  if(F.cityQuery) p.set('city',F.cityQuery);
+  if(F.priceMin!=null) p.set('pmin',F.priceMin);
+  if(F.priceMax!=null) p.set('pmax',F.priceMax);
+  if(F.capMin>0) p.set('cap',F.capMin);
+  if(F.unitsMin!=null) p.set('u',F.unitsMin);
+  if(F.unitsMax!=null) p.set('ux',F.unitsMax);
+  if(F.yearMin!=null) p.set('yr',F.yearMin);
+  if(F.yearMax!=null) p.set('yrx',F.yearMax);
+  if(F.status!=='all') p.set('st',F.status);
+  if(F.afford) p.set('aff','1');
+  if(F.unwarrantable) p.set('unw','1');
+  if(F.nonqm) p.set('nq','1');
+  if(F.hasPhone) p.set('hp','1');
+  if(F.hasEmail) p.set('he','1');
+  if(scenario!=='25') p.set('sc',scenario);
+  const so=$('#sort')&&$('#sort').value; if(so && so!=='deal') p.set('sort',so);
+  if(colSort) p.set('cs',colSort.key+':'+(colSort.dir>0?'a':'d'));
+  if(heat!=='none') p.set('ht',heat);
+  if(view!=='grid') p.set('v',view);
+  const qs=p.toString();
+  // NOTE: the app declares a local `history` (chat log) that shadows window.history — must qualify.
+  try{ window.history.replaceState(null,'',location.pathname+(qs?('?'+qs):'')); }catch(e){}
+}
+// Snapshot the ORIGINAL query at first script-eval — an early render() can call stateToURL()
+// and rewrite location.search (it drops ?v= whenever the current view isn't grid) BEFORE
+// urlToState() runs, clobbering a shared ?v=/?t= link. Read shared state from this frozen copy.
+const INITIAL_SEARCH = location.search;
+function urlToState(){
+  const p=new URLSearchParams(INITIAL_SEARCH); let any=false;
+  if(p.get('t')){ F.types=new Set(p.get('t').split(',').filter(Boolean)); any=true; }
+  if(p.get('fm')){ F.firms=new Set(p.get('fm').split('~').filter(Boolean)); any=true; }
+  if(p.get('city')){ F.cityQuery=p.get('city'); if($('#fCity'))$('#fCity').value=F.cityQuery; any=true; }
+  if(p.get('pmin')){ F.priceMin=+p.get('pmin'); if($('#fPriceMin'))$('#fPriceMin').value=F.priceMin; any=true; }
+  if(p.get('pmax')){ F.priceMax=+p.get('pmax'); if($('#fPriceMax'))$('#fPriceMax').value=F.priceMax; any=true; }
+  if(p.get('cap')){ F.capMin=+p.get('cap'); if($('#fCap')){$('#fCap').value=F.capMin; $('#capVal').textContent=F.capMin+'%+';} any=true; }
+  if(p.get('u')){ F.unitsMin=+p.get('u'); if($('#fUnits'))$('#fUnits').value=F.unitsMin; any=true; }
+  if(p.get('ux')){ F.unitsMax=+p.get('ux'); if($('#fUnitsMax'))$('#fUnitsMax').value=F.unitsMax; any=true; }
+  if(p.get('yr')){ F.yearMin=+p.get('yr'); if($('#fYear'))$('#fYear').value=F.yearMin; any=true; }
+  if(p.get('yrx')){ F.yearMax=+p.get('yrx'); if($('#fYearMax'))$('#fYearMax').value=F.yearMax; any=true; }
+  if(p.get('st')){ F.status=p.get('st'); any=true; }
+  if(p.get('aff')==='1'){ F.afford=true; if($('#fAfford'))$('#fAfford').checked=true; any=true; }
+  if(p.get('unw')==='1'){ F.unwarrantable=true; any=true; }
+  if(p.get('nq')==='1'){ F.nonqm=true; any=true; }
+  if(p.get('hp')==='1'){ F.hasPhone=true; if($('#fHasPhone'))$('#fHasPhone').checked=true; any=true; }
+  if(p.get('he')==='1'){ F.hasEmail=true; if($('#fHasEmail'))$('#fHasEmail').checked=true; any=true; }
+  if(p.get('sc')){ scenario=p.get('sc'); }
+  if(p.get('sort')&&$('#sort')){ $('#sort').value=p.get('sort'); if($('#sortTop'))$('#sortTop').value=p.get('sort'); }
+  if(p.get('v')){ view=p.get('v'); }
+  if(p.get('cs')){ const [k,d]=p.get('cs').split(':'); colSort={key:k,dir:d==='a'?1:-1}; }
+  if(p.get('ht')){ heat=p.get('ht'); }
+  return any;
+}
+function csvEsc(v){ v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"'; }
+function exportCSV(){
+  const cols=listCols();
+  const rows=sortRows(applyFilters(DATA.ranked.slice()));
+  const head=['Address','City','ZIP'].concat(cols.map(c=>c.label));
+  const lines=[head.map(csvEsc).join(',')];
+  rows.forEach(p=>{ const row=[p.address,p.city,p.zip].concat(cols.map(c=>fieldVal(p,c.k))); lines.push(row.map(csvEsc).join(',')); });
+  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([lines.join('\n')],{type:'text/csv'})); a.download='cre-explorer-'+rows.length+'.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000);
+}
+function copyShareLink(){ const t=$('#linkbtn'); try{ navigator.clipboard.writeText(location.href); if(t){ const o=t.textContent; t.textContent='✓ Copied'; setTimeout(()=>t.textContent=o,1400); } }catch(e){ if(t) t.textContent='⌘C to copy'; } }
+// ---- side-by-side compare of shortlisted listings ----
+const CMP=[
+  {l:'Type', f:p=>esc(p.type||'—')},
+  {l:'Price', f:p=>fmt(p.price)},
+  {l:'$/unit', f:p=>{const v=ppu(p);return v?'$'+v.toLocaleString():'—';}},
+  {l:'$/SF', f:p=>{const v=ppsf(p);return v?'$'+v.toLocaleString():'—';}},
+  {l:'Units', f:p=>p.units??'—'},
+  {l:'Cap rate', f:p=>p.cap_rate!=null?p.cap_rate+'%':'—'},
+  {l:'DSCR', f:p=>p.finance&&p.finance.dscr!=null?p.finance.dscr:'—'},
+  {l:'Cash-on-cash', f:p=>p.finance&&p.finance.coc!=null?p.finance.coc+'%':'—'},
+  {l:'Cash needed', f:p=>p.finance?fmt(p.finance.cashNeeded):'—'},
+  {l:'Cash flow/yr', f:p=>p.finance?fmt(p.finance.cashFlow):'—'},
+  {l:'Est. rent', f:p=>{const e=rentEstimate(p);return e?'$'+e.std.toLocaleString()+'/mo':'—';}},
+  {l:'Beds / Baths', f:p=>(p.beds!=null||p.baths!=null)?((p.beds??'—')+' / '+(p.baths??'—')):'—'},
+  {l:'SqFt', f:p=>p.sqft?(+p.sqft).toLocaleString():'—'},
+  {l:'Year built', f:p=>p.year_built||'—'},
+  {l:'Warrantability', f:p=>esc(p.warrantable_status||'—')},
+  {l:'Deal score', f:p=>p.deal&&p.deal.score!=null?p.deal.score:'—'},
+  {l:'Invest score', f:p=>p.composite??'—'},
+];
+function openCompare(){
+  const ov=$('#cmpov'), body=$('#cmpbody');
+  const items=DATA.ranked.filter(p=>SHORT.has(p.id));
+  if(items.length<2){ body.innerHTML='<div class="cmphint">Star at least 2 listings (the ★ on any card) to compare them side by side.</div>'; ov.classList.add('on'); return; }
+  const head='<tr><th class="cmpk" scope="col"></th>'+items.map(p=>`<th scope="col">${esc(p.address)}<div class="cmpsub">${esc(p.city)}</div></th>`).join('')+'</tr>';
+  const rows=CMP.map(m=>`<tr><td class="cmpk">${m.l}</td>`+items.map(p=>`<td>${m.f(p)}</td>`).join('')+'</tr>').join('');
+  body.innerHTML=`<div class="cmptools no-print"><button id="cmpcsv">⬇ CSV</button><button id="cmpprint">🖨 Print</button></div><table class="cmptbl"><thead>${head}</thead><tbody>${rows}</tbody></table>`;
+  ov.classList.add('on');
+}
+function decodeEnt(s){ return String(s).replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"').replace(/&#39;/g,"'"); }
+function exportCompareCSV(){
+  const items=DATA.ranked.filter(p=>SHORT.has(p.id)); if(items.length<2) return;
+  const cols=['Address','City'].concat(CMP.map(m=>m.l));
+  const lines=[cols.map(csvEsc).join(',')];
+  items.forEach(p=>{ const row=[p.address,p.city].concat(CMP.map(m=>decodeEnt(m.f(p)))); lines.push(row.map(csvEsc).join(',')); });
+  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([lines.join('\n')],{type:'text/csv'})); a.download='cre-compare-'+items.length+'.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000);
+}
+function printCompare(){ document.body.classList.add('printing-compare'); window.print(); }
+// ---- paginated DOM (perf): render in pages, "Show more" appends ----
+let _rows=[], _shown=0;
+const PAGE=()=> view==='grid'?60:200;
+function moreBtn(){ return _shown<_rows.length ? `<div class="showmore-wrap"><button class="showmore" id="showmore">Show ${Math.min(PAGE(),_rows.length-_shown)} more · showing ${_shown} of ${_rows.length}</button></div>` : ''; }
+// Infinite scroll: auto-trigger the "show more" grow when it scrolls near view (reuses the
+// existing #showmore click handler → _shown+=PAGE → paintPage). Viewport-rooted so it works
+// whether the page or an inner pane scrolls. Re-pointed at the fresh button after each repaint.
+let _moreIO=null, _moreBusy=false;
+function wireAutoMore(){
+  const btn=document.getElementById('showmore'); if(!btn) return;
+  if(typeof IntersectionObserver==='undefined') return;
+  if(!_moreIO) _moreIO=new IntersectionObserver(es=>{ if(_moreBusy) return; if(es.some(e=>e.isIntersecting)){ const b=document.getElementById('showmore'); if(b){ _moreBusy=true; b.click(); setTimeout(()=>{_moreBusy=false;},60); } } },{rootMargin:'500px 0px'});
+  _moreIO.disconnect(); _moreIO.observe(btn);
+}
+function paintPage(){
+  const g=$('#grid'); const r=_rows; const slice=r.slice(0,_shown);
+  if(!r.length){ g.className='grid'; g.innerHTML=`<div class="empty">No properties match these filters.<br><span class="small">Loosen a filter or hit “Reset all”.</span></div>`; return; }
+  if(view==='table'){                                    // full columnar spreadsheet (sortable + drag-reorder + resizable cols)
+    g.className='listhost'; g.innerHTML=renderTableGrouped(slice)+moreBtn();
+    const csig=listCols().map(c=>c.k).join(',');
+    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
+  }
+  else if(view==='kv'){                                  // key→value block per record (deep inspection)
+    g.className='kvhost'; g.innerHTML=renderKVGrouped(slice)+moreBtn();
+  }
+  else if(view==='list'){                                // compact one-row-per-record: identity + inline summary chips
+    g.className='compacthost'; g.innerHTML=renderCompactGrouped(slice)+moreBtn();
+  }
+  else { g.className='grid'; g.innerHTML=groupWrap(slice,card,'grid')+moreBtn(); enrichAssessor(); }
+  applyHeat(slice);
+  resolveContacts(slice);   // fetch REAL phones for the visible agents, then repaint (TK-10687)
+  wireAutoMore();           // convert the show-more button into infinite scroll
+}
+// ===== nav-agent inline view renderers: List (compact) / KV / Table + group-by-any-field =====
+// The rail sort already ordered rows; sortRows() also front-sorts by the group key when groupBy is set,
+// so groups arrive contiguous here — we just detect a key change and emit a header band.
+function groupKeyLabel(p){ if(!groupBy) return ''; let v=fieldVal(p,groupBy); if(v==null||v==='') return '(none)'; return String(v); }
+// Generic grouping wrapper for non-table views: emits a heading band before each group.
+function groupWrap(rows, renderOne, kind){
+  if(!groupBy){ return rows.map(renderOne).join(''); }
+  let out='', last=null;
+  rows.forEach(p=>{ const gk=groupKeyLabel(p); if(gk!==last){ last=gk;
+    out+=`<div class="grouphead grouphead-${kind}">${esc(fieldLabel(groupBy))}: <b>${esc(gk)}</b></div>`; }
+    out+=renderOne(p); });
+  return out;
+}
+function fieldLabel(k){ if(k==='addr') return 'Property'; const f=FIELDS.find(x=>x.k===k); return f?f.label:k; }
+// ---- compact List: address identity + inline summary chips from the visible columns ----
+function compactChips(p){
+  const cols=listCols().slice(0,7);   // a handful of the visible fields as inline chips (keeps the row light)
+  return cols.map(c=>`<span class="cchip"><span class="cck">${esc(c.label)}</span> ${colCell(c.k,p)}</span>`).join('');
+}
+function renderCompactRow(p){
+  return `<div class="crow" data-id="${p.id}">`
+    +`<button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button>`
+    +`<div class="cid"><span class="cla">${esc(p.address)}</span><span class="clc">${esc(p.city)}, CA ${p.zip||''}</span></div>`
+    +`<div class="cfields">${compactChips(p)}</div></div>`;
+}
+function renderCompactGrouped(rows){ return `<div class="compactlist">${groupWrap(rows,renderCompactRow,'compact')}</div>`; }
+// ---- KV: each record a labeled key→value block, every visible field vertical ----
+function renderKVCard(p){
+  const cols=listCols();
+  const rowsHtml=[`<div class="kvrow"><span class="kvk">Property</span><span class="kvv">${esc(p.address)}, ${esc(p.city)} CA ${p.zip||''}</span></div>`]
+    .concat(cols.map(c=>`<div class="kvrow"><span class="kvk">${esc(c.label)}</span><span class="kvv">${colCell(c.k,p)}</span></div>`)).join('');
+  return `<div class="kvcard" data-id="${p.id}">`
+    +`<div class="kvhead"><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="Shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button><span class="kvaddr">${esc(p.address)}</span></div>`
+    +rowsHtml+`</div>`;
+}
+function renderKVGrouped(rows){ return `<div class="kvgrid">${groupWrap(rows,renderKVCard,'kv')}</div>`; }
+// ---- Table: reuse the full renderList() spreadsheet, injecting group header rows when grouped ----
+function renderTableGrouped(rows){
+  if(!groupBy) return renderList(rows);
+  // Build the grouped body DIRECTLY (no regex-splice of renderList's output) so col-resize sees the
+  // same thead/table shell and we never rebuild a body just to discard it.
+  const ncols=listCols().length+1;
+  let bodyRows='', last=null;
+  rows.forEach(p=>{ const gk=groupKeyLabel(p);
+    if(gk!==last){ last=gk; bodyRows+=`<tr class="grouprow"><td class="groupcell" colspan="${ncols}">${esc(fieldLabel(groupBy))}: <b>${esc(gk)}</b></td></tr>`; }
+    bodyRows+=renderListBodyRow(p); });
+  return renderListShell(bodyRows);
+}
+// The table shell (listwrap + thead), body injected. Shared by renderList() and the grouped table so
+// the <thead>/sortable/drag headers are identical in both paths (col-resize signature stays stable).
+function renderListShell(bodyHtml){
+  const cols=listCols();
+  const arrow=k=>colSort&&colSort.key===k?(colSort.dir>0?' asc':' desc'):'';
+  const head='<th scope="col" class="stick sortable'+arrow('addr')+'" data-sortk="addr" title="Sort by address">Property</th>'+cols.map(c=>`<th scope="col" class="sortable dragcol${arrow(c.k)}" draggable="true" data-sortk="${c.k}" data-k="${c.k}" title="Drag to reorder · click to sort by ${esc(c.label)}">${esc(c.label)}</th>`).join('');
+  return `<div class="listwrap"><table class="listtbl" data-no-sort><thead><tr>${head}</tr></thead><tbody>${bodyHtml}</tbody></table></div>`;
+}
+// One <tr> body row, factored out of renderList so the grouped table can interleave group headers.
+function renderListBodyRow(p){
+  const cols=listCols();
+  return `<tr class="lrow" data-id="${p.id}">`
+    +`<td class="stick"><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button> <span class="la">${esc(p.address)}</span><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div>${sourceLink(p)?`<div class="lsrc">${sourceLink(p)}</div>`:''}</td>`
+    +cols.map(c=>`<td>${colCell(c.k,p)}</td>`).join('')+`</tr>`;
+}
+// ---- saved searches (name + restore the full shareable-URL state) ----
+const SAVED=(function(){ try{ return JSON.parse(localStorage.getItem('cre_saved')||'[]'); }catch(e){ return []; } })();
+function saveSaved(){ try{ localStorage.setItem('cre_saved', JSON.stringify(SAVED)); }catch(e){} }
+function buildSavedList(){ const el=$('#savedList'); if(!el) return;
+  el.innerHTML = SAVED.length
+    ? SAVED.map((s,i)=>`<div class="savedrow"><button class="savedname" data-qs="${(s.qs||'').replace(/"/g,'&quot;')}" title="Restore this saved view">📁 ${esc(s.name)}</button><button class="saveddel" data-del="${i}" aria-label="Delete saved search">✕</button></div>`).join('')
+    : '<div class="savedempty">None yet — filter, then “Save current view”.</div>';
+}
+// ---- per-listing clipboard summary (honest fields only) ----
+function cardSummary(p){ const f=p.finance||{}; const L=[];
+  L.push(`${p.address}, ${p.city} CA ${p.zip||''}`.trim());
+  const l2=[p.type, p.units?`${p.units} unit${p.units>1?'s':''}`:null, p.price?fmt(p.price):null].filter(Boolean).join(' · '); if(l2) L.push(l2);
+  const m=[]; if(p.cap_rate!=null) m.push('Cap '+p.cap_rate+'%'); if(ppu(p)&&!p.isCondo) m.push('$'+ppu(p).toLocaleString()+'/unit'); if(ppsf(p)) m.push('$'+ppsf(p).toLocaleString()+'/SF'); if(m.length) L.push(m.join(' · '));
+  const fin=[]; if(f.dscr!=null) fin.push('DSCR '+f.dscr); if(f.coc!=null) fin.push('CoC '+f.coc+'%'); if(f.cashNeeded!=null) fin.push('Cash needed '+fmt(f.cashNeeded)); if(f.cashFlow!=null) fin.push('Cash flow/yr '+fmt(f.cashFlow)); if(fin.length) L.push(fin.join(' · '));
+  const r=rentEstimate(p); const ex=[]; if(r) ex.push('Est. rent $'+r.std.toLocaleString()+'/mo'); if(p.deal&&p.deal.score!=null) ex.push('Deal score '+p.deal.score+'/100'); if(p.warrantable_status) ex.push('Warrantability: '+p.warrantable_status); if(ex.length) L.push(ex.join(' · '));
+  if(p.source) L.push(p.source);
+  return L.join('\n');
+}
+function copySummary(id, btn){ const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
+  const done=()=>{ if(btn){ btn.textContent='✓'; setTimeout(()=>btn.textContent='📋',1200); } };
+  try{ navigator.clipboard.writeText(cardSummary(p)).then(done).catch(done); }catch(e){ done(); }
+}
+function applyQuick(k){
+  if(k==='condo'){ F.types=new Set(['Condo']); }
+  else if(k==='mf'){ F.types=new Set(['Multifamily']); }
+  else if(k==='nonqm'){ F.nonqm=true; }
+  else if(k==='unw'){ F.unwarrantable=true; F.types=new Set(['Condo']); }
+  else if(k==='afford'){ F.afford=true; if($('#fAfford')) $('#fAfford').checked=true; }
+  else if(k==='short'){ F.shortlist=true; }
+  syncChips(); render();
+}
+function median(a){ if(!a.length) return null; const s=a.slice().sort((x,y)=>x-y); const m=Math.floor(s.length/2); return s.length%2?s[m]:(s[m-1]+s[m])/2; }
+function renderStats(r){
+  const sb=$('#statsbar'); if(!sb) return;
+  if(!r||!r.length){ sb.innerHTML=''; return; }
+  const prices=r.map(p=>p.price).filter(v=>v>0);
+  const ppus=r.map(p=>ppu(p)).filter(v=>v>0);
+  const ppsfs=r.map(p=>ppsf(p)).filter(v=>v>0);
+  const caps=r.map(p=>p.cap_rate).filter(v=>v!=null);
+  const aff=r.filter(p=>p.finance&&p.finance.affordable).length;
+  const rents=r.map(p=>{ const e=rentEstimate(p); return e?e.std:null; }).filter(v=>v);
+  const pill=(l,v)=>v==null?'':`<span class="statpill"><span class="sl">${l}</span> <b>${v}</b></span>`;
+  sb.innerHTML =
+    pill('Median price', prices.length?'$'+Math.round(median(prices)).toLocaleString():null)
+    +pill('Median $/unit', ppus.length?'$'+Math.round(median(ppus)).toLocaleString():null)
+    +pill('Median $/SF', ppsfs.length?'$'+Math.round(median(ppsfs)).toLocaleString():null)
+    +pill('Avg cap', caps.length?(caps.reduce((a,b)=>a+(+b),0)/caps.length).toFixed(2)+'% ('+caps.length+' disc.)':null)
+    +pill('Median est. rent', rents.length?'$'+Math.round(median(rents)).toLocaleString()+'/mo':null)
+    +pill('In budget', aff+' of '+r.length);
+}
+function render(){
+  const g=$('#grid'); const fc=activeFilterCount();
+  $('#fcount').textContent=fc; $('#fcount').classList.toggle('on',fc>0);
+  { const sb=$('#shortbtn'); if(sb){ sb.textContent='⭐ Shortlist ('+SHORT.size+')'; sb.classList.toggle('shorton',F.shortlist); } }
+  renderActiveBar(); stateToURL(); updateFacetCounts();
+  // Wait-for-selection: on a clean load (no filter chosen) don't dump 2,134 cards — prompt for a pick.
+  // A free-text search counts as a selection, so typing in the search box shows results immediately.
+  if(fc===0 && !F.cityQuery && !idxQuery){
+    g.className='grid';
+    g.innerHTML=`<div class="empty">👈 Expand a filter tab on the left — or start with one of these:
+      <div class="quickstart">
+        <button class="qchip" data-quick="condo">🏢 Condos</button>
+        <button class="qchip" data-quick="mf">🏠 Multifamily</button>
+        <button class="qchip" data-quick="nonqm">◆ Non-QM</button>
+        <button class="qchip" data-quick="unw">⚑ Unwarrantable</button>
+        <button class="qchip" data-quick="afford">✓ In budget</button>
+        ${SHORT.size?`<button class="qchip" data-quick="short">⭐ My shortlist (${SHORT.size})</button>`:''}
+      </div>
+      <span class="small">${(DATA.ranked.length).toLocaleString()} listings loaded · ${DATA.meta.market} — results appear instantly; your active filters show as removable chips up top.</span></div>`;
+    $('#count').innerHTML=`<b>${(DATA.ranked.length).toLocaleString()}</b> loaded · choose a filter to view`;
+    $('#statsbar').innerHTML='';
+    saveState(); return;
+  }
+  let r = applyFilters(DATA.ranked.slice());
+  sortRows(r);
+  renderStats(r);
+  _rows=r; _shown=Math.min(PAGE(), r.length);   // paginate the DOM; stats/count/facets still use the full set
+  paintPage();
+  $('#count').innerHTML = `<b>${r.length}</b> of ${DATA.ranked.length} properties${fc?` · ${fc} filter${fc>1?'s':''} active`:''}`;
+  $('#foot').textContent = `${scenario==='cash'?'all-cash':SCEN[scenario].downPct+'% down @ '+SCEN[scenario].ratePct+'%'} · * →X% = projected cap after value-add (not in-place). Cap rates broker-stated — verify NOI in DD.`;
+  saveState();
+}
+
+// ---- build the filter UI from the data ----
+function buildFacets(){
+  const tCount={}, fCount={}, cCount={};
+  DATA.ranked.forEach(p=>{ tCount[p.type]=(tCount[p.type]||0)+1; const fl=firmLabel(p); fCount[fl]=(fCount[fl]||0)+1; if(p.city) cCount[p.city]=(cCount[p.city]||0)+1; });
+  const chipHTML=(obj,key)=>Object.entries(obj).sort((a,b)=>b[1]-a[1])
+    .map(([k,c])=>`<span class="chip" data-${key}="${k.replace(/"/g,'&quot;')}">${k}<span class="ct">${c}</span></span>`).join('');
+  // Condos are merged into the grid as a real 'Condo' asset type (chip comes from the data below).
+  // Prepend one special in-grid filter: ⚑ Unwarrantable = Condo AND not FHA-approved (non-QM target).
+  const nonqmChip='<span class="chip" data-warr="nonqm" style="border-color:rgba(188,140,255,.55);color:var(--purp,#bc8cff)" title="Non-QM target set: unwarrantable condos + small multifamily (1–9 units) + mixed-use">◆ Non-QM</span>';
+  const unwChip='<span class="chip" data-warr="unwarrantable" style="border-color:rgba(248,81,73,.55);color:var(--bad)" title="Non-FHA-approved condos — filters the grid to non-QM targets">⚑ Unwarrantable</span>';
+  $('#fType').innerHTML=nonqmChip+unwChip+chipHTML(tCount,'t');
+  $('#fFirm').innerHTML=chipHTML(fCount,'fm');
+  // top 14 cities as quick chips; the search box covers the rest
+  const topCities=Object.entries(cCount).sort((a,b)=>b[1]-a[1]).slice(0,14);
+  $('#fCityChips').innerHTML=topCities.map(([k,c])=>`<span class="chip" data-cy="${k.replace(/"/g,'&quot;')}">${k}<span class="ct">${c}</span></span>`).join('');
+  // sort options mirrored into the top bar
+  $('#sortTop').innerHTML=$('#sort').innerHTML;
+}
+
+// ---- chat context ----
+function getContext(){
+  const lines=[];
+  lines.push(`MARKET: ${DATA.meta.market}. Budget: $400,000 down. Active scenario: ${scenario==='cash'?'ALL CASH':SCEN[scenario].downPct+'% down, '+SCEN[scenario].ratePct+'%/'+SCEN[scenario].amortYears+'yr'}.`);
+  lines.push(`Active filters: types=[${[...F.types]}], firms=[${[...F.firms]}], city~"${F.cityQuery}", price ${F.priceMin||'any'}-${F.priceMax||'any'}, capMin=${F.capMin}, unitsMin=${F.unitsMin||'any'}, unitsMax=${F.unitsMax||'any'}, builtAfter=${F.yearMin||'any'}, builtBefore=${F.yearMax||'any'}, status=${F.status}, inBudgetOnly=${F.afford}, sort=${$('#sort').value}.`);
+  lines.push('RANKED PROPERTIES (current scenario):');
+  applyFilters(DATA.ranked).slice(0,120).forEach(p=>{ const f=p.finance;
+    lines.push(`#${p.rank} id=${p.id} | ${p.address}, ${p.city} | ${p.type} | $${p.price.toLocaleString()} | ${p.units}u | cap ${p.cap_rate!=null?p.cap_rate+'%':'n/d'}${p.verified===true?' VERIFIED':''} | firm ${firmLabel(p)} | CoC ${f.coc!=null?f.coc+'%':'n/a'} | DSCR ${f.dscr??'n/a'} | cash needed $${f.cashNeeded.toLocaleString()} ${f.affordable?'(in budget)':'(OVER)'} | composite ${p.composite} | status ${p.status}`);
+  });
+  if(DATA.comps && DATA.comps.submarket_cap_benchmarks){
+    lines.push('\nSUBMARKET CAP BENCHMARKS:');
+    Object.entries(DATA.comps.submarket_cap_benchmarks).forEach(([k,v])=>lines.push(`- ${k}: ${v}`));
+  }
+  const lc = Object.entries(window.liveComps||{}).filter(([,v])=>v&&v.length);
+  if(lc.length){ lines.push('\nLIVE COMPS THIS SESSION:'); lc.forEach(([id,arr])=>{ const p=DATA.ranked.find(x=>x.id===id); lines.push(`for ${p?p.address:id}:`); arr.forEach(c=>lines.push(`  - ${c.title} | ${[c.price,c.cap,c.units,c.sf].filter(Boolean).join(' · ')}`)); }); }
+  return lines.join('\n');
+}
+
+// ---- chat-issued actions (extended for the rich filters) ----
+const TYPE_ALIASES={multifamily:'Multifamily',retail:'Retail',office:'Office',industrial:'Industrial','mixed':'Mixed-use','mixed-use':'Mixed-use',land:'Land',hospitality:'Hospitality'};
+function applyAction(a){
+  const done=[];
+  if(a.scenario && SCEN[String(a.scenario)]){ setScenario(String(a.scenario)); done.push('scenario→'+a.scenario); }
+  if(a.sort && SORTS[a.sort]){ $('#sort').value=a.sort; $('#sortTop').value=a.sort; done.push('sort→'+a.sort); }
+  const setType=v=>{ const key=TYPE_ALIASES[String(v).toLowerCase()]; if(key){ F.types=new Set([key]); done.push('type→'+key); } };
+  if(a.type) setType(a.type);
+  if(a.filter && a.filter!=='all' && a.filter!=='active' && a.filter!=='affordable') setType(a.filter);
+  if(a.filter==='affordable'){ F.afford=true; done.push('in-budget only'); }
+  if(a.filter==='active'){ F.status='active'; done.push('active only'); }
+  if(a.filter==='all'){ F.types.clear(); F.afford=false; F.status='all'; done.push('cleared'); }
+  if(a.firm){ F.firms=new Set([a.firm]); done.push('firm→'+a.firm); }
+  if(a.city){ F.cityQuery=String(a.city).toLowerCase(); $('#fCity').value=a.city; done.push('city→'+a.city); }
+  if(a.maxPrice!=null && !isNaN(+a.maxPrice)){ F.priceMax=+a.maxPrice; $('#fPriceMax').value=+a.maxPrice; done.push('max $'+(+a.maxPrice).toLocaleString()); }
+  if(a.minPrice!=null && !isNaN(+a.minPrice)){ F.priceMin=+a.minPrice; $('#fPriceMin').value=+a.minPrice; done.push('min $'+(+a.minPrice).toLocaleString()); }
+  if(a.capMin!=null && !isNaN(+a.capMin)){ F.capMin=+a.capMin; $('#fCap').value=+a.capMin; $('#capVal').textContent=F.capMin?F.capMin+'%+':'any'; done.push('cap ≥'+a.capMin+'%'); }
+  if(a.unitsMin!=null && !isNaN(+a.unitsMin)){ F.unitsMin=+a.unitsMin; $('#fUnits').value=+a.unitsMin; done.push('units ≥'+a.unitsMin); }
+  if(a.unitsMax!=null && !isNaN(+a.unitsMax)){ F.unitsMax=+a.unitsMax; $('#fUnitsMax').value=+a.unitsMax; done.push('units ≤'+a.unitsMax); }
+  if(a.yearMin!=null && !isNaN(+a.yearMin)){ F.yearMin=+a.yearMin; $('#fYear').value=+a.yearMin; done.push('built ≥'+a.yearMin); }
+  if(a.yearMax!=null && !isNaN(+a.yearMax)){ F.yearMax=+a.yearMax; $('#fYearMax').value=+a.yearMax; done.push('built ≤'+a.yearMax); }
+  syncChips(); render();
+  if(a.highlightId){ const el=$('#card-'+a.highlightId); if(el){ el.scrollIntoView({behavior:'smooth',block:'center'}); el.classList.add('flash'); setTimeout(()=>el.classList.remove('flash'),2200); done.push('→'+a.highlightId); } }
+  return done;
+}
+
+function setScenario(s){ scenario=s; localStorage.setItem('cre_scenario',s);
+  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===s)); recompute(); }
+
+function syncChips(){
+  $$('#fType .chip').forEach(c=>{ if(c.dataset.warr==='unwarrantable') c.classList.toggle('active',F.unwarrantable); else if(c.dataset.warr==='nonqm') c.classList.toggle('active',F.nonqm); else c.classList.toggle('active',F.types.has(c.dataset.t)); });
+  $$('#fFirm .chip').forEach(c=>c.classList.toggle('active',F.firms.has(c.dataset.fm)));
+  $$('#fCityChips .chip').forEach(c=>c.classList.toggle('active',F.cityQuery===c.dataset.cy.toLowerCase()));
+  $$('#fStatus .chip').forEach(c=>c.classList.toggle('active',F.status===c.dataset.st));
+}
+
+// ---- Top-of-page natural-language chat bar (client-side; drives F via applyAction) ----
+const CHAT_TYPES=[['multifamily','Multifamily'],['multi-family','Multifamily'],['multi family','Multifamily'],['apartment','Multifamily'],['office','Office'],['industrial','Industrial'],['retail','Retail'],['mixed-use','Mixed-use'],['mixed use','Mixed-use'],['net lease','Retail'],['net-lease','Retail'],['nnn','Retail'],['land','Land'],['hotel','Hospitality'],['hospitality','Hospitality']];
+function chatMoney(s){ s=String(s).toLowerCase().replace(/[,\s$]/g,''); let n=parseFloat(s); if(isNaN(n))return null; if(/m/.test(s))n*=1e6; else if(/k/.test(s))n*=1e3; return Math.round(n); }
+function chatCities(){ try{ return [...new Set((DATA||[]).map(r=>(r.city||'').trim()).filter(Boolean))].sort((a,b)=>b.length-a.length); }catch(e){ return []; } }
+function chatClear(){
+  F.types.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.unwarrantable=F.nonqm=false;
+  ['#fCity','#fPriceMin','#fPriceMax','#fUnits','#fUnitsMax','#fYear','#fYearMax'].forEach(x=>{const e=$(x); if(e)e.value='';});
+  const c=$('#fCap'); if(c){ c.value=0; const v=$('#capVal'); if(v)v.textContent='any'; }
+  syncChips(); render();
+}
+function applyChat(raw){
+  const msg=$('#chatmsg'); const s=' '+String(raw||'').toLowerCase().replace(/\s+/g,' ').trim()+' ';
+  if(!s.trim())return;
+  if(/^\s*(clear|reset|all|everything|start over|show all)\s*$/.test(s)){ chatClear(); if(msg)msg.textContent='Cleared'; return; }
+  // fresh search — clear chat-controlled fields + their drawer inputs first
+  F.types.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.unwarrantable=F.nonqm=false;
+  ['#fCity','#fPriceMin','#fPriceMax','#fUnits','#fUnitsMax','#fYear','#fYearMax'].forEach(x=>{const e=$(x); if(e)e.value='';});
+  const a={}; const got=[]; let m;
+  // warrantability / condo
+  if(/unwarrantable|non-?warrantable|not warrantable/.test(s)){ F.unwarrantable=true; got.push('unwarrantable condos'); }
+  if(/non-?qm\b|non qm/.test(s)){ F.nonqm=true; got.push('non-QM'); }
+  if(/\bcondos?\b/.test(s) && !F.unwarrantable){ F.types.add('Condo'); got.push('Condo'); }
+  // financing scenario (the seg now lives in the left drawer)
+  if(/\ball\s*cash\b/.test(s)){ a.scenario='cash'; got.push('all cash'); }
+  else if(/\b30\s*%?\s*down\b/.test(s)){ a.scenario='30'; got.push('30% down'); }
+  else if(/\b25\s*%?\s*down\b/.test(s)){ a.scenario='25'; got.push('25% down'); }
+  // units
+  const WORDU={duplex:2,triplex:3,fourplex:4,fiveplex:5};
+  for(const w in WORDU){ if(s.includes(' '+w)){ a.unitsMin=a.unitsMax=WORDU[w]; got.push(WORDU[w]+' units'); } }
+  if(m=s.match(/(\d+)\s*(?:-|to)\s*(\d+)\s*unit/)){ a.unitsMin=+m[1]; a.unitsMax=+m[2]; got.push(m[1]+'–'+m[2]+' units'); }
+  else if(m=s.match(/(?:under|below|less than|up to|max(?:imum)?|<)\s*(\d+)\s*unit/)){ a.unitsMax=+m[1]; got.push('≤'+m[1]+' units'); }
+  else if(m=s.match(/(?:over|above|more than|at least|min(?:imum)?|\+|>)\s*(\d+)\s*unit/)){ a.unitsMin=+m[1]; got.push(m[1]+'+ units'); }
+  else if(m=s.match(/(\d+)\s*unit/)){ a.unitsMin=a.unitsMax=+m[1]; got.push(m[1]+' units'); }
+  // type (set on F directly so Condo/multi don't get clobbered by applyAction's single-type set)
+  const types=[]; for(const [k,v] of CHAT_TYPES){ if(s.includes(' '+k) && !types.includes(v)) types.push(v); }
+  types.forEach(v=>F.types.add(v)); if(types.length) got.push(types.join('+'));
+  // city (known city names from the data; longest-first to avoid substring collisions)
+  for(const c of chatCities()){ const cl=c.toLowerCase(); if(s.includes(' '+cl+' ') || s.includes(' in '+cl)){ a.city=c; got.push(c); break; } }
+  // price
+  if((m=s.match(/\$?\s*([\d.,]+\s*[mk]?)\s*(?:-|to)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b/.test(m[0])){ a.minPrice=chatMoney(m[1]); a.maxPrice=chatMoney(m[2]); got.push('$'+m[1].trim()+'–$'+m[2].trim()); }
+  else if((m=s.match(/(?:under|below|less than|up to|<)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b|\d{4,}/.test(m[0])){ a.maxPrice=chatMoney(m[1]); got.push('< $'+m[1].trim()); }
+  else if((m=s.match(/(?:over|above|more than|at least|>)\s*\$?\s*([\d.,]+\s*[mk]?)/)) && /\$|[mk]\b|\d{4,}/.test(m[0])){ a.minPrice=chatMoney(m[1]); got.push('> $'+m[1].trim()); }
+  // cap rate
+  if(m=s.match(/(\d+(?:\.\d+)?)\s*%?\s*cap/) || (m=s.match(/cap\s*(?:rate)?\s*(?:over|above|min|>|of)?\s*(\d+(?:\.\d+)?)/))){ a.capMin=+m[1]; got.push(m[1]+'%+ cap'); }
+  // year
+  if(m=s.match(/(?:built\s*after|newer than|after|since)\s*(\d{4})/)){ a.yearMin=+m[1]; got.push('after '+m[1]); }
+  if(m=s.match(/(?:built\s*before|older than|before)\s*(\d{4})/)){ a.yearMax=+m[1]; got.push('before '+m[1]); }
+  applyAction(a);   // maps a.* → F + drawer inputs, then syncChips()+render()
+  if(msg) msg.textContent = got.length ? ('Showing: '+got.join(' · ')) : ('Couldn’t parse — try “4 units in Los Angeles”');
+}
+(function(){ const q=$('#chatq'), go=$('#chatgo'), cl=$('#chatclear'), ex=$('#chatex');
+  if(q) q.addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); applyChat(q.value); } });
+  if(go) go.addEventListener('click',()=>applyChat(q?q.value:''));
+  if(cl) cl.addEventListener('click',()=>{ if(q)q.value=''; chatClear(); const m=$('#chatmsg'); if(m)m.textContent='Cleared'; });
+  if(ex) ex.addEventListener('click',e=>{ const c=e.target.closest('.chatchip'); if(!c)return; if(q)q.value=c.dataset.q; applyChat(c.dataset.q); });
+})();
+
+// ---- persistence ----
+function saveState(){ try{ localStorage.setItem('cre_filters', JSON.stringify({
+  types:[...F.types],firms:[...F.firms],cityQuery:F.cityQuery,priceMin:F.priceMin,priceMax:F.priceMax,
+  capMin:F.capMin,unitsMin:F.unitsMin,unitsMax:F.unitsMax,yearMin:F.yearMin,yearMax:F.yearMax,status:F.status,afford:F.afford,hasPhone:F.hasPhone,hasEmail:F.hasEmail,sort:$('#sort').value })); }catch(e){} }
+function loadState(){ try{ const s=JSON.parse(localStorage.getItem('cre_filters')||'{}');
+  if(s.types) F.types=new Set(s.types); if(s.firms) F.firms=new Set(s.firms);
+  F.cityQuery=s.cityQuery||''; F.priceMin=s.priceMin??null; F.priceMax=s.priceMax??null;
+  F.capMin=s.capMin||0; F.unitsMin=s.unitsMin??null; F.unitsMax=s.unitsMax??null; F.yearMin=s.yearMin??null; F.yearMax=s.yearMax??null; F.status=s.status||'all'; F.afford=!!s.afford; F.hasPhone=!!s.hasPhone; F.hasEmail=!!s.hasEmail;
+  if(s.cityQuery) $('#fCity').value=s.cityQuery; if(s.priceMin) $('#fPriceMin').value=s.priceMin; if(s.priceMax) $('#fPriceMax').value=s.priceMax;
+  if(s.capMin){ $('#fCap').value=s.capMin; $('#capVal').textContent=s.capMin+'%+'; } if(s.unitsMin) $('#fUnits').value=s.unitsMin; if(s.unitsMax) $('#fUnitsMax').value=s.unitsMax; if(s.yearMin) $('#fYear').value=s.yearMin; if(s.yearMax) $('#fYearMax').value=s.yearMax;
+  $('#fAfford').checked=F.afford; if($('#fHasPhone'))$('#fHasPhone').checked=F.hasPhone; if($('#fHasEmail'))$('#fHasEmail').checked=F.hasEmail; if(s.sort){ $('#sort').value=s.sort; }
+}catch(e){} }
+function resetAll(){ F.types.clear(); F.firms.clear(); F.cityQuery=''; F.priceMin=F.priceMax=F.unitsMin=F.unitsMax=F.yearMin=F.yearMax=null; F.capMin=0; F.status='all'; F.afford=false; F.unwarrantable=false; F.nonqm=false; F.hasPhone=false; F.hasEmail=false;
+  ['fCity','fPriceMin','fPriceMax','fUnits','fUnitsMax','fYear','fYearMax'].forEach(id=>$('#'+id).value=''); $('#fCap').value=0; $('#capVal').textContent='any'; $('#fAfford').checked=false; if($('#fHasPhone'))$('#fHasPhone').checked=false; if($('#fHasEmail'))$('#fHasEmail').checked=false;
+  syncChips(); render(); }
+
+// ---- chat ----
+function addMsg(role, text, actions){
+  const d=document.createElement('div'); d.className='msg '+role;
+  d.innerHTML = (text||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/```action[\s\S]*?```/g,'').trim()
+    .replace(/\*\*(.+?)\*\*/g,'<b>$1</b>').replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\n/g,'<br>');
+  if(actions&&actions.length) d.innerHTML += `<div class="actiontag">⚡ view updated: ${actions.join(', ')}</div>`;
+  $('#log').appendChild(d); $('#log').scrollTop=$('#log').scrollHeight; return d;
+}
+function parseAction(reply){ const m=reply.match(/```action\s*([\s\S]*?)```/); if(!m) return null; try{ return JSON.parse(m[1].trim()); }catch{ return null; } }
+async function ask(text){
+  addMsg('u', text); history.push({role:'user',content:text});
+  $('#send').disabled=true; const thinking=addMsg('sys','…thinking');
+  try{
+    const r = await fetch('/api/claude-chat',{ method:'POST', headers:{'Content-Type':'application/json'},
+      body: JSON.stringify({ messages: history, context: getContext() }) });
+    const j = await r.json(); thinking.remove();
+    if(j.error){ addMsg('sys','⚠ '+j.error); }
+    else { const act=parseAction(j.reply); const applied=act?applyAction(act):null; addMsg('a', j.reply, applied); history.push({role:'assistant',content:j.reply}); }
+  }catch(e){ thinking.remove(); addMsg('sys','⚠ '+e.message); }
+  $('#send').disabled=false; $('#q').focus();
+}
+
+// ---- property history + live comps ----
+window.liveComps = {};
+async function fetchHistory(btn){
+  const id=btn.dataset.id, out=$('#hist-'+id); if(!out) return;
+  if(out.dataset.loaded){ out.style.display = out.style.display==='none'?'block':'none'; return; }
+  out.innerHTML='<span class="small">…checking WhoLivedThere archive (free)…</span>';
+  try{
+    const q=new URLSearchParams({address:btn.dataset.address||'',city:btn.dataset.city||'',zip:btn.dataset.zip||''});
+    const j=await (await fetch('/api/history?'+q)).json();
+    out.dataset.loaded='1';
+    if(j.available===false){ out.innerHTML='<span class="b warn">⚠ archive offline</span>'; return; }
+    if(!j.matched){ out.innerHTML='<span class="small">📜 No record in the WhoLivedThere archive for this address yet.</span>'; return; }
+    const P=j.parcel, money=v=>v?'$'+Number(v).toLocaleString():'—';
+    let h='<div class="small" style="margin-top:6px;border-top:1px solid #2a2a2a;padding-top:6px">📜 <b>WhoLivedThere archive</b>';
+    if(P){ h+=`<div class="badges" style="margin:4px 0">${P.year_built?`<span class="b">built ${P.year_built}</span>`:''}${P.use_type?`<span class="b type">${P.use_type}</span>`:''}${P.units?`<span class="b">${P.units} unit${P.units>1?'s':''}</span>`:''}${P.total_value?`<span class="b ok" title="LA County assessor total assessed value">assessed ${money(P.total_value)}</span>`:''}${P.land_value?`<span class="b">land ${money(P.land_value)}</span>`:''}${P.improvement_value?`<span class="b">improv ${money(P.improvement_value)}</span>`:''}</div><div class="small">${P.use_description||''} · APN ${P.apn||'—'} · ${P.situs_address||''}</div>`; }
+    if(j.permits&&j.permits.length){ h+=`<div class="small" style="margin-top:4px">🔨 Permits (${j.permits.length}): `+j.permits.slice(0,5).map(pm=>`${pm.permit_type||'permit'}${pm.valuation?' $'+Number(pm.valuation).toLocaleString():''}${pm.issue_date?' ('+String(pm.issue_date).slice(0,10)+')':''}`).join(' · ')+'</div>'; }
+    if(j.filming&&j.filming.length){ h+=`<div class="small" style="margin-top:4px">🎬 Filmed here: `+j.filming.slice(0,5).map(f=>`${f.title||'production'}${f.year?' ('+f.year+')':''}`).join(' · ')+'</div>'; }
+    if(j.events&&j.events.length){ h+=`<div class="small" style="margin-top:4px">📌 ${j.events.slice(0,4).map(ev=>ev.summary||ev.kind).join(' · ')}</div>`; }
+    out.innerHTML=h+'</div>';
+  }catch(e){ out.innerHTML='<span class="b warn">⚠ '+e.message+'</span>'; }
+}
+async function fetchLiveComps(id){
+  const p = DATA.ranked.find(x=>x.id===id); if(!p) return;
+  const out = $('#lc-'+id); if(!out) return;
+  out.innerHTML = '<span class="small">…pulling fresh comps (~$0.03)…</span>';
+  try{
+    const r = await fetch('/api/comps',{method:'POST',headers:{'Content-Type':'application/json'},
+      body:JSON.stringify({address:p.address,city:p.city,type:p.type})});
+    const j = await r.json();
+    if(j.error && !(j.comps&&j.comps.length)){ out.innerHTML = `<span class="b warn">⚠ ${j.error}</span>`; return; }
+    window.liveComps[id] = j.comps||[];
+    const none = (!j.comps||!j.comps.length)?' — none scraped (source may have blocked)':'';
+    out.innerHTML = `<div class="small">Live comps via ${j.source||'web'} (cost ${j.cost})${none}:</div>` +
+      (j.comps||[]).map(c=>`<div class="lc-item"><span>${esc(c.title||c.url||'comp')}</span>${c.url?` <button class="copyb" data-copyval="${esc(c.url)}" title="Copy comp URL">⧉</button>`:''} — ${esc([c.price,c.cap,c.units,c.sf].filter(Boolean).join(' · '))}</div>`).join('');
+  }catch(e){ out.innerHTML = `<span class="b bad">⚠ ${e.message}</span>`; }
+}
+
+// ---- drawer open/close ----
+function openDrawer(){ $('#drawer').classList.add('open'); $('#scrim').classList.add('on'); }
+function closeDrawer(){ $('#drawer').classList.remove('open'); $('#scrim').classList.remove('on'); }
+
+// ── Inline list-row expansion + Agent CRM actions ──────────────────────────────────
+function postJSON(url,body){ return fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body||{})}).then(r=>r.json()); }
+function toast(msg){ let t=document.getElementById('crmToast'); if(!t){ t=document.createElement('div'); t.id='crmToast'; t.className='crmToast'; document.body.appendChild(t); } t.textContent=msg; t.classList.add('on'); clearTimeout(t._t); t._t=setTimeout(()=>t.classList.remove('on'),1900); }
+// In-place refresh after any CRM save — repaints EVERY visible surface for this listing (list
+// agent cell + open list-row panel + card block) WITHOUT a full render() (which would collapse
+// expanded rows). Works for every asset type in either view.
+function refreshContactUI(id){
+  const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
+  try{ delete p.__idxHay; }catch(_){}   // CRM changed → let the search haystack rebuild
+
+  const row=[...document.querySelectorAll('.listtbl tr.lrow')].find(r=>r.dataset.id===id);
+  if(row){ const cols=listCols(); const ai=cols.findIndex(c=>c.k==='agent');
+    if(ai>=0 && row.children[ai+1]) row.children[ai+1].innerHTML=colCell('agent',p);
+    const exp=row.nextElementSibling;
+    if(exp&&exp.classList.contains('lexp')&&exp.classList.contains('open')){
+      const body=exp.querySelector('.lexpbody'); if(body){ body.innerHTML=expandBody(p); body.dataset.loaded='1'; }
+    }
+  }
+  const cb=document.getElementById('cardagent-'+id); if(cb) cb.outerHTML=cardAgentBlock(p);
+}
+async function markContacted(id){
+  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/contacted',{}); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('✓ marked contacted '+fmtWhen(j.contact.contacted_at)); } else toast('⚠ '+(j.error||'failed')); }
+  catch(e){ toast('⚠ '+e.message); }
+}
+async function saveAgentInfo(id){
+  const scope=document.getElementById('composeM')&&document.getElementById('composeM').classList.contains('on')?document.getElementById('composeM'):document;
+  const inputs=[...scope.querySelectorAll('.crmf')].filter(i=>i.dataset.id===id);
+  const body={}; inputs.forEach(i=>body[i.dataset.info]=i.value.trim());
+  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/info',body); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); toast('💾 contact info saved'); } else toast('⚠ '+(j.error||'failed')); }
+  catch(e){ toast('⚠ '+e.message); }
+}
+async function deleteLetter(id,idx){
+  try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/letter/'+encodeURIComponent(idx)+'/delete',{}); if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('letter deleted'); } }
+  catch(e){ toast('⚠ '+e.message); }
+}
+function agentDraft(p,a){ return `Hi ${a.name||'there'},\n\nI'm interested in ${p.address||'your listing'}${p.city?' in '+p.city:''}${p.price?' (listed '+fmt(p.price)+')':''}. Could you share the setup — current rents / in-place NOI, any financing in place, and whether an offer near list would be entertained?\n\nThanks`; }
+// The full per-listing contact modal body (editable info + mark-contacted + compose/save + letter log).
+// The single per-listing "Agent window" — ONE modal with tabs: Agent · Letters · Find · Comps · History.
+function contactModalBody(p){
+  const a=agentInfo(p), id=p.id, firm=a.firm;
+  const tel=a.phone?`<a class="ain" href="tel:${esc(a.phone).replace(/[^0-9+]/g,'')}">📞 ${esc(a.phone)}</a>`:'';
+  const eml=a.email?`<a class="ain" href="mailto:${esc(a.email)}">✉ ${esc(a.email)}</a>`:'';
+  const letters=a.letters.length?`<div class="crm-letters"><div class="deeplbl">Saved letters (${a.letters.length}) — saved, not sent</div>`
+    +a.letters.map((l,i)=>`<div class="lett"><div class="letth"><b>${esc(l.subject||'(no subject)')}</b>${l.draft_id?`<span class="achip done" title="Gmail draft created${l.to?' to '+esc(l.to):''} — review & send in Gmail">📤 drafted</span>`:''}<span class="letts">${fmtWhen(l.ts)}</span><button class="letdel" data-letdel="${id}" data-idx="${l.id||i}" title="Delete this saved letter">✕</button></div><div class="lettb">${esc(l.body||'')}</div></div>`).join('')+`</div>`:'<div class="tabmut">No saved letters yet.</div>';
+  const find=[];
+  if(firm&&firm!=='Unknown'){
+    find.push(`<a class="findlink" href="${gsearch('"'+firm+'" commercial real estate Los Angeles')}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm)} — Google</a>`);
+    find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/companies/?keywords=${encodeURIComponent(firm)}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm)} — LinkedIn</a>`);
+    find.push(`<button class="findlink" data-firmfilter="${esc(firm).replace(/"/g,'&quot;')}" title="Show only ${esc(firm)} listings in the grid">▦ More from ${esc(firm)} (in-app)</button>`);
+  }
+  if(a.name){
+    find.push(`<a class="findlink" href="${gsearch('"'+a.name+'"'+(firm&&firm!=='Unknown'?' "'+firm+'"':'')+' real estate agent')}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — Google</a>`);
+    find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/people/?keywords=${encodeURIComponent(a.name+(firm&&firm!=='Unknown'?' '+firm:''))}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — LinkedIn</a>`);
+    find.push(`<a class="findlink" href="${gsearch('"'+a.name+'" DRE license California real estate')}" target="_blank" rel="noopener noreferrer">🪪 ${esc(a.name)} — DRE license</a>`);
+  }
+  listingLinkBits(p,firm).forEach(b=>find.push(b));
+  find.push(`<span class="findlink findloop">${loopnetLink(p)}</span>`);
+  const findHtml=find.length?`<div class="findgrid">${find.join('')}</div>`:'<div class="tabmut">Add an agent/firm in the Agent tab to enable lookups.</div>';
+  return `<div class="composeCard tabwin">
+    <div class="composeH">🧑‍💼 <b>${esc(p.address||'listing')}</b>${p.city?', '+esc(p.city):''}<span class="winmeta">${a.name?esc(a.name):'no agent yet'}${a.contacted_at?' · ✓ '+fmtWhen(a.contacted_at):''}</span><button class="composeX" title="Close">✕</button></div>
+    <div class="tabbar">
+      <button class="tabt active" data-tab="agent">Agent</button>
+      <button class="tabt" data-tab="letters">Letters${a.letters.length?' ('+a.letters.length+')':''}</button>
+      <button class="tabt" data-tab="find">Find</button>
+      <button class="tabt" data-tab="comps">Comps</button>
+      <button class="tabt" data-tab="history">History</button>
+    </div>
+    <div class="tabpane active" data-pane="agent">
+      <div class="cm-info">
+        <input class="crmf" data-info="name" data-id="${id}" placeholder="Agent name" value="${esc(a.name||'')}">
+        <input class="crmf" data-info="phone" data-id="${id}" placeholder="Phone" value="${esc(a.phone||'')}">
+        <input class="crmf" data-info="email" data-id="${id}" placeholder="Email" value="${esc(a.email||'')}">
+      </div>
+      ${(tel||eml)?`<div class="ainrow">${tel} ${eml}</div>`:''}
+      <div class="cm-notes"><div class="deeplbl">Notes</div><textarea class="crmf" data-info="notes" data-id="${id}" rows="4" placeholder="Notes on this agent / listing">${esc(a.notes||'')}</textarea></div>
+      <div class="cm-row">
+        <button class="abtn saveinfobtn" data-saveinfo="${id}">💾 Save info + notes</button>
+        <button class="abtn markbtn" data-mark="${id}">${a.contacted_at?'↻ Update contacted date':'✓ Mark contacted'}</button>
+        ${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}
+      </div>
+    </div>
+    <div class="tabpane" data-pane="letters">
+      <div class="composeGate">⚑ <b>Save letter</b> logs it. <b>Send via George</b> creates a Gmail <b>DRAFT</b> — NOT sent; you review &amp; hit send in Gmail.</div>
+      <input id="composeSubj" placeholder="Subject" value="${esc('Interest in '+(p.address||'your listing')+(p.city?', '+p.city:''))}">
+      <textarea id="composeBody" rows="7">${esc(agentDraft(p,a))}</textarea>
+      <div class="composeActs">
+        <button id="composeSave" class="primary" data-id="${id}">💾 Save letter</button>
+        <button id="composeSend" class="gated" data-id="${id}" title="Create a Gmail DRAFT to the agent (fill Email in the Agent tab) — not sent">📤 Send via George</button>
+        <button id="composeCopy">📋 Copy</button>
+      </div>
+      ${letters}
+    </div>
+    <div class="tabpane" data-pane="find">${findHtml}</div>
+    <div class="tabpane" data-pane="comps">
+      <button class="lcbtn" data-id="${id}" title="Pull fresh comps via a cloud browser (~$0.03)">🔄 Pull live comps (~$0.03)</button>
+      <div class="lcout" id="lc-${id}"></div>
+    </div>
+    <div class="tabpane" data-pane="history">
+      <button class="histbtn" data-id="${id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (free)">📜 Load property history</button>
+      <div class="histout" id="hist-${id}"></div>
+    </div>
+  </div>`;
+}
+// One modal, opens for EVERY listing from any surface. Its own click delegation (it lives on
+// document.body, outside #grid) handles save-info / mark / save-letter / delete-letter.
+function openContact(id){
+  const p=DATA.ranked.find(x=>x.id===id); if(!p) return;
+  let m=document.getElementById('composeM');
+  if(!m){ m=document.createElement('div'); m.id='composeM'; m.className='composeM'; document.body.appendChild(m); }
+  m.dataset.id=id; m.innerHTML=contactModalBody(p); m.classList.add('on');
+  m.onclick=async(e)=>{
+    if(e.target===m || e.target.closest('.composeX,.composeX2')){ closeCompose(); return; }
+    const tb=e.target.closest('.tabt'); if(tb){ activateTab(m,tb.dataset.tab); return; }
+    const cv=e.target.closest('[data-copyval]'); if(cv){ navigator.clipboard.writeText(cv.dataset.copyval).then(()=>toast('📋 copied')).catch(()=>{}); return; }
+    const ff=e.target.closest('[data-firmfilter]'); if(ff){ closeCompose(); F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
+    const lc=e.target.closest('.lcbtn'); if(lc){ fetchLiveComps(lc.dataset.id); return; }
+    const hi=e.target.closest('.histbtn'); if(hi){ fetchHistory(hi); return; }
+    if(e.target.closest('#composeCopy')){ navigator.clipboard.writeText(m.querySelector('#composeBody').value); toast('📋 copied'); return; }
+    if(e.target.closest('#composeSave')){
+      const subject=m.querySelector('#composeSubj').value, body=m.querySelector('#composeBody').value;
+      try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/letter',{subject,body,channel:'letter'});
+        if(j.contact){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('💾 letter saved to contact log'); }
+        else toast('⚠ '+(j.error||'save failed')); }catch(err){ toast('⚠ '+err.message); }
+      return;
+    }
+    if(e.target.closest('#composeSend')){
+      const to=((m.querySelector('.crmf[data-info=email]')||{}).value||'').trim();
+      if(!to){ toast('add the agent email first'); return; }
+      const subject=m.querySelector('#composeSubj').value, body=m.querySelector('#composeBody').value;
+      if(!confirm('Create a Gmail DRAFT to '+to+' via George?\n\nIt will NOT be sent — it lands in your Gmail drafts (steve@designerwallcoverings.com) for you to review and send.')) return;
+      try{ const j=await postJSON('/api/contacts/'+encodeURIComponent(id)+'/send-draft',{to,subject,body});
+        if(j.ok){ window.CONTACTS[id]=j.contact; refreshContactUI(id); repaintModal(id); toast('📤 Gmail draft created — review & send in Gmail'); }
+        else if(j.error==='george_not_configured'){ toast('⚠ George drafting not enabled on this box'); alert('George drafting isn’t enabled on this server yet — the letter is not drafted.\n\nTo turn it on, GEORGE_AUTH must be set in the CRCP env (one-time). The letter is unaffected; use 💾 Save letter to keep it.'); }
+        else toast('⚠ '+(j.detail||j.error||'draft failed')); }
+      catch(err){ toast('⚠ '+err.message); }
+      return;
+    }
+    if(e.target.closest('[data-saveinfo]')){ saveAgentInfo(id); return; }
+    if(e.target.closest('[data-mark]')){ markContacted(id); return; }
+    const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(id, ld.dataset.idx); return; }
+  };
+}
+// Switch the active tab within the agent window.
+function activateTab(m,tab){
+  m.querySelectorAll('.tabt').forEach(t=>t.classList.toggle('active',t.dataset.tab===tab));
+  m.querySelectorAll('.tabpane').forEach(pn=>pn.classList.toggle('active',pn.dataset.pane===tab));
+}
+// Repaint the open modal in place (after mark/letter changes) — preserves open state AND active tab.
+function repaintModal(id){ const m=document.getElementById('composeM'); const p=DATA.ranked.find(x=>x.id===id);
+  if(m && m.classList.contains('on') && m.dataset.id===id && p){
+    const cur=(m.querySelector('.tabt.active')||{}).dataset; const tab=cur?cur.tab:'agent';
+    m.innerHTML=contactModalBody(p); activateTab(m,tab||'agent');
+  } }
+function openCompose(id){ openContact(id); }   // back-compat alias
+function closeCompose(){ const m=document.getElementById('composeM'); if(m) m.classList.remove('on'); }
+
+// ---- Broker-history pilot index ----
+// Keyed by both listing id (e.g. crx2477882) and lowercase address.
+// Loaded once at boot; cards read from this map at render time.
+window.BROKER_HIST = {};   // id → enrichment object
+window.BROKER_HIST_ADDR = {};  // lc-address → enrichment object
+fetch('/api/broker-history').then(r=>r.json()).then(j=>{
+  (j.properties||[]).forEach(p=>{
+    if(p.id) window.BROKER_HIST[p.id]=p;
+    if(p.address) window.BROKER_HIST_ADDR[p.address.toLowerCase().trim()]=p;
+  });
+  // Re-render if DATA is already loaded (race: demographics loads fast too)
+  if(window.DATA_READY) render();
+}).catch(()=>{}); // graceful — broker pilot data is additive, not load-blocking
+
+// ---- boot ----
+fetch('data/comps.json').then(r=>r.json()).then(c=>{ if(DATA) DATA.comps=c; else window.__comps=c; }).catch(()=>{});
+fetch('data/demographics.json').then(r=>r.json()).then(d=>{ window.DEMO=d.byCity||{}; if(DATA) render(); }).catch(()=>{});
+// Map a condo (separate /api/condos schema) into the explorer's property shape so it flows through
+// finance, scoring, filters, sort, and the card/list renderers as a first-class 'Condo' asset type.
+function mapCondo(c){
+  return { id:c.id, address:c.address, city:c.city, zip:c.zip, type:'Condo',
+    price:+c.price||0, units:1, cap_rate:null, year_built:c.year_built||null,
+    status:'Active', source:c.source, firm:c.firm_name||null, verified:false,
+    isCondo:true, warrantable_status:c.warrantable_status,
+    beds:c.beds, baths:c.baths, sqft:c.sqft, hoa:c.hoa, broker_name:c.broker_name,
+    project_name:c.project_name, upside_note:null, qwen:null };
+}
+function loadRanked(_retry){ return fetch('data/ranked.json').then(r=>{ if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); }).catch(e=>{ if(_retry>0){ return new Promise(res=>setTimeout(res,1500)).then(()=>loadRanked(_retry-1)); } throw e; }); } loadRanked(1).then(async d=>{
+  DATA=d; if(window.__comps) DATA.comps=window.__comps;
+  // Merge condo inventory in-grid (best-effort; explorer still works if the API is down).
+  let condoN=0; try{ const cd=await (await fetch('/api/condos')).json(); const cs=(cd.condos||[]).map(mapCondo); condoN=cs.length; DATA.ranked=DATA.ranked.concat(cs); }catch(e){}
+  $('#sub').innerHTML = `${d.ranked.length} listings${condoN?` · ${condoN} condos`:''} · <b>${d.meta.market}</b>${d.generated?' · sourced '+d.generated:''}`;
+
+  scenario = localStorage.getItem('cre_scenario')||'25';
+  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===scenario));
+  buildFacets();
+  loadState();
+  urlToState();  // a shared ?query overrides saved localStorage state and restores the exact view
+  $$('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===scenario));
+
+  // drawer wiring
+  $('#burger').onclick=openDrawer; $('#dclose').onclick=closeDrawer; $('#scrim').onclick=closeDrawer;
+  $('#reset').onclick=resetAll;
+  // collapsible filter tabs — click a section header to expand/collapse; all collapsed on load
+  $('#drawer').addEventListener('click',e=>{ const h=e.target.closest('h4'); if(h && h.parentElement.classList.contains('fsec')) h.parentElement.classList.toggle('collapsed'); });
+  // Fields section starts EXPANDED and first — all other fsec sections collapse on load.
+  $$('#drawer .fsec').forEach(s=>{ if(s.id!=='fieldsSec') s.classList.add('collapsed'); });
+  // active-filter summary bar: remove one filter or reset all
+  $('#activebar').addEventListener('click',e=>{ if(e.target.closest('#afreset')){ resetAll(); return; } const c=e.target.closest('.afchip'); if(c) clearFilter(c.dataset.clear); });
+  // saved searches
+  buildSavedList();
+  $('#savedList').addEventListener('click',e=>{ const del=e.target.closest('[data-del]'); if(del){ SAVED.splice(+del.dataset.del,1); saveSaved(); buildSavedList(); return; } const n=e.target.closest('.savedname'); if(n){ location.href=location.pathname+(n.dataset.qs||''); } });
+  $('#savebtn').onclick=()=>{ const name=(prompt('Name this saved search:')||'').trim(); if(!name) return; SAVED.push({name, qs:location.search||''}); saveSaved(); buildSavedList(); };
+  $('#linkbtn').onclick=copyShareLink;
+  $('#csvbtn').onclick=exportCSV;
+  // search-all-fields box (Steve list-build rule 7) — filters across EVERY field, space = AND
+  { const si=$('#idxSearch'); if(si) si.oninput=()=>{ idxQuery=si.value.trim().toLowerCase(); if($('#railSearch'))$('#railSearch').value=si.value; render(); }; }
+  // Reset the PRIMARY List table layout back to the tight default: column order, resize widths,
+  // and visibility. Clears idxColOrder + col-resize's cr:<pathname> widths + resets VIS to defaults.
+  { const rb=$('#idxReset'); if(rb) rb.onclick=()=>{
+      COLORDER=[]; saveColOrder();
+      try{ Object.keys(localStorage).forEach(k=>{ if(k.indexOf('cr:'+location.pathname+':')===0) localStorage.removeItem(k); }); }catch(e){}
+      FIELDS.forEach(f=>VIS[f.k]=!!f.def); saveVis();
+      lastColSig=''; buildFieldToggles(); render();
+    }; }
+  // facet chip handlers (multi-select toggles)
+  $('#fType').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return;
+    if(c.dataset.warr==='nonqm'){ F.nonqm=!F.nonqm; syncChips(); render(); return; }
+    if(c.dataset.warr==='unwarrantable'){ F.unwarrantable=!F.unwarrantable; if(F.unwarrantable) F.types=new Set(['Condo']); syncChips(); render(); return; }
+    F.types.has(c.dataset.t)?F.types.delete(c.dataset.t):F.types.add(c.dataset.t);
+    if(!F.types.has('Condo')) F.unwarrantable=false;
+    syncChips(); render(); };
+  $('#fFirm').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; F.firms.has(c.dataset.fm)?F.firms.delete(c.dataset.fm):F.firms.add(c.dataset.fm); syncChips(); render(); };
+  $('#fCityChips').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; const cy=c.dataset.cy.toLowerCase(); F.cityQuery=(F.cityQuery===cy)?'':cy; $('#fCity').value=(F.cityQuery?c.dataset.cy:''); syncChips(); render(); };
+  $('#fStatus').onclick=e=>{ const c=e.target.closest('.chip'); if(!c) return; F.status=c.dataset.st; syncChips(); render(); };
+  $('#fCity').oninput=()=>{ F.cityQuery=$('#fCity').value.trim().toLowerCase(); syncChips(); render(); };
+  $('#fPriceMin').oninput=()=>{ F.priceMin=$('#fPriceMin').value?+$('#fPriceMin').value:null; render(); };
+  $('#fPriceMax').oninput=()=>{ F.priceMax=$('#fPriceMax').value?+$('#fPriceMax').value:null; render(); };
+  $('#fCap').oninput=()=>{ F.capMin=+$('#fCap').value; $('#capVal').textContent=F.capMin?F.capMin+'%+':'any'; render(); };
+  $('#fUnits').oninput=()=>{ F.unitsMin=$('#fUnits').value?+$('#fUnits').value:null; render(); };
+  $('#fUnitsMax').oninput=()=>{ F.unitsMax=$('#fUnitsMax').value?+$('#fUnitsMax').value:null; render(); };
+  $('#fYear').oninput=()=>{ F.yearMin=$('#fYear').value?+$('#fYear').value:null; render(); };
+  $('#fYearMax').oninput=()=>{ F.yearMax=$('#fYearMax').value?+$('#fYearMax').value:null; render(); };
+  $('#fAfford').onchange=()=>{ F.afford=$('#fAfford').checked; render(); };
+  $('#fHasPhone').onchange=()=>{ F.hasPhone=$('#fHasPhone').checked; render(); };
+  $('#fHasEmail').onchange=()=>{ F.hasEmail=$('#fHasEmail').checked; render(); };
+  $('#scenario').onclick=e=>{ if(!e.target.dataset.s) return; setScenario(e.target.dataset.s); render(); };
+
+  // sort (drawer + top bar mirror each other)
+  $('#sort').onchange=()=>{ colSort=null; $('#sortTop').value=$('#sort').value; render(); };
+  $('#sortTop').onchange=()=>{ colSort=null; $('#sort').value=$('#sortTop').value; render(); };
+  $('#sortTop').value=$('#sort').value;
+  // density
+  const dv=String(Math.min(10,Math.max(1,+(localStorage.getItem('cre_density')||'3')||3))); $('#density').value=dv; $('#densVal').textContent=dv; document.documentElement.style.setProperty('--cols',dv); localStorage.setItem('cre_density',dv);
+  $('#density').oninput=()=>{ document.documentElement.style.setProperty('--cols',$('#density').value); $('#densVal').textContent=$('#density').value; localStorage.setItem('cre_density',$('#density').value); if($('#railDensity')){$('#railDensity').value=$('#density').value; $('#railDensVal').textContent=$('#density').value;} };
+
+  // ===== right-hand control rail (nav-agent skill surface) =====
+  wireCtrlRail(dv);
+
+  // field show/hide toggles + list/grid view
+  buildFieldToggles();
+  $('#fFields').onchange=e=>{ const cb=e.target.closest('input[data-fk]'); if(!cb) return; VIS[cb.dataset.fk]=cb.checked; saveVis(); buildFieldToggles(); render(); };
+  { const rf=$('#railFields'); if(rf) rf.onchange=e=>{ const cb=e.target.closest('input[data-fk]'); if(!cb) return; VIS[cb.dataset.fk]=cb.checked; saveVis(); buildFieldToggles(); render(); }; }
+  $('#fAll').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=true); saveVis(); buildFieldToggles(); render(); };
+  $('#fNone').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=false); saveVis(); buildFieldToggles(); render(); };
+  $('#fDef').onclick=()=>{ FIELDS.forEach(f=>VIS[f.k]=!!f.def); saveVis(); buildFieldToggles(); render(); };
+  // ── panel drag-to-reorder columns (Fix 3) ──
+  // Dragging the ⠿ handle on a .ftog row reorders COLORDER then rebuilds toggles + re-renders list.
+  let pDragKey=null;
+  $('#fFields').addEventListener('dragstart',e=>{ const row=e.target.closest('.ftog'); if(!row) return;
+    if(!e.target.closest('.fdrag')){ e.preventDefault(); return; }  // only ⠿ handle starts a move
+    pDragKey=row.dataset.fk; e.dataTransfer.effectAllowed='move'; try{e.dataTransfer.setData('text/plain',pDragKey);}catch(_){}
+    row.classList.add('fdragging'); });
+  $('#fFields').addEventListener('dragover',e=>{ if(!pDragKey) return; const row=e.target.closest('.ftog'); if(!row||row.dataset.fk===pDragKey) return;
+    e.preventDefault(); e.dataTransfer.dropEffect='move';
+    const r=row.getBoundingClientRect(), after=(e.clientY-r.top)>r.height/2;
+    $('#fFields').querySelectorAll('.ftog').forEach(x=>x.classList.remove('fdropT','fdropB'));
+    row.classList.toggle('fdropB',after); row.classList.toggle('fdropT',!after); });
+  $('#fFields').addEventListener('dragleave',e=>{ const row=e.target.closest('.ftog'); if(row) row.classList.remove('fdropT','fdropB'); });
+  $('#fFields').addEventListener('drop',e=>{ if(!pDragKey) return; const row=e.target.closest('.ftog'); if(!row){ pDragKey=null; return; } e.preventDefault();
+    const tgt=row.dataset.fk;
+    if(tgt!==pDragKey){
+      const r=row.getBoundingClientRect(), after=(e.clientY-r.top)>r.height/2;
+      syncColOrder(); const from=COLORDER.indexOf(pDragKey);
+      if(from>=0){ COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,pDragKey);
+        saveColOrder(); lastColSig=''; buildFieldToggles(); render(); } }
+    pDragKey=null; });
+  $('#fFields').addEventListener('dragend',()=>{ $('#fFields').querySelectorAll('.fdragging,.fdropT,.fdropB').forEach(x=>x.classList.remove('fdragging','fdropT','fdropB')); pDragKey=null; });
+  $$('#viewseg button').forEach(b=>b.classList.toggle('active',b.dataset.view===view));
+  $('#viewseg').onclick=e=>{ const b=e.target.closest('button[data-view]'); if(!b) return; view=b.dataset.view; localStorage.setItem('cre_view',view); $$('#viewseg button').forEach(x=>x.classList.toggle('active',x.dataset.view===view)); render(); };
+
+  // chat
+  const EX=['Show only multifamily under $1M, sorted by cash-on-cash','Which deals actually cash-flow at 30% down?','Office and industrial in Pasadena','Take me to the Reseda mixed-use','Best in-budget deals'];
+  $('#examples').innerHTML = EX.map(x=>`<button class="ex">${x}</button>`).join('');
+  $('#examples').onclick=e=>{ if(e.target.classList.contains('ex')){ $('#q').value=e.target.textContent; $('#q').focus(); } };
+  $('#chatform').onsubmit=e=>{ e.preventDefault(); const t=$('#q').value.trim(); if(!t) return; $('#q').value=''; ask(t); };
+  $('#grid').onclick=e=>{ const cp=e.target.closest('[data-copy]'); if(cp){ copySummary(cp.dataset.copy, cp); return; } const qk=e.target.closest('[data-quick]'); if(qk){ applyQuick(qk.dataset.quick); return; } const more=e.target.closest('#showmore'); if(more){ _shown=Math.min(_shown+PAGE(), _rows.length); paintPage(); return; } const st=e.target.closest('[data-star]'); if(st){ toggleStar(st.dataset.star); st.classList.toggle('on',SHORT.has(st.dataset.star)); const sb=$('#shortbtn'); if(sb) sb.textContent='⭐ Shortlist ('+SHORT.size+')'; if(F.shortlist) render(); return; } const th=e.target.closest('.listtbl th[data-sortk]'); if(th){ const k=th.dataset.sortk; if(colSort&&colSort.key===k) colSort.dir*=-1; else colSort={key:k,dir:['type','firm','warr','broker','status','zip','addr','rec','phone','email','firmweb'].includes(k)?1:-1}; render(); return; } const b=e.target.closest('.lcbtn'); if(b){ fetchLiveComps(b.dataset.id); return; } const h=e.target.closest('.histbtn'); if(h){ fetchHistory(h); return; }
+    const cv=e.target.closest('[data-copyval]'); if(cv){ navigator.clipboard.writeText(cv.dataset.copyval).then(()=>toast('📋 copied')).catch(()=>toast('⚠ copy failed')); return; }
+    const ff=e.target.closest('[data-firmfilter]'); if(ff){ if(e.metaKey||e.ctrlKey||e.shiftKey) F.firms.add(ff.dataset.firmfilter); else F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
+    // drill atoms: card asset-type + city → filter the grid to that value (URL updates via render→stateToURL)
+    const tf=e.target.closest('[data-typefilter]'); if(tf){ if(e.metaKey||e.ctrlKey||e.shiftKey) F.types.add(tf.dataset.typefilter); else F.types=new Set([tf.dataset.typefilter]); syncChips(); render(); return; }
+    const cf=e.target.closest('[data-cityfilter]'); if(cf){ const cy=cf.dataset.cityfilter.toLowerCase(); F.cityQuery=(F.cityQuery===cy)?'':cy; if($('#fCity'))$('#fCity').value=(F.cityQuery?cf.dataset.cityfilter:''); syncChips(); render(); return; }
+    const ct=e.target.closest('[data-contact]'); if(ct){ openCompose(ct.dataset.contact); return; }
+    const mk=e.target.closest('[data-mark]'); if(mk){ markContacted(mk.dataset.mark); return; }
+    const si=e.target.closest('[data-saveinfo]'); if(si){ saveAgentInfo(si.dataset.saveinfo); return; }
+    const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(ld.dataset.letdel, ld.dataset.idx); return; }
+    // Row-click NO LONGER opens the agent pop-out (Steve 2026-08-18: "do not allow this to pop out, or
+    // move round. fix in place"). Contact info renders directly in the row; explicit links/buttons still work.
+  };
+  // ── drag-to-reorder PRIMARY List columns (Steve list-build rule 2) ──
+  // Delegated on #grid since the list table is re-rendered each render(). Property (addr) is a
+  // fixed anchor (no data-k) so it never participates as a drag source or drop target.
+  let idxDragKey=null;
+  $('#grid').addEventListener('dragstart',e=>{ const th=e.target.closest('.listtbl th.dragcol'); if(!th||!th.dataset.k) return;
+    if(e.target.closest && e.target.closest('.cr-handle')){ e.preventDefault(); return; }  // that grab is a resize, not a move
+    idxDragKey=th.dataset.k; e.dataTransfer.effectAllowed='move'; try{ e.dataTransfer.setData('text/plain',idxDragKey); }catch(_){}
+    th.classList.add('dragging'); });
+  $('#grid').addEventListener('dragover',e=>{ if(!idxDragKey) return; const th=e.target.closest('.listtbl th.dragcol'); if(!th||!th.dataset.k||th.dataset.k===idxDragKey) return;
+    e.preventDefault(); e.dataTransfer.dropEffect='move';
+    const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
+    th.classList.toggle('dropR',after); th.classList.toggle('dropL',!after); });
+  $('#grid').addEventListener('dragleave',e=>{ const th=e.target.closest('.listtbl th.dragcol'); if(th) th.classList.remove('dropL','dropR'); });
+  $('#grid').addEventListener('drop',e=>{ if(!idxDragKey) return; const th=e.target.closest('.listtbl th.dragcol'); if(!th||!th.dataset.k){ idxDragKey=null; return; }
+    e.preventDefault(); const tgt=th.dataset.k;
+    if(tgt!==idxDragKey){ const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
+      syncColOrder(); const from=COLORDER.indexOf(idxDragKey);
+      if(from>=0){ COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,idxDragKey);
+        saveColOrder(); lastColSig=''; render(); } }
+    idxDragKey=null; });
+  $('#grid').addEventListener('dragend',()=>{ document.querySelectorAll('.listtbl th.dragging,.listtbl th.dropL,.listtbl th.dropR').forEach(x=>x.classList.remove('dragging','dropL','dropR')); idxDragKey=null; });
+  $('#shortbtn').onclick=()=>{ F.shortlist=!F.shortlist; render(); };
+  $('#cmpbtn').onclick=openCompare;
+  $('#heat').value=heat; $('#heat').onchange=()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat',heat); render(); };
+  $('#cmpov').addEventListener('click',e=>{ if(e.target.id==='cmpov') e.currentTarget.classList.remove('on'); });
+  $('#cmpmodal').addEventListener('click',e=>{ if(e.target.closest('#cmpcsv')) exportCompareCSV(); else if(e.target.closest('#cmpprint')) printCompare(); });
+  window.addEventListener('afterprint',()=>document.body.classList.remove('printing-compare'));
+  // a11y: Escape closes any open overlay (compare / broker / property-history modals)
+  document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ ['cmpov','bkov'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('on'); }); } });
+
+  window.DATA_READY = true;
+  recompute(); syncChips(); render();
+}).catch(e=>{ $('#sub').textContent='Listings are still loading — refresh in a moment if this persists.'; console.warn('ranked.json load failed:',e); });
+</script>
+  <script src="/col-resize.js" defer></script>
+  <script src="/global-search.js" defer></script>
+<script src="/corner-nav.js" defer></script>
+<script>
+// Day/Night toggle — theme already applied before first paint by the head snippet.
+// crcp-theme key; Day (light) is the default. Only an explicit click persists.
+(function(){
+  var root=document.documentElement;
+  function label(t){
+    var b=document.getElementById('crcpThemeToggle'); if(!b) return;
+    b.innerHTML=(t==='light'?'☀️':'🌙')+' <span>'+(t==='light'?'Day':'Night')+'</span>';
+  }
+  function applyTheme(t){ root.setAttribute('data-theme',t); label(t); }
+  function mount(){
+    if(document.getElementById('crcpThemeToggle')){ label(root.getAttribute('data-theme')||'light'); return; }
+    var btn=document.createElement('button');
+    btn.id='crcpThemeToggle'; btn.type='button';
+    btn.title='Toggle day / night'; btn.setAttribute('aria-label','Toggle day / night theme');
+    btn.addEventListener('click',function(){
+      var nt=root.getAttribute('data-theme')==='light'?'dark':'light';
+      applyTheme(nt);
+      try{ localStorage.setItem('crcp-theme',nt); }catch(e){}
+    });
+    var host=document.querySelector('header .nav')||document.querySelector('header');
+    if(host){ host.appendChild(btn); } else { btn.style.cssText+=';position:fixed;top:12px;right:14px;z-index:99999'; document.body.appendChild(btn); }
+    label(root.getAttribute('data-theme')||'light');
+  }
+  if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',mount); else mount();
+})();
+</script>
+  <!-- nav-agent REMOVED (Steve 2026-08-18: "no more pop out … fix into the ui"): its floating
+       movable/removable panel overlay (.na-restorebar, position:fixed) popped out over the grid.
+       The native Fields show/hide/reorder panel (#fFields) is independent and stays. -->
+  <!-- <script src="/nav-agent/nav-agent.js" defer></script> -->
+
+  <!-- ── Controls-rail resize bar (Steve 2026-08-20): drag the #ctrlrail (Controls) panel wider ──
+       A fixed vertical grab-bar pinned to the rail's left edge via right:var(--railW), so it auto-
+       tracks the width and never scrolls with the rail content. Drag = resize, dbl-click = reset,
+       width persisted to localStorage. Hidden with the rail below 1100px. -->
+  <style>
+    .railrsz{ position:fixed; right:var(--railW,272px); top:57px; bottom:0; width:10px; margin-right:-5px; cursor:ew-resize; z-index:25; }
+    .railrsz::before{ content:''; position:absolute; left:4px; top:0; bottom:0; width:2px; background:transparent; transition:background .15s; }
+    .railrsz:hover::before, .railrsz.rsz-active::before{ background:var(--blue,#3b7dff); }
+    body.rsz-dragging{ cursor:ew-resize; user-select:none; }
+    @media (max-width:1100px){ .railrsz{ display:none; } }
+  </style>
+  <script>
+  (function(){
+    var rail=document.getElementById('ctrlrail'); if(!rail) return;
+    var KEY='crcp_railW';
+    try{ var s=parseInt(localStorage.getItem(KEY),10); if(s>=200&&s<=760) document.documentElement.style.setProperty('--railW', s+'px'); }catch(_){}
+    var h=document.createElement('div'); h.className='railrsz'; h.title='Drag to resize the Controls panel · double-click to reset'; document.body.appendChild(h);
+    var dragging=false;
+    function onMove(e){ if(!dragging) return; var w=Math.max(200, Math.min(760, window.innerWidth - e.clientX)); document.documentElement.style.setProperty('--railW', w+'px'); }
+    function onUp(){ if(!dragging) return; dragging=false; document.body.classList.remove('rsz-dragging'); h.classList.remove('rsz-active');
+      document.removeEventListener('mousemove',onMove); document.removeEventListener('mouseup',onUp);
+      try{ localStorage.setItem(KEY, Math.round(rail.getBoundingClientRect().width)); }catch(_){} }
+    h.addEventListener('mousedown',function(e){ e.preventDefault(); dragging=true; document.body.classList.add('rsz-dragging'); h.classList.add('rsz-active');
+      document.addEventListener('mousemove',onMove); document.addEventListener('mouseup',onUp); });
+    h.addEventListener('dblclick',function(){ document.documentElement.style.removeProperty('--railW'); try{ localStorage.removeItem(KEY); }catch(_){} });
+  })();
+  </script>
+  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
+<script src="/user-badge.js" defer></script></body>
+</html>
diff --git a/front-page-audit/screens/crcp/repro/repro.html b/front-page-audit/screens/crcp/repro/repro.html
new file mode 100644
index 0000000..83e4ed3
--- /dev/null
+++ b/front-page-audit/screens/crcp/repro/repro.html
@@ -0,0 +1,464 @@
+<!doctype html><html data-theme="dark"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>CRCP repro</title>
+<link rel="stylesheet" href="/crcp-theme.css">
+<style>
+:root { --cols:3; --drawerW:316px; }
+
+  :root { --cols:3; --drawerW:316px; }
+  * { box-sizing:border-box; }
+  :focus-visible { outline:2px solid var(--blue); outline-offset:2px; border-radius:4px; }
+  @media (prefers-reduced-motion: reduce){ *,*::before,*::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; scroll-behavior:auto !important; } }
+  body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }
+
+  /* ---- top bar ---- */
+  header { display:flex; align-items:center; gap:14px; padding:12px 20px; border-bottom:1px solid var(--line); position:sticky; top:0; z-index:30; background:var(--headbg); backdrop-filter:blur(8px); }
+  .burger { display:inline-flex; align-items:center; gap:8px; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:9px 13px; font-size:15px; cursor:pointer; font-weight:600; }
+  .burger:hover { border-color:var(--blue); }
+  .fcount { display:none; background:var(--blue); color:#04122b; font-size:11px; font-weight:700; border-radius:20px; padding:1px 7px; }
+  .fcount.on { display:inline-block; }
+  .htitle h1 { margin:0; font-size:18px; letter-spacing:.2px; }
+  .htitle .sub { color:var(--mut); font-size:12px; }
+  header .spacer { margin-left:auto; }
+  /* nav-link cluster; on wide screens it sits inline right-aligned, on tablets/phones it becomes a swipeable strip (see @media 1024) */
+  .topnav { display:flex; align-items:center; flex-wrap:nowrap; }
+  .seg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .seg button { background:var(--card); color:var(--ink); border:0; border-right:1px solid var(--line); padding:8px 11px; font-size:12px; cursor:pointer; }
+  .seg button:last-child { border-right:0; }
+  .seg button.active { background:#1f6feb; color:#fff; }
+  #drawer .seg { display:flex; width:100%; }
+  #drawer .seg button { flex:1; text-align:center; padding:9px 6px; }
+
+  /* ---- left drawer ---- */
+  #scrim { position:fixed; inset:0; background:rgba(0,0,0,.5); opacity:0; pointer-events:none; transition:opacity .2s; z-index:38; }
+  #scrim.on { opacity:1; pointer-events:auto; }
+  #drawer { position:fixed; top:0; left:0; height:100vh; width:var(--drawerW); background:var(--bg2); border-right:1px solid var(--line); transform:translateX(-100%); transition:transform .22s ease; z-index:40; overflow-y:auto; padding:0 0 40px; }
+  #drawer.open { transform:translateX(0); box-shadow:6px 0 30px rgba(0,0,0,.4); }
+  .dhead { display:flex; align-items:center; justify-content:space-between; padding:16px 18px 10px; position:sticky; top:0; background:var(--bg2); border-bottom:1px solid var(--line); z-index:2; }
+  .dhead b { font-size:15px; } .dhead .x { background:none; border:0; color:var(--mut); font-size:18px; cursor:pointer; }
+  .fsec { padding:14px 18px; border-bottom:1px solid var(--line); }
+  .fsec h4 { margin:0 0 9px; font-size:11px; text-transform:uppercase; letter-spacing:.6px; color:var(--mut); }
+  .chips { display:flex; flex-wrap:wrap; gap:6px; }
+  .chip { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:18px; padding:5px 11px; font-size:12px; cursor:pointer; user-select:none; }
+  .chip:hover { color:var(--ink); }
+  .chip.active { border-color:var(--blue); color:var(--blue); background:rgba(88,166,255,.08); }
+  .chip .ct { color:var(--mut); font-size:10px; margin-left:4px; }
+  .chip.zero { opacity:.4; }
+  .row2 { display:flex; gap:8px; }
+  .fld { flex:1; }
+  .fld label { display:block; font-size:10px; color:var(--mut); margin-bottom:3px; }
+  input[type=text], input[type=number], select { width:100%; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:8px 10px; font-size:13px; }
+  input[type=range] { width:100%; accent-color:var(--blue); }
+  .rangeval { font-size:11px; color:var(--blue); float:right; }
+  .toggle { display:flex; align-items:center; gap:8px; font-size:13px; cursor:pointer; }
+  .resetbtn { width:calc(100% - 36px); margin:14px 18px 0; background:transparent; color:var(--bad); border:1px solid #7d2b28; border-radius:9px; padding:9px; font-size:13px; cursor:pointer; }
+
+  /* ---- results bar + grid ---- */
+  .bar { display:flex; flex-wrap:wrap; align-items:center; gap:14px; padding:11px 20px; border-bottom:1px solid var(--line); position:sticky; top:57px; background:var(--headbg); backdrop-filter:blur(6px); z-index:20; }
+  .bar .count { font-size:13px; color:var(--mut); } .bar .count b { color:var(--ink); }
+  .bar label { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.5px; }
+  .bar .right { margin-left:auto; display:flex; align-items:center; gap:12px; flex-wrap:wrap; justify-content:flex-end; min-width:0; }  /* wrap so the search/heat/sort controls can't overflow under the right-hand #ctrlrail when main is narrow */
+  .grid { display:grid; grid-template-columns:repeat(var(--cols),minmax(0,1fr)); gap:22px; padding:30px 32px 44px; }
+  @media (max-width:900px){ .grid{ grid-template-columns:repeat(2,minmax(0,1fr));} }
+  /* ---- iPad / tablet / narrow-desktop band (≤1024): keep header ONE fixed row so sticky offsets hold,
+         turn the nav-link cluster into a swipeable horizontal strip instead of clipping off-screen. ---- */
+  @media (max-width:1024px){
+    .grid{ grid-template-columns:repeat(2,minmax(0,1fr)); }  /* docked-drawer band: 2 cols so cards aren't crushed (iPad landscape) */
+    header{ flex-wrap:nowrap; gap:10px; }
+    header .spacer{ display:none; }                    /* nav no longer right-pushed; topnav flexes to fill */
+    .htitle{ min-width:0; flex:0 1 auto; }             /* let the title shrink instead of shoving nav off-screen */
+    .htitle h1{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+    .htitle .sub{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }  /* keep title block 2 lines → header stays ~57px so sticky bar/drawer offsets hold */
+    .topnav{ flex:1 1 auto; min-width:0; overflow-x:auto; overflow-y:hidden;
+             -webkit-overflow-scrolling:touch; scrollbar-width:none; }  /* iOS momentum-scroll, hidden scrollbar */
+    .topnav::-webkit-scrollbar{ display:none; }
+    .topnav > a, .topnav > .seg{ flex:0 0 auto; }      /* items keep intrinsic width; strip scrolls, nothing clips */
+    .topnav > a{ padding:8px 0; }                      /* taller touch target for finger taps */
+    html, body{ overflow-x:hidden; max-width:100%; }   /* no horizontal page scroll on iPad (html is the scroller) */
+    .bar{ max-width:100%; top:auto; }                   /* sort/density strip: no hairline overflow; un-stick so it can't tuck under the taller (burger+title) iPad header */
+  }
+  @media (max-width:620px){ .grid{ grid-template-columns:1fr;} }
+  .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:24px; display:flex; flex-direction:column; gap:10px; position:relative; transition:box-shadow .2s,border-color .2s; }
+  .card.uc { opacity:.62; }
+  .card.flash { border-color:var(--blue); box-shadow:0 0 0 3px rgba(88,166,255,.35); }
+  .top { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; }
+  .rank { font-size:12px; color:var(--mut); }
+  .score { font-size:30px; font-weight:700; line-height:1; }
+  .score small { font-size:11px; color:var(--mut); font-weight:400; display:block; text-align:right; }
+  .addr { font-weight:600; font-size:15px; }
+  .city { color:var(--mut); font-size:13px; }
+  .badges { display:flex; flex-wrap:wrap; gap:6px; }
+  .b { font-size:11px; padding:3px 8px; border-radius:20px; border:1px solid var(--line); color:var(--mut); }
+  .b.type { color:var(--blue); border-color:#284b7a; }
+  .b.deal-badge { font-weight:700; cursor:help; }
+  .b.ok { color:var(--acc); border-color:#1f6f37; }
+  .b.warn { color:var(--warn); border-color:#6b5320; }
+  .b.bad { color:var(--bad); border-color:#7d2b28; }
+  .b.rec-Buy { color:var(--acc); border-color:#1f6f37; font-weight:600; }
+  .b.rec-Watch { color:var(--warn); border-color:#6b5320; font-weight:600; }
+  .b.rec-Pass { color:var(--bad); border-color:#7d2b28; font-weight:600; }
+  .metrics { display:grid; grid-template-columns:1fr 1fr; gap:6px 14px; font-size:13px; border-top:1px solid var(--line); border-bottom:1px solid var(--line); padding:10px 0; }
+  .metrics div { display:flex; justify-content:space-between; }
+  .metrics .k { color:var(--mut); }
+  .metrics .v { font-variant-numeric:tabular-nums; font-weight:600; }
+  .pos { color:var(--acc); } .neg { color:var(--bad); } .na { color:var(--mut); }
+  .thesis { font-size:13px; color:var(--ink); }
+  .lst { font-size:12px; color:var(--mut); margin:2px 0 0; padding-left:16px; }
+  .src { font-size:12px; } .src a { color:var(--blue); text-decoration:none; }
+  .lcbtn,.histbtn { background:transparent; color:var(--warn); border:1px dashed #6b5320; border-radius:14px; padding:2px 8px; font-size:11px; cursor:pointer; }
+  .histbtn { color:var(--blue); border-color:#284b7a; }
+  .lcout,.histout { font-size:11px; color:var(--mut); margin-top:6px; }
+  .lcout a { color:var(--blue); text-decoration:none; }
+  .lcout .lc-item { padding:4px 0; border-top:1px solid var(--line); }
+  .small { font-size:11px; color:var(--mut); }
+  .statusline { font-size:12px; font-weight:600; font-variant-numeric:tabular-nums; padding:5px 9px; border-radius:8px; border:1px solid var(--line); background:rgba(255,255,255,.02); }
+  .statusline.ok { color:var(--acc); border-color:#1f6f37; }
+  .statusline.warn { color:var(--warn); border-color:#6b5320; }
+  .statusline.bad { color:var(--bad); border-color:#7d2b28; }
+  .statusline.mut { color:var(--mut); }
+  .statusline.none { color:var(--warn); border-color:#6b5320; border-style:dashed; }
+  .foot { color:var(--mut); font-size:12px; padding:0 20px 8px; }
+  .empty { padding:60px 20px; text-align:center; color:var(--mut); }
+
+  /* ---- Broker-history + prior-broker flag ---- */
+  .brokerflag { display:inline-flex; align-items:center; gap:5px; font-size:11px; padding:3px 9px; border-radius:20px; background:rgba(255,159,9,.10); color:#ff9f09; border:1px solid rgba(255,159,9,.35); font-weight:700; cursor:help; margin-top:2px; }
+  .txhist { font-size:11px; color:var(--mut); margin-top:4px; line-height:1.5; }
+  .txhist .tx-row { display:flex; gap:8px; align-items:baseline; padding:2px 0; border-bottom:1px dashed rgba(255,255,255,.05); }
+  .txhist .tx-row:last-child { border-bottom:0; }
+  .txhist .tx-date { color:var(--blue); font-variant-numeric:tabular-nums; min-width:72px; }
+  .txhist .tx-ev { flex:1; }
+  .txhist .tx-price { color:var(--acc); font-weight:600; font-variant-numeric:tabular-nums; }
+  .brokerline { font-size:11px; padding:4px 9px; background:rgba(88,166,255,.06); border-radius:7px; border:1px solid rgba(88,166,255,.15); color:#a5c8ff; margin-top:3px; }
+  .brokerline strong { color:var(--blue); }
+
+  /* ---- docked chat ---- */
+  #chatwrap { border-top:1px solid var(--line); background:var(--bg2); padding:16px 20px 40px; }
+  #chatwrap h2 { font-size:15px; margin:0 0 2px; }
+  #chatwrap .hint { color:var(--mut); font-size:12px; margin-bottom:10px; }
+  #log { display:flex; flex-direction:column; gap:10px; max-height:42vh; overflow-y:auto; padding:4px 2px; }
+  .msg { max-width:80%; padding:9px 13px; border-radius:12px; font-size:14px; line-height:1.45; white-space:pre-wrap; word-wrap:break-word; }
+  .msg.u { align-self:flex-end; background:#1f6feb; color:#fff; border-bottom-right-radius:3px; }
+  .msg.a { align-self:flex-start; background:var(--card); border:1px solid var(--line); border-bottom-left-radius:3px; }
+  .msg.a code { background:var(--bg); padding:1px 5px; border-radius:4px; }
+  .msg.sys { align-self:center; color:var(--mut); font-size:12px; background:transparent; }
+  .actiontag { display:inline-block; margin-top:6px; font-size:11px; color:var(--acc); border:1px solid #1f6f37; border-radius:6px; padding:2px 7px; }
+  #chatform { display:flex; gap:10px; margin-top:12px; }
+  #q { flex:1; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:11px 13px; font-size:14px; }
+  #send { background:#1f6feb; color:#fff; border:0; border-radius:10px; padding:0 20px; font-size:14px; font-weight:600; cursor:pointer; }
+  #send:disabled { opacity:.5; cursor:default; }
+  .examples { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
+  .ex { font-size:12px; color:var(--blue); border:1px dashed #284b7a; border-radius:16px; padding:4px 10px; cursor:pointer; background:transparent; }
+
+  /* ---- view toggle (grid/list) ---- */
+  .viewseg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .viewseg button { background:var(--card); color:var(--mut); border:0; border-right:1px solid var(--line); padding:7px 12px; font-size:12px; cursor:pointer; }
+  .viewseg button:last-child { border-right:0; }
+  .viewseg button.active { background:#1f6feb; color:#fff; }
+
+  /* ===== nav-agent inline views: compact List / KV / Table group headers ===== */
+  .compacthost, .kvhost { padding:20px 32px 44px; }
+  .compactlist { display:flex; flex-direction:column; gap:6px; }
+  .crow { display:flex; align-items:center; gap:12px; background:var(--card); border:1px solid var(--line); border-radius:9px; padding:8px 12px; }
+  .crow .starbtn { flex:0 0 auto; }
+  .crow .cid { flex:0 0 220px; min-width:180px; }
+  .crow .cla { display:block; font-weight:600; color:var(--ink); font-size:13.5px; }
+  .crow .clc { display:block; color:var(--mut); font-size:11px; }
+  .crow .cfields { flex:1; display:flex; flex-wrap:wrap; gap:6px 14px; align-items:center; min-width:0; }
+  .cchip { font-size:12px; color:var(--ink); white-space:nowrap; }
+  .cchip .cck { color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.3px; margin-right:3px; }
+  .kvgrid { display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:16px; }
+  .kvcard { background:var(--card); border:1px solid var(--line); border-radius:11px; padding:12px 14px; }
+  .kvhead { display:flex; align-items:center; gap:8px; margin-bottom:8px; padding-bottom:8px; border-bottom:1px solid var(--line); }
+  .kvaddr { font-weight:600; font-size:14px; color:var(--ink); }
+  .kvrow { display:flex; gap:10px; padding:3px 0; font-size:12.5px; border-bottom:1px solid rgba(127,127,127,.08); }
+  .kvrow:last-child { border-bottom:0; }
+  .kvk { flex:0 0 42%; color:var(--mut); text-transform:uppercase; font-size:10px; letter-spacing:.3px; padding-top:2px; }
+  .kvv { flex:1; color:var(--ink); min-width:0; word-break:break-word; }
+  .grouphead { padding:8px 12px; margin:10px 0 4px; background:rgba(88,166,255,.08); border-left:3px solid var(--blue); border-radius:4px; font-size:12px; color:var(--mut); text-transform:uppercase; letter-spacing:.4px; }
+  .grouphead b { color:var(--ink); }
+  .kvgrid .grouphead { grid-column:1/-1; }
+  tr.grouprow td.groupcell { background:rgba(88,166,255,.10); font-size:11px; text-transform:uppercase; letter-spacing:.4px; color:var(--mut); padding:7px 10px; }
+  tr.grouprow td.groupcell b { color:var(--ink); }
+
+  /* ===== right-hand control rail (in-flow, NOT floating) — nav-agent skill surface ===== */
+  #ctrlrail { flex:0 0 var(--railW,272px); width:var(--railW,272px); border-left:1px solid var(--line); background:var(--headbg); align-self:stretch; padding:12px 12px 40px; position:sticky; top:57px; max-height:calc(100vh - 57px); overflow-y:auto; }
+  #ctrlrail .railtitle { font-size:11px; text-transform:uppercase; letter-spacing:.6px; color:var(--mut); margin:2px 4px 12px; display:flex; align-items:center; gap:6px; }
+  .rpanel { border:1px solid var(--line); border-radius:9px; margin-bottom:8px; overflow:hidden; background:var(--card); }
+  .rpanel > .rphead { width:100%; display:flex; align-items:center; justify-content:space-between; gap:8px; background:transparent; color:var(--ink); border:0; padding:10px 12px; font-size:12.5px; font-weight:600; cursor:pointer; text-align:left; }
+  .rpanel > .rphead:hover { color:var(--blue); }
+  .rpanel > .rphead .caret { color:var(--mut); font-size:11px; transition:transform .15s; }
+  .rpanel.open > .rphead .caret { transform:rotate(90deg); color:var(--blue); }
+  .rpanel > .rpbody { display:none; padding:4px 12px 12px; }
+  .rpanel.open > .rpbody { display:block; }
+  .rpbody select, .rpbody input[type=text] { width:100%; background:var(--bg); color:var(--ink); border:1px solid var(--line); border-radius:7px; padding:7px 9px; font-size:12px; margin-top:6px; }
+  .rpbody .rlbl { font-size:11px; color:var(--mut); margin-top:8px; display:block; }
+  .rpbody .ftog { display:flex; width:100%; margin:0 0 4px; }
+  @media (max-width:1100px){ #ctrlrail { display:none; } }   /* on narrow screens the left drawer already carries these controls */
+  /* ---- field show/hide toggles: one CARD per field, click to toggle on/off ---- */
+  .fieldtoggles { display:flex; flex-wrap:wrap; gap:6px; max-height:360px; overflow-y:auto; padding:2px 4px 2px 0; }
+  .ftog { display:inline-flex; align-items:center; gap:5px; font-size:12px; color:var(--mut); cursor:pointer; padding:5px 9px;
+          border:1px solid var(--line); border-radius:9px; background:var(--card); user-select:none; transition:border-color .12s,color .12s,background .12s; }
+  .ftog input { display:none; }                                   /* card IS the toggle; checkbox drives state via :has() */
+  .ftog::before { content:'○'; font-size:11px; color:var(--mut); }
+  .ftog:hover { border-color:var(--blue); color:var(--ink); }
+  .ftog:has(input:checked) { border-color:var(--blue); color:var(--blue); background:rgba(88,166,255,.10); }
+  .ftog:has(input:checked)::before { content:'●'; color:var(--blue); }
+  .ftog .colbadge { color:var(--mut); font-size:10px; }
+  /* panel drag-to-reorder (Fix 3) */
+  .ftog .flbl { flex:1; }
+  .ftog .fdrag { cursor:grab; color:var(--mut); font-size:12px; opacity:.5; user-select:none; line-height:1; margin-right:2px; }
+  .ftog:hover .fdrag { opacity:1; }
+  .ftog.fdragging { opacity:.45; }
+  .ftog.fdropT { box-shadow:inset 0 3px 0 var(--gold); }
+  .ftog.fdropB { box-shadow:inset 0 -3px 0 var(--gold); }
+  .minibtn { background:var(--card); color:var(--mut); border:1px solid var(--line); border-radius:7px; padding:4px 9px; font-size:11px; cursor:pointer; margin-right:5px; }
+  .minibtn:hover { border-color:var(--blue); color:var(--ink); }
+  /* ---- list view ---- */
+  .listhost { display:block; padding:20px 20px 30px; }
+  .listwrap { overflow:auto; border:1px solid var(--line); border-radius:12px; }
+  /* col-resize.js wraps the table in .cr-scroll (overflow:auto) — THAT is the th's nearest scroll
+     container, so bounding ITS height (not .listwrap's) is what makes the sticky header (thead
+     top:0) AND sticky first column (td.stick left:0) actually freeze while scrolling both axes.
+     --tbl-top = the sticky app-chrome height above the table (tune per layout). */
+  .cr-scroll { max-height:calc(100vh - var(--tbl-top, 178px)); }
+  /* max-content so columns size to their content and expand left/right (no artificial squish);
+     min-width:100% keeps it filling the pane when there are few columns; .listwrap scrolls-x. */
+  .listtbl { width:max-content; min-width:100%; border-collapse:collapse; font-size:12.5px; white-space:nowrap; }
+  .listtbl th, .listtbl td { text-align:right; padding:9px 13px; border-top:1px solid var(--line); }
+  .listtbl th { position:sticky; top:0; background:var(--bg2); color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; border-top:0; z-index:2; }
+  .listtbl td.stick, .listtbl th.stick { text-align:left; position:sticky; left:0; background:var(--card); z-index:1; }
+  .listtbl th.stick { background:var(--bg2); z-index:3; }  /* corner = header row + first col: highest */
+  .listtbl th.sortable { cursor:pointer; } .listtbl th.sortable:hover { color:var(--ink); }
+  /* sort arrow via CSS ::after (not literal text) so the header TEXT stays stable and col-resize's tableSig-keyed saved widths survive re-render (TK-10089) */
+  .listtbl th.asc::after { content:' ▲'; color:var(--gold); } .listtbl th.desc::after { content:' ▼'; color:var(--gold); }
+  /* draggable column reorder on the PRIMARY list table (Steve list-build rule) */
+  .listtbl th.dragcol { cursor:grab; }
+  .listtbl th.dragging { opacity:.45; }
+  .listtbl th.dropL { box-shadow:inset 3px 0 0 var(--blue); }
+  .listtbl th.dropR { box-shadow:inset -3px 0 0 var(--blue); }
+  .listtbl tr.lrow:hover td { background:rgba(88,166,255,.06); }
+  .listtbl tr.lrow:hover td.stick { background:var(--card); }
+  .listtbl .la { font-weight:600; color:var(--ink); text-decoration:none; }
+  .listtbl .la:hover { color:var(--blue); }
+  .listtbl .lc { color:var(--mut); font-size:11px; }
+  .listtbl .lsrc { margin-top:2px; }
+  .srclink { color:var(--blue); text-decoration:none; font-size:11px; font-weight:600; } .srclink:hover { text-decoration:underline; }
+  .listtbl .flink { color:var(--ink); text-decoration:none; border-bottom:1px dotted #3a4453; }
+  .listtbl .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  .listtbl tr.lrow { cursor:pointer; }
+  .listtbl .exptog { display:inline-block; width:11px; color:var(--mut); font-size:10px; transition:transform .15s; }
+  .listtbl tr.lrow.open td.stick { background:var(--card); }
+  .listtbl tr.lexp > td { padding:0; border-top:0; }
+  .listtbl tr.lexp .lexpbody { max-height:0; overflow:hidden; transition:max-height .25s ease; background:var(--bg); }
+  .listtbl tr.lexp.open .lexpbody { max-height:1200px; padding:12px 16px; border-top:1px solid var(--line); }
+  .lexpbody .src { text-align:left; white-space:normal; font-size:12px; }
+  .lexpbody .deep { text-align:left; white-space:normal; margin-top:8px; display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
+  .lexpbody .deeplbl { color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; margin-right:2px; }
+  .lexpbody .deep a, .lexpbody .deep button { font-size:11px; padding:3px 9px; border-radius:14px; border:1px solid var(--line); background:var(--card); color:var(--ink); text-decoration:none; cursor:pointer; }
+  .lexpbody .deep a:hover, .lexpbody .deep button:hover { border-color:var(--blue); color:var(--blue); }
+  .lexpbody .lcout, .lexpbody .histout { text-align:left; white-space:normal; }
+  /* Agent · contact column cell */
+  .listtbl td .agentcell { text-align:left; white-space:normal; min-width:170px; display:flex; flex-direction:column; gap:3px; }
+  .agentcell .amut { color:var(--mut); font-style:italic; font-size:11px; }
+  .agentcell .ainrow { display:flex; flex-wrap:wrap; gap:8px; }
+  .agentcell .ain { color:var(--mut); text-decoration:none; font-size:11px; }
+  .agentcell .ain:hover { color:var(--blue); }
+  .ain a { color:var(--blue); text-decoration:none; } .ain a:hover { text-decoration:underline; }
+  .tierb { font-size:9px; text-transform:uppercase; letter-spacing:.4px; color:var(--mut); border:1px solid var(--line); border-radius:4px; padding:0 4px; vertical-align:middle; }
+  .pocketbadge { font-size:11px; font-weight:600; color:var(--gold,#d29922); background:rgba(210,153,34,.12); border:1px solid rgba(210,153,34,.4); border-radius:5px; padding:1px 7px; white-space:nowrap; }
+  .agentcell .chiprow:empty { display:none; }
+  .agentcell .achip { display:inline-block; font-size:10px; color:var(--mut); background:var(--card); border:1px solid var(--line); border-radius:10px; padding:1px 6px; margin-right:4px; }
+  .agentcell .achip.done { color:#3fb950; border-color:#264f2e; }
+  .agentcell .acts { display:flex; flex-wrap:wrap; gap:5px; margin-top:2px; }
+  .abtn { font-size:10.5px; padding:2px 8px; border-radius:12px; border:1px solid var(--line); background:var(--card); color:var(--ink); cursor:pointer; }
+  .abtn:hover { border-color:var(--blue); color:var(--blue); }
+  .abtn.contactbtn { border-color:#2f5233; color:#8fdfa0; }
+  .abtn.markbtn { border-color:#284b7a; color:#8ec0ff; }
+  /* CRM block inside the inline panel */
+  .lexpbody .crm { margin-top:10px; border-top:1px solid var(--line); padding-top:10px; text-align:left; }
+  .crm-info { display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
+  .crm-info .crmf { background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12px; padding:5px 8px; min-width:150px; }
+  .crm-info .crmf:focus { outline:none; border-color:var(--blue); }
+  .crm-acts { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-top:8px; }
+  .crm-letters { margin-top:10px; }
+  .crm-letters .lett { border:1px solid var(--line); border-radius:8px; padding:7px 9px; margin-top:6px; background:var(--bg); }
+  .crm-letters .letth { display:flex; align-items:center; gap:8px; font-size:12px; }
+  .crm-letters .letts { color:var(--mut); font-size:10px; }
+  .crm-letters .letdel { margin-left:auto; background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:12px; }
+  .crm-letters .letdel:hover { color:var(--bad,#f85149); }
+  .crm-letters .lettb { white-space:pre-wrap; font-size:11.5px; color:var(--mut); margin-top:4px; }
+  /* Compose-letter modal */
+  .composeM { position:fixed; inset:0; background:rgba(0,0,0,.55); display:none; align-items:center; justify-content:center; z-index:9000; }
+  .composeM.on { display:flex; }
+  .composeCard { background:var(--card); border:1px solid var(--line); border-radius:14px; width:min(620px,92vw); max-height:88vh; overflow:auto; padding:16px; box-shadow:0 12px 40px rgba(0,0,0,.5); }
+  .composeH { display:flex; align-items:center; font-size:14px; margin-bottom:8px; }
+  .composeH .composeX { margin-left:auto; background:transparent; border:0; color:var(--mut); font-size:16px; cursor:pointer; }
+  .composeGate { font-size:11px; color:#e3b341; background:#241d0e; border:1px solid #5a4a1e; border-radius:8px; padding:6px 9px; margin-bottom:10px; }
+  .composeCard input, .composeCard textarea { width:100%; box-sizing:border-box; background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:13px; padding:8px 10px; margin-bottom:8px; font-family:inherit; }
+  .composeCard textarea { resize:vertical; line-height:1.5; }
+  .composeActs { display:flex; gap:8px; justify-content:flex-end; }
+  .composeActs button { font-size:12px; padding:6px 14px; border-radius:10px; border:1px solid var(--line); background:var(--card); color:var(--ink); cursor:pointer; }
+  .composeActs button.primary { border-color:#2f5233; color:#8fdfa0; }
+  .composeActs button.gated { border-color:#5a4a1e; color:#e3b341; }
+  .composeActs button.gated:hover:not([disabled]) { border-color:#e3b341; }
+  .composeActs button[disabled] { opacity:.4; cursor:not-allowed; }
+  /* Tabbed agent window */
+  .composeCard.tabwin { width:min(680px,94vw); }
+  .composeH .winmeta { color:var(--mut); font-size:11px; font-weight:400; margin-left:10px; }
+  .tabbar { display:flex; gap:2px; border-bottom:1px solid var(--line); margin:4px 0 12px; flex-wrap:wrap; }
+  .tabt { background:transparent; border:0; border-bottom:2px solid transparent; color:var(--mut); font-size:12.5px; padding:7px 12px; cursor:pointer; }
+  .tabt:hover { color:var(--ink); }
+  .tabt.active { color:var(--ink); border-bottom-color:var(--blue); font-weight:600; }
+  .tabpane { display:none; }
+  .tabpane.active { display:block; }
+  .tabmut { color:var(--mut); font-size:12px; font-style:italic; padding:8px 2px; }
+  .cm-row { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:10px; }
+  .tabpane .ainrow { display:flex; flex-wrap:wrap; gap:14px; margin:8px 0; }
+  .tabpane .ain { color:var(--mut); text-decoration:none; font-size:12px; }
+  .tabpane .ain:hover { color:var(--blue); }
+  .findgrid { display:flex; flex-direction:column; gap:7px; }
+  .findlink { display:inline-block; background:var(--card); border:1px solid var(--line); border-radius:8px; color:var(--ink); text-decoration:none; font-size:12.5px; padding:8px 11px; cursor:pointer; text-align:left; }
+  .findlink:hover { border-color:var(--blue); color:var(--blue); }
+  .findlink.findloop { padding:8px 11px; }
+  .findlink.findloop a { color:inherit; text-decoration:none; }
+  .crmToast { position:fixed; bottom:22px; left:50%; transform:translateX(-50%) translateY(12px); background:var(--bg2); border:1px solid var(--line); color:var(--ink); font-size:12.5px; padding:9px 16px; border-radius:20px; opacity:0; transition:opacity .2s,transform .2s; z-index:9500; pointer-events:none; }
+  .crmToast.on { opacity:1; transform:translateX(-50%) translateY(0); }
+  /* Card-view agent-contact block (every card, every asset type) */
+  .card .cardagent { margin-top:8px; border-top:1px solid var(--line); padding-top:8px; font-size:12px; }
+  .card .cardagent .ca-top { display:flex; flex-wrap:wrap; align-items:center; gap:6px; }
+  .card .cardagent .ainrow { display:flex; flex-wrap:wrap; gap:10px; margin-top:3px; }
+  .card .cardagent .ain { color:var(--mut); text-decoration:none; font-size:11px; }
+  .card .cardagent .ain:hover { color:var(--blue); }
+  .card .cardagent .acts { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
+  /* Full contact modal info row */
+  .composeCard .cm-info { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-bottom:10px; }
+  .composeCard .cm-info .crmf { background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12px; padding:6px 8px; min-width:140px; flex:1 1 140px; margin:0; }
+  .composeCard .cm-info .crmf:focus { outline:none; border-color:var(--blue); }
+  .composeH .flink { font-size:11px; font-weight:400; margin-left:8px; }
+  /* Inline (no-outside-link) agent bits */
+  .aname { font-weight:600; color:var(--ink); }
+  a.aname { text-decoration:none; cursor:pointer; } a.aname:hover { color:var(--blue); text-decoration:underline; }
+  .flink { cursor:pointer; }
+  .listtbl td .flink { border-bottom:1px dotted #3a4453; }
+  .listtbl td .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  /* ── drill atom: every displayed data point filters to its deeper view ── */
+  .drill { cursor:pointer; border-bottom:1px dotted transparent; }
+  .drill:hover { border-bottom-color:currentColor; filter:brightness(1.15); }
+  .badges .b.drill:hover { border-bottom-color:currentColor; }
+  .city .drill { color:inherit; }
+  .city .drill:hover { color:var(--blue); border-bottom-color:var(--blue); }
+  .copyb { background:transparent; border:0; color:var(--mut); cursor:pointer; font-size:11px; padding:0 3px; border-radius:4px; }
+  .copyb:hover { color:var(--blue); }
+  .crm-note, .lexpbody .crm-note { margin-top:6px; font-size:12px; color:var(--mut); white-space:pre-wrap; background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:6px 9px; text-align:left; }
+  .composeCard .cm-notes { margin-bottom:10px; }
+  .composeCard .cm-notes .deeplbl { margin-bottom:4px; }
+  .composeCard .cm-notes textarea { width:100%; box-sizing:border-box; background:var(--bg); border:1px solid var(--line); border-radius:8px; color:var(--ink); font-size:12.5px; padding:7px 9px; font-family:inherit; resize:vertical; line-height:1.5; }
+  .composeCard .cm-notes textarea:focus { outline:none; border-color:var(--blue); }
+  /* ---- Amazon-style docked left filter rail (desktop) ---- */
+  .shell { display:flex; align-items:flex-start; }
+  main { flex:1 1 auto; min-width:0; }
+  @media (min-width:901px){
+    #burger { display:none; }
+    #scrim { display:none !important; }
+    #drawer { position:sticky; top:57px; height:calc(100vh - 57px); transform:none; flex:0 0 var(--drawerW); box-shadow:none; }
+    #drawer .dhead .x { display:none; }
+  }
+  /* collapsible filter tabs */
+  .fsec > h4 { cursor:pointer; display:flex; align-items:center; justify-content:space-between; user-select:none; }
+  .fsec > h4::after { content:'▾'; color:var(--mut); font-size:11px; transition:transform .15s; margin-left:8px; }
+  .fsec.collapsed > h4::after { transform:rotate(-90deg); }
+  .fsec.collapsed > *:not(h4) { display:none !important; }
+  .assessline { border-top:1px dashed rgba(255,255,255,.06); padding-top:6px; margin-top:2px; }
+  /* active-filter summary bar */
+  .activebar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:10px 20px; border-bottom:1px solid var(--line); background:rgba(88,166,255,.03); }
+  .activebar:empty { display:none; }
+  .activebar .lbl { font-size:11px; color:var(--mut); text-transform:uppercase; letter-spacing:.5px; }
+  .afchip { display:inline-flex; align-items:center; gap:6px; background:rgba(88,166,255,.10); border:1px solid rgba(88,166,255,.4); color:var(--blue); border-radius:16px; padding:4px 10px; font-size:12px; cursor:pointer; }
+  .afchip:hover { background:rgba(88,166,255,.2); } .afchip .xx { opacity:.65; font-weight:700; }
+  .afreset { background:transparent; border:1px solid #7d2b28; color:var(--bad); border-radius:14px; padding:4px 11px; font-size:12px; cursor:pointer; margin-left:auto; }
+  .afreset:hover { background:rgba(248,81,73,.1); }
+  /* shortlist stars */
+  .starbtn { background:none; border:0; color:var(--mut); font-size:18px; line-height:1; cursor:pointer; padding:0; }
+  .starbtn:hover, .starbtn.on { color:var(--warn); }
+  .copybtn { background:none; border:0; color:var(--mut); font-size:14px; line-height:1; cursor:pointer; padding:0; }
+  .copybtn:hover { color:var(--blue); }
+  .listtbl .starbtn { font-size:14px; margin-right:5px; vertical-align:middle; }
+  #shortbtn.shorton { background:rgba(210,153,34,.14); border-color:var(--warn); color:var(--warn); }
+  /* aggregate stats strip */
+  .statsbar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:9px 20px; border-bottom:1px solid var(--line); }
+  .statsbar:empty { display:none; }
+  .statpill { font-size:12px; color:var(--ink); background:var(--card); border:1px solid var(--line); border-radius:8px; padding:4px 10px; }
+  .statpill .sl { color:var(--mut); } .statpill b { font-variant-numeric:tabular-nums; }
+  .heatlegend { display:none; align-items:center; gap:5px; font-size:11px; color:var(--mut); }
+  .heatlegend.on { display:inline-flex; }
+  .heatlegend .bar { width:56px; height:8px; border-radius:4px; background:linear-gradient(90deg,hsl(0,70%,45%),hsl(60,70%,45%),hsl(120,70%,45%)); }
+  .showmore-wrap { grid-column:1/-1; text-align:center; padding:20px 0 6px; }
+  .showmore { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:10px; padding:9px 20px; font-size:13px; cursor:pointer; }
+  .showmore:hover { border-color:var(--blue); }
+  .quickstart { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin:16px 0; }
+  .qchip { background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:18px; padding:7px 14px; font-size:13px; cursor:pointer; }
+  .qchip:hover { border-color:var(--blue); color:var(--blue); }
+  .savedrow { display:flex; gap:6px; align-items:center; margin-bottom:5px; }
+  .savedname { flex:1; text-align:left; background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:8px; padding:6px 9px; font-size:12px; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+  .savedname:hover { border-color:var(--blue); color:var(--blue); }
+  .saveddel { background:none; border:0; color:var(--mut); cursor:pointer; font-size:13px; }
+  .saveddel:hover { color:var(--bad); }
+  #savebtn { width:100%; background:transparent; border:1px dashed #284b7a; color:var(--blue); border-radius:8px; padding:7px; font-size:12px; cursor:pointer; margin-top:4px; }
+  .savedempty { color:var(--mut); font-size:11px; }
+  /* compare overlay */
+  #cmpov { display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:60; align-items:flex-start; justify-content:center; padding:40px 14px; overflow:auto; }
+  #cmpov.on { display:flex; }
+  #cmpmodal { width:min(1100px,97vw); background:var(--card); border:1px solid var(--line); border-radius:15px; padding:20px 22px; position:relative; }
+  .cmpx { position:absolute; top:12px; right:14px; background:none; border:0; color:var(--mut); font-size:22px; cursor:pointer; }
+  .cmptbl { width:100%; border-collapse:collapse; font-size:13px; }
+  .cmptbl th, .cmptbl td { padding:8px 11px; border-top:1px solid var(--line); text-align:right; white-space:nowrap; }
+  .cmptbl thead th { position:sticky; top:0; background:var(--card); text-align:right; vertical-align:bottom; color:var(--ink); }
+  .cmptbl .cmpk, .cmptbl thead th.cmpk { text-align:left; color:var(--mut); position:sticky; left:0; background:var(--card); }
+  .cmptbl thead th .cmpsub { color:var(--mut); font-size:11px; font-weight:400; }
+  .cmphint { color:var(--mut); padding:24px 4px; }
+  .cmptools { display:flex; gap:8px; justify-content:flex-end; margin-bottom:10px; }
+  .cmptools button { background:var(--card); border:1px solid var(--line); color:var(--blue); border-radius:8px; padding:6px 12px; font-size:12px; cursor:pointer; }
+  .cmptools button:hover { border-color:var(--blue); }
+  @media print {
+    body.printing-compare * { visibility:hidden; }
+    body.printing-compare #cmpov, body.printing-compare #cmpov * { visibility:visible; }
+    body.printing-compare #cmpov { position:absolute; inset:0; background:#fff; display:block; padding:0; overflow:visible; }
+    body.printing-compare #cmpmodal { width:100%; box-shadow:none; border:0; background:#fff; color:#000; }
+    body.printing-compare #cmpmodal h3 { color:#000; }
+    body.printing-compare .cmpx, body.printing-compare .no-print { display:none !important; }
+    body.printing-compare .cmptbl th, body.printing-compare .cmptbl td { color:#000; border-color:#999; }
+    body.printing-compare .cmptbl thead th, body.printing-compare .cmptbl .cmpk { background:#fff; color:#000; }
+    body.printing-compare .cmpsub { color:#444; }
+  }
+
+</style>
+</head><body>
+<header>
+  <button class="burger" id="burger" aria-label="Open filters">☰ Filters <span class="fcount" id="fcount">0</span></button>
+  <div class="htitle">
+    <h1>LA County CRE — Investment Explorer</h1>
+    <div class="sub" id="sub">loading…</div>
+  </div>
+  <div class="spacer"></div>
+  <div class="topnav">
+  <a href="/crcp.html" style="color:var(--warn);text-decoration:none;font-size:13px;margin-right:12px" title="Commercial Real Estate Control Panel (live)">🛰️ CRCP</a>
+  <a href="/map.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All properties on a map, colored by type, with overhead aerial photos">🗺️ Map</a>
+  <a href="/mls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Spreadsheet-style listings + grid — all property data, sortable & searchable">📋 Listings</a>
+  <a href="/valley-pools.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="San Fernando Valley single-family pool homes under $1.2M — deal-colored map + $/sqft + price-cut signals + payment estimate">🏊 Valley Pools</a>
+  <a href="/deals.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Liquid-glass deal-intelligence charts">◆ Deals</a>
+  <a href="/deals-flow.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Closed deal flow — public-record sales + FHA + SEC REIT, source-badged">💰 Deal Flow</a>
+  <a href="/rent-rolls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Rent-roll → DSCR underwriting">📑 Rent Rolls</a>
+  <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All brokers/agents — grid + list, sortable by every field">👥 Broker grid</a>
+  <a href="/brokers.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent relationship mind-map">🕸️ Broker map</a>
+  <a href="/graphics.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / firm / warrantability charts">📊 Graphics</a>
+  <a href="/condos.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LA condos — FHA warrantability">🏢 Condos</a>
+  <a href="/fha-loans.html" style="color:var(--gold,#e9c46a);text-decoration:none;font-size:13px;margin-right:12px" title="HUD/FHA multifamily loans originally ≤3.5% — historic-low-rate flagged">◆ FHA Loans</a>
+  <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent contacts — name, company, phone, email">👤 Brokers</a>
+  <a href="/linkedin.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LinkedIn follow-list">🔗 LinkedIn</a>
+  <a href="/licensed.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Licensed directory — RE agents, firms, title & escrow + firm IG">📋 Licensed</a>
+  </div><!-- /.topnav -->
+</header>
+<div style="padding:40px 20px;color:var(--mut)">…grid would render here…</div>
+<script src="/corner-nav.js" defer></script>
+<script>window.addEventListener("load",function(){var s=document.getElementById("sub");if(s)s.textContent="15440 listings · 2677 condos · Los Angeles County, CA";});</script>
+</body></html>
\ No newline at end of file
diff --git a/front-page-audit/screens/crcp/repro/serve-fb.cjs b/front-page-audit/screens/crcp/repro/serve-fb.cjs
new file mode 100644
index 0000000..44f31a1
--- /dev/null
+++ b/front-page-audit/screens/crcp/repro/serve-fb.cjs
@@ -0,0 +1,5 @@
+const http=require("http"),fs=require("fs");const SID=process.env.CRCP_SID||"(set CRCP_SID env var)";
+http.createServer((req,res)=>{const p=req.url;
+ if(p==="/"||p.startsWith("/?")){res.writeHead(200,{"Content-Type":"text/html"});res.end(fs.readFileSync(__dirname+"/real-index-fallback.html"));return;}
+ const u=new URL("http://127.0.0.1:9912"+p);const pr=http.request({hostname:u.hostname,port:u.port,path:u.pathname+u.search,method:req.method,headers:{Cookie:"crcp_sid="+SID}},(r2)=>{res.writeHead(r2.statusCode,r2.headers);r2.pipe(res);});pr.on("error",e=>{res.writeHead(502);res.end(e.message)});req.pipe(pr);
+}).listen(0,"127.0.0.1",function(){console.log("FBPORT="+this.address().port)});
diff --git a/front-page-audit/screens/crcp/repro/serve-probe.cjs b/front-page-audit/screens/crcp/repro/serve-probe.cjs
new file mode 100644
index 0000000..457d6d4
--- /dev/null
+++ b/front-page-audit/screens/crcp/repro/serve-probe.cjs
@@ -0,0 +1,5 @@
+const http=require("http"),fs=require("fs");const SID=process.env.CRCP_SID||"(set CRCP_SID env var)",UP={h:"127.0.0.1",p:9912};
+http.createServer((req,res)=>{const p=req.url;
+ if(p==="/"||p.startsWith("/?")){res.writeHead(200,{"Content-Type":"text/html"});res.end(fs.readFileSync(__dirname+"/real-index-probe.html"));return;}
+ const u=new URL("http://127.0.0.1:9912"+p);const pr=http.request({hostname:u.hostname,port:u.port,path:u.pathname+u.search,method:req.method,headers:{Cookie:"crcp_sid="+SID}},(r2)=>{res.writeHead(r2.statusCode,r2.headers);r2.pipe(res);});pr.on("error",e=>{res.writeHead(502);res.end(e.message)});req.pipe(pr);
+}).listen(0,"127.0.0.1",function(){console.log("PROBEPORT="+this.address().port)});
diff --git a/front-page-audit/screens/crcp/repro/serve-real.cjs b/front-page-audit/screens/crcp/repro/serve-real.cjs
new file mode 100644
index 0000000..5d43b2d
--- /dev/null
+++ b/front-page-audit/screens/crcp/repro/serve-real.cjs
@@ -0,0 +1,20 @@
+const http=require("http");
+const SID=process.env.CRCP_SID||"(set CRCP_SID env var)";
+const UP="http://127.0.0.1:9912";
+const fs=require("fs");
+http.createServer((req,res)=>{
+  const p=req.url;
+  if(p==="/"||p.startsWith("/?")){
+    res.writeHead(200,{"Content-Type":"text/html"});
+    res.end(fs.readFileSync(__dirname+"/real-index.html"));
+    return;
+  }
+  // proxy everything else to upstream with the cookie
+  const http2=require("http");
+  const u=new URL(UP+p);
+  const preq=http2.request({hostname:u.hostname,port:u.port,path:u.pathname+u.search,method:req.method,headers:{Cookie:"crcp_sid="+SID}},(pr)=>{
+    res.writeHead(pr.statusCode,pr.headers); pr.pipe(res);
+  });
+  preq.on("error",e=>{res.writeHead(502);res.end("proxy err "+e.message);});
+  req.pipe(preq);
+}).listen(0,"127.0.0.1",function(){console.log("REALPORT="+this.address().port);});
diff --git a/front-page-audit/screens/crcp/repro/serve-repro.js b/front-page-audit/screens/crcp/repro/serve-repro.js
new file mode 100644
index 0000000..7494d0d
--- /dev/null
+++ b/front-page-audit/screens/crcp/repro/serve-repro.js
@@ -0,0 +1,12 @@
+const http=require("http"),fs=require("fs"),path=require("path");
+const dir=__dirname;
+const types={".html":"text/html",".js":"application/javascript",".css":"text/css"};
+http.createServer((req,res)=>{
+  let p=req.url==="/"?"/repro.html":req.url.split("?")[0];
+  const f=path.join(dir,p);
+  const resolved=path.resolve(f);
+  const dirResolved=path.resolve(dir);
+  if(!resolved.startsWith(dirResolved+path.sep)&&resolved!==dirResolved){res.writeHead(403);res.end("forbidden");return;}
+  fs.readFile(f,(e,d)=>{ if(e){res.writeHead(404);res.end("nf "+p);return;}
+    res.writeHead(200,{"Content-Type":types[path.extname(f)]||"text/plain"});res.end(d);});
+}).listen(0,"127.0.0.1",function(){console.log("PORT="+this.address().port);});
diff --git a/front-page-audit/screens/flockedwallpaper/2026-09-22T21-13-05-738Z/desktop-1280x800.png b/front-page-audit/screens/flockedwallpaper/2026-09-22T21-13-05-738Z/desktop-1280x800.png
new file mode 100644
index 0000000..4436dcb
Binary files /dev/null and b/front-page-audit/screens/flockedwallpaper/2026-09-22T21-13-05-738Z/desktop-1280x800.png differ
diff --git a/front-page-audit/screens/flockedwallpaper/2026-09-22T21-13-05-738Z/mobile-390x844.png b/front-page-audit/screens/flockedwallpaper/2026-09-22T21-13-05-738Z/mobile-390x844.png
new file mode 100644
index 0000000..b7f7c79
Binary files /dev/null and b/front-page-audit/screens/flockedwallpaper/2026-09-22T21-13-05-738Z/mobile-390x844.png differ
diff --git a/front-page-audit/screens/jutewallpaper/2026-09-22T21-10-42-273Z/desktop-1280x800.png b/front-page-audit/screens/jutewallpaper/2026-09-22T21-10-42-273Z/desktop-1280x800.png
new file mode 100644
index 0000000..816a249
Binary files /dev/null and b/front-page-audit/screens/jutewallpaper/2026-09-22T21-10-42-273Z/desktop-1280x800.png differ
diff --git a/front-page-audit/screens/jutewallpaper/2026-09-22T21-10-42-273Z/mobile-390x844.png b/front-page-audit/screens/jutewallpaper/2026-09-22T21-10-42-273Z/mobile-390x844.png
new file mode 100644
index 0000000..9ef1cc8
Binary files /dev/null and b/front-page-audit/screens/jutewallpaper/2026-09-22T21-10-42-273Z/mobile-390x844.png differ
diff --git a/front-page-audit/screens/linenwallpaper/2026-09-22T21-10-42-273Z/desktop-1280x800.png b/front-page-audit/screens/linenwallpaper/2026-09-22T21-10-42-273Z/desktop-1280x800.png
new file mode 100644
index 0000000..6e5d737
Binary files /dev/null and b/front-page-audit/screens/linenwallpaper/2026-09-22T21-10-42-273Z/desktop-1280x800.png differ
diff --git a/front-page-audit/screens/linenwallpaper/2026-09-22T21-10-42-273Z/mobile-390x844.png b/front-page-audit/screens/linenwallpaper/2026-09-22T21-10-42-273Z/mobile-390x844.png
new file mode 100644
index 0000000..8edfdd6
Binary files /dev/null and b/front-page-audit/screens/linenwallpaper/2026-09-22T21-10-42-273Z/mobile-390x844.png differ
diff --git a/front-page-audit/screens/linenwallpaper/2026-09-22T21-13-05-738Z/desktop-1280x800.png b/front-page-audit/screens/linenwallpaper/2026-09-22T21-13-05-738Z/desktop-1280x800.png
new file mode 100644
index 0000000..b1e4ba2
Binary files /dev/null and b/front-page-audit/screens/linenwallpaper/2026-09-22T21-13-05-738Z/desktop-1280x800.png differ
diff --git a/front-page-audit/screens/linenwallpaper/2026-09-22T21-13-05-738Z/mobile-390x844.png b/front-page-audit/screens/linenwallpaper/2026-09-22T21-13-05-738Z/mobile-390x844.png
new file mode 100644
index 0000000..f15ec46
Binary files /dev/null and b/front-page-audit/screens/linenwallpaper/2026-09-22T21-13-05-738Z/mobile-390x844.png differ
diff --git a/front-page-audit/screens/silkwallpaper/2026-09-22T21-10-42-273Z/desktop-1280x800.png b/front-page-audit/screens/silkwallpaper/2026-09-22T21-10-42-273Z/desktop-1280x800.png
new file mode 100644
index 0000000..77694ed
Binary files /dev/null and b/front-page-audit/screens/silkwallpaper/2026-09-22T21-10-42-273Z/desktop-1280x800.png differ
diff --git a/front-page-audit/screens/silkwallpaper/2026-09-22T21-10-42-273Z/mobile-390x844.png b/front-page-audit/screens/silkwallpaper/2026-09-22T21-10-42-273Z/mobile-390x844.png
new file mode 100644
index 0000000..6188b03
Binary files /dev/null and b/front-page-audit/screens/silkwallpaper/2026-09-22T21-10-42-273Z/mobile-390x844.png differ
diff --git a/front-page-audit/screens/silkwallpaper/2026-09-22T21-13-05-738Z/desktop-1280x800.png b/front-page-audit/screens/silkwallpaper/2026-09-22T21-13-05-738Z/desktop-1280x800.png
new file mode 100644
index 0000000..a2b212b
Binary files /dev/null and b/front-page-audit/screens/silkwallpaper/2026-09-22T21-13-05-738Z/desktop-1280x800.png differ
diff --git a/front-page-audit/screens/silkwallpaper/2026-09-22T21-13-05-738Z/mobile-390x844.png b/front-page-audit/screens/silkwallpaper/2026-09-22T21-13-05-738Z/mobile-390x844.png
new file mode 100644
index 0000000..a69c086
Binary files /dev/null and b/front-page-audit/screens/silkwallpaper/2026-09-22T21-13-05-738Z/mobile-390x844.png differ

← 46b9a82 auto-data-snapshot: 2026-09-22T16:09:18 (1 data files) — fro  ·  back to Agent Cabinet  ·  auto-data-snapshot: 2026-09-22T16:42:26 (2 data files) — fro bca6c2b →