/* ═══════════════════════════════════════════════════════════════════════════
   EXPLORER — the neighborhood orbit, ported into the Analytics tab.
   ═══════════════════════════════════════════════════════════════════════════

   SOURCE. prototypes/neighborhood-explorer.html, which was built as a
   standalone document and is kept on disk as the reference implementation.
   Dr. Rocker, 2026-08-03 (1:44:12): "This should be in the same file, by the
   way, and just another tab." Joah Massey, 2026-08-31, independently: "Explorer
   and Analytics could be combined... Explorer is so clean and easy to
   understand." This file is that merge.

   EVERY RULE IS SCOPED TO #nx-app, MECHANICALLY. The prototype and this
   application share 21 class names — card, rail, kpi, toolbar, on, off, n —
   because both were written against DESIGN.md. Unscoped, `.rail` alone would
   restyle the Zoning tab's controls drawer, and the failure would present as
   "the redesign broke the tab the reviewer praised."

   THE TOKEN BLOCK IS SCOPED TOO, AND THAT IS THE IMPORTANT ONE. The prototype
   declared --bg, --panel, --ink, --line, --chrome and thirty more on :root,
   with the SAME NAMES as cambridge.css and different values. Left on :root it
   would have repainted the entire application from the moment this file
   loaded. As #nx-app it cascades to the explorer and stops at its boundary.

   The prototype's html{} and body{} rules are dropped, not scoped. They belong
   to a standalone document; body{overflow:hidden} would have frozen the host
   page's scroll.

   DO NOT hand-edit the scoping. Regenerate:
     python3 scripts/scope-explorer-css.py > explorer.css
   ═══════════════════════════════════════════════════════════════════════════ */

/* ================================================================
   ONE TOKEN BLOCK — civic palette lifted from cambridge.css / DESIGN.md.
   Every colour, space and radius below this block is a var() reference.
   Zero colour literals appear after this rule. (Research rule 1)
   ================================================================ */
#nx-app {
  /* surfaces */
  --bg:#f4f6f9; --panel:#ffffff; --wash:#eef2f6; --wash-2:#f7f9fb;
  --line:#e3e8ee; --line-2:#cfd8e0; --map-void:#ecf0f3;
  /* ink */
  --ink:#1e2732; --ink-2:#3d4854; --muted:#586b7d; --muted-2:#8593a2;
  /* ── CHROME · Atlas Nr. 51 Spinellblau. THE interface accent since
     2026-08-16, replacing teal, mirroring the same migration in
     cambridge.css (see its :root block for the full reasoning). It is a WCAG
     fix, not a taste change: teal #2b8577 measures 4.11:1 on the #f4f6f9 page
     field and fails the AA floor of 4.5; Spinellblau measures 6.66:1, and
     7.22:1 for white-on-fill.

     ── THE FORM LAW · read this before using either blue ──
     --chrome and --inc-4 below are THE SAME HEX, #1c5c84. That is not a
     mistake and it must not be "cleaned up": --inc-4 is the top step of the
     published four-class income ramp, reproduced from cambridge.js and
     prototypes/reports.html, and repointing it would silently restyle a legend
     that is already in circulation. The two never collide because they are
     separated by FORM, not by hue:
       · chrome blue — a 2px underline or left border, a focus ring, a filled
                       control with panel-coloured text, or a wash at <=12%.
       · income blue — a map fill, a legend swatch, a bar. Never chrome.
     The one place they sit in the same row is .nrow.is-on (chrome left border
     + chrome number badge) beside its .ndot income swatch, ~120px apart at
     opposite ends of the row: a 2px rule and a filled badge against an 10px
     rimmed square. Different form, so they read as different things.
     Chrome blue is NEVER painted into the key map or the Mapbox canvas as
     data — see --teal. ── */
  --chrome:#1c5c84; --chrome-wash:#eff4f9; --chrome-line:#c3d7e6;

  /* ── TEAL · WAS the accent until 2026-08-16. NO LONGER CHROME. Three kinds
     of reader keep it, and each is a deliberate exception:
     (a) DRAWN GEOGRAPHY — .mpoly / .mplate / .map-poly.is-sel / .selkey, and
         the Mapbox sel-fill + sel-line paints that tokenColor() reads out of
         this block. Selection is blue in the DOM and teal on any map surface
         on purpose: chrome blue may not be painted into a map (form law).
     (b) The obs / mod / prv / crw PROVENANCE ENCODING (.prov, .nchip). Those
         four hues ARE the encoding, and `mod` is already --blue-deep — moving
         `obs` to chrome blue would merge two of the four classes.
     (c) .tag.live and .units.up — semantic verdicts, which cambridge.css also
         kept teal for exactly this reason. ── */
  --teal:#2b8577; --teal-deep:#1f5b52; --teal-wash:#eef6f4; --teal-line:#bcdad3;
  --blue:#2f6db0; --blue-deep:#1c5c84; --blue-wash:#eff4f9; --blue-line:#c3d7e6;
  /* --amber moved #a9781f -> Civic Burnt Orange #B45309 on 2026-08-16, same
     WCAG reason as the accent: #a9781f measures 3.60:1 on the page field and
     fails AA, #B45309 measures 4.64:1 and passes. -wash and -line did not
     move; they are washes, and they were never the failure. Alerts and
     highlights only — never a primary control. */
  --amber:#B45309; --amber-wash:#fbf3e4; --amber-line:#e8d6ae;
  --risk:#c0504d; --risk-wash:#fdf4f4; --risk-line:#e7c4c3;
  --violet:#6d3f9c; --violet-wash:#f3eefa; --violet-line:#d9c9ec;
  /* ONE income palette — four ordered civic blues, macro classes.
     Identical in the rail dots, the class strip, the mobile legend.
     --inc-4, --bar-hi and --ten-1 are all #1c5c84, the same hex as --chrome.
     DATA endpoints, published; they do not move. See THE FORM LAW above. */
  --inc-1:#c5dbe4; --inc-2:#87b3c6; --inc-3:#4a86a4; --inc-4:#1c5c84;
  /* single-series distributions: age · education · journey to work */
  --bar:#4a86a4; --bar-hi:#1c5c84; --bar-track:#eef2f6;
  /* tenure — ordered occupancy ramp, occupied to vacant */
  --ten-1:#1c5c84; --ten-2:#6e9db4; --ten-3:#b9ccd7; --ten-4:#dfe6eb;
  /* population composition — neutral civic family, always direct-labelled */
  --pop-1:#274b60; --pop-2:#6f8688; --pop-3:#9fb0bd; --pop-4:#c9d3da;
  /* hairlines on drawn geometry */
  --rim:rgba(30,39,50,.34);
  /* geometry — exactly two corner radii, one 4/8 spacing scale */
  --r-panel:12px; --r-ctl:8px;
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px;
  --shadow:0 1px 2px rgba(30,39,50,.08);
}
#nx-app *, #nx-app *::before, #nx-app *::after {box-sizing:border-box;margin:0;padding:0}
#nx-app button, #nx-app input, #nx-app a {font:inherit;font-variant-numeric:inherit}
#nx-app button {background:none;border:0;color:inherit;cursor:pointer}
#nx-app a {color:var(--blue-deep)}
#nx-app :focus-visible {outline:2px solid var(--chrome);outline-offset:2px}
#nx-app svg :focus-visible {outline:none}
#nx-app .sr {position:absolute!important;width:1px!important;height:1px!important;overflow:hidden!important;
  clip:rect(0 0 0 0)!important;white-space:nowrap!important;border:0!important}
#nx-app .tnum {font-variant-numeric:tabular-nums lining-nums}/* ---------------- document chrome (spec page, not the product) --------------- */
#nx-app .page {width:min(1500px,100%);margin:0 auto;padding:0 20px 64px}
#nx-app .doc-head {padding:40px 0 22px;border-bottom:1px solid var(--line)}
#nx-app .doc-kicker {font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:700}
#nx-app .doc-title {font-family:'Instrument Serif',serif;font-weight:400;font-size:44px;line-height:1.06;margin:10px 0 8px;letter-spacing:-.01em}
#nx-app .doc-title em {font-style:italic}
#nx-app .doc-sub {max-width:78ch;color:var(--muted);font-size:15px}
#nx-app .doc-meta {display:flex;flex-wrap:wrap;gap:var(--s2);margin-top:var(--s4)}
#nx-app .doc-meta span {font-size:11.5px;color:var(--muted);border:1px solid var(--line);border-radius:var(--r-ctl);padding:4px 9px;background:var(--panel)}/* --------- "What changed and why" register --------- */
#nx-app .changed {margin-top:var(--s5);border:1px solid var(--line);border-left:3px solid var(--teal);
  border-radius:var(--r-panel);background:var(--panel);padding:var(--s4) var(--s5) var(--s5)}
#nx-app .ch-t {font-family:'Instrument Serif',serif;font-weight:400;font-size:24px;line-height:1.15}
#nx-app .ch-sub {font-size:13px;color:var(--muted);max-width:96ch;margin-top:2px}
#nx-app .ch-hd, #nx-app .ch-row {display:grid;grid-template-columns:34px 1fr 1.15fr 0.85fr;gap:var(--s4);align-items:baseline}
#nx-app .ch-hd {margin-top:var(--s4);padding-bottom:6px;border-bottom:1px solid var(--line-2)}
#nx-app .ch-hd span {font-size:10px;letter-spacing:.12em;text-transform:uppercase;font-weight:700;color:var(--muted)}
#nx-app .ch-row {padding:9px 0;border-bottom:1px solid var(--line)}
#nx-app .ch-row:last-of-type {border-bottom:0}
#nx-app .ch-n {font-family:'Instrument Serif',serif;font-size:17px;color:var(--muted-2);line-height:1}
#nx-app .ch-was {font-size:12.5px;color:var(--muted);line-height:1.45}
#nx-app .ch-now {font-size:12.5px;color:var(--ink);line-height:1.45}
#nx-app .ch-now b {font-weight:700}
#nx-app .ch-rule {font-size:11.5px;color:var(--teal-deep);font-weight:700;line-height:1.4}
#nx-app .ch-keep {margin-top:var(--s4);border-top:1px solid var(--line-2);padding-top:var(--s3);
  font-size:12.5px;color:var(--muted);line-height:1.55;max-width:120ch}
#nx-app .ch-keep b {color:var(--ink)}
#nx-app code {font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;color:var(--ink-2);
  background:var(--wash);border:1px solid var(--line);padding:1px 4px}/* --------- stage + frame --------- */
#nx-app .stage {padding-top:48px}
#nx-app .stage-head {display:flex;align-items:flex-start;gap:var(--s4);margin-bottom:var(--s4)}
#nx-app .stage-num {font-family:'Instrument Serif',serif;font-size:32px;line-height:1;color:var(--muted-2);min-width:40px}
#nx-app .stage-title {font-family:'Instrument Serif',serif;font-weight:400;font-size:27px;line-height:1.15;letter-spacing:-.01em}
#nx-app .stage-desc {color:var(--muted);font-size:13.5px;max-width:100ch;margin-top:3px}
#nx-app .frame-scroll {overflow-x:auto;overflow-y:hidden;padding:2px 2px 10px}
#nx-app .frame {width:1440px;background:var(--panel);border:1px solid var(--line-2);border-radius:var(--r-panel);
  overflow:hidden;box-shadow:var(--shadow)}
#nx-app .figcap {font-size:11.5px;color:var(--muted);margin-top:4px}
#nx-app .figcap b {color:var(--ink-2);font-weight:700}
#nx-app .notes {display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s3) var(--s5);margin-top:var(--s4);list-style:none}
#nx-app .notes li {border-top:1px solid var(--line);padding-top:var(--s2);font-size:12.5px;color:var(--muted);line-height:1.5}
#nx-app .notes li b {display:block;color:var(--ink);font-size:10.5px;letter-spacing:.11em;text-transform:uppercase;font-weight:700;margin-bottom:3px}
#nx-app .notes li em {font-style:normal;color:var(--teal-deep);font-weight:700}
#nx-app .pfoot {margin-top:52px;border-top:1px solid var(--line);padding-top:var(--s4);font-size:12px;color:var(--muted);
  display:flex;gap:var(--s5);flex-wrap:wrap}
#nx-app .pfoot b {color:var(--ink-2)}/* ================================================================
   PRODUCT SHELL — 1440 x 900
   ================================================================ */
#nx-app .app {display:grid;grid-template-columns:380px 1fr 389px;grid-template-rows:1fr 30px;
  height:900px;background:var(--panel)}/* ---- topbar ---- */
#nx-app .topbar {grid-column:1/4;display:flex;align-items:center;gap:var(--s3);padding:0 var(--s5);
  border-bottom:1px solid var(--line);background:var(--panel)}
#nx-app .brand {display:flex;align-items:baseline;gap:var(--s2);flex:0 0 auto}
#nx-app .brand .mark {font-family:'Instrument Serif',serif;font-size:21px;letter-spacing:-.01em;color:var(--ink)}
#nx-app .brand .bsub {font-size:13px;font-weight:600;color:var(--ink-2);white-space:nowrap}
#nx-app .tsep {width:1px;height:22px;background:var(--line);flex:0 0 1px}
#nx-app .tnote {font-size:11.5px;color:var(--muted-2);white-space:nowrap}
#nx-app .steps {margin-left:auto;display:flex;gap:var(--s2);flex:0 0 auto}
#nx-app .steps span {display:inline-flex;align-items:center;gap:6px;font-size:11.5px;color:var(--muted);
  border:1px solid var(--line);border-radius:var(--r-ctl);padding:4px 9px;background:var(--wash-2)}
#nx-app .steps b {font-weight:800;color:var(--ink-2);font-size:11px}
#nx-app .iconbtn {width:28px;height:28px;border:1px solid var(--line-2);border-radius:var(--r-ctl);
  color:var(--muted);display:grid;place-items:center;font-size:13px;line-height:1;flex:0 0 auto}/* ---- independence strip ---- */
#nx-app .indep {grid-column:1/4;display:flex;align-items:center;gap:var(--s3);padding:0 var(--s5);
  border-top:1px solid var(--line);background:var(--wash-2);font-size:11px;color:var(--muted)}
#nx-app .indep b {font-weight:700;color:var(--ink-2)}
#nx-app .indep .r {margin-left:auto;display:flex;gap:var(--s4)}/* ---- left rail ---- */
#nx-app .rail {border-right:1px solid var(--line);display:flex;flex-direction:column;padding:var(--s3) var(--s2) var(--s2);min-height:0}
#nx-app .rail-hd {display:flex;justify-content:space-between;align-items:baseline;padding:0 6px var(--s2);flex-wrap:wrap}
#nx-app .rail-hd h2 {font-size:14px;font-weight:800;letter-spacing:-.005em}
#nx-app .eyebrow {font-size:10px;letter-spacing:.12em;text-transform:uppercase;font-weight:700;color:var(--muted)}
#nx-app .srch {display:flex;align-items:center;gap:6px;margin:0 4px var(--s2);height:32px;padding:0 8px;
  border:1px solid var(--line);background:var(--wash-2);border-radius:var(--r-ctl)}
#nx-app .srch svg {flex:0 0 13px;color:var(--muted-2)}
#nx-app .srch input {flex:1;min-width:0;border:0;background:none;outline:0;font-size:12.5px}
#nx-app .srch input::placeholder {color:var(--muted-2)}
/* THE PICKER GETS A FLOOR  (2026-09-10)
   `flex:1; min-height:0` let this absorb whatever height was left — and with
   the key map moved into the rail there was none left: measured 13px tall at
   1440, showing zero of thirteen rows. It also made the rail's own
   `scrollHeight - clientHeight` structurally ZERO forever, so every "the rail
   does not overflow" measurement was true and meaningless, including three of
   mine. A floor makes the list keep about eight rows and pushes the excess
   into the rail's scroll, where a reader can actually reach it.
   scripts/checks/viewport-fit.mjs now asserts four rows visible from the
   reader's side, which is the assertion that would have caught this. */
#nx-app .nlist {flex:1 1 auto;min-height:248px;overflow:auto;display:flex;flex-direction:column;gap:2px;padding:1px 4px 6px;scrollbar-width:thin}
#nx-app .nlist::-webkit-scrollbar {width:6px}
#nx-app .nlist::-webkit-scrollbar-thumb {background:var(--line-2);border-radius:var(--r-ctl)}
#nx-app .nrow {width:100%;display:grid;grid-template-columns:24px 1fr 10px;align-items:center;gap:var(--s2);
  padding:6px 8px 6px 6px;border:1px solid transparent;border-left:2px solid transparent;
  border-radius:var(--r-ctl);text-align:left}
#nx-app .nrow:hover {background:var(--wash-2);border-color:var(--line)}
#nx-app .nrow.is-on {background:var(--chrome-wash);border-color:var(--chrome-line);border-left-color:var(--chrome)}
#nx-app .nnum {width:22px;height:20px;display:grid;place-items:center;border:1px solid var(--line-2);
  border-radius:var(--r-ctl);background:var(--panel);font-size:10.5px;font-weight:800;color:var(--ink-2)}
#nx-app .nrow.is-on .nnum {background:var(--chrome);border-color:var(--chrome);color:var(--panel)}
#nx-app .nname {font-size:12px;font-weight:600;line-height:1.2;color:var(--ink-2)}
#nx-app .nrow.is-on .nname {color:var(--ink);font-weight:700}
#nx-app .ndot {width:10px;height:10px;border:1px solid var(--rim);display:block}
/* The rulenote this used to hold explained that the list and the key map both
   select a place. It has been display:none since it was written, so the one
   sentence reconciling two controls never rendered. The controls now
   demonstrate the pairing by lighting each other (see markRow), which is why
   the sentence is not being resurrected — .rail-help says the instruction once,
   at the top, and the behaviour says the rest. */
#nx-app .rail-ft {display:none}
/* The instruction lived on its own line under the heading and repeated what
   the heading already framed: two blocks, ~57px, to say "choose a
   neighbourhood" and "you can do it two ways". Folded into the heading as a
   third element — one block, one instruction, ~22px back for the rail's scroll
   budget, and copy rule 1 satisfied rather than argued with. */
#nx-app .rail-hd-how {flex-basis:100%;font-size:11.5px;color:var(--muted);margin-top:2px;}
/* .phead now sits in the rail under the key map: it names the place and the
   sources it is drawn from. The 27px hero it used to be belonged to a 389px
   pane; here it is a caption over a map. */
#nx-app .rail .phead {padding:8px 4px 2px;border-top:1px solid var(--line);margin-top:8px;flex:0 0 auto;}
/* TWO TO A ROW, AT ELEVEN PIXELS. Four provenance chips took a row each here —
   91px of a rail whose scroll budget the picker needs. The first attempt at
   this dropped them to 9.5px, which is the exact trade this plan spent a whole
   phase refusing: buying layout by going under the type floor, on the one
   surface whose job is to be read. Reverted.
   The width is going to TRACKING, not to size. At .13em a 24-character chip is
   ~194px and two will not fit 372px; at .04em it is ~170px and they pair. Same
   type size, same words, same information, 47px back. Folding them behind a
   <details> was also rejected: visible provenance is what this dashboard is
   FOR, and it does not go behind a disclosure triangle to buy space. */
#nx-app .rail .pmeta {gap:4px;margin-top:6px;}
#nx-app .rail .pmeta .prov {padding:0 5px;letter-spacing:.04em;}
/* AN ABSOLUTE ELEMENT NEEDS A POSITIONED ANCESTOR, OR IT ESCAPES  (2026-09-10)
   This is the visually-hidden heading the rail keeps for screen readers. With
   `position:absolute` and no offsets it holds its static position — but its
   CONTAINING BLOCK was the initial one, not the scrolling rail, so a 1px box
   sitting below the fold inside a scroller extended the DOCUMENT instead of the
   scroller. body is `overflow:hidden`, so the page grew 42px at 1440 and 86px
   at 1100 with no way to reach any of it. The overflow matched this element's
   overhang to the pixel, twice, which is how it was found.
   .ptitle is now the containing block, and clip-path replaces the deprecated
   clip. One px, one rule, one unreachable page. */
#nx-app .rail .ptitle {position:relative;}
#nx-app .sr-only {position:absolute;width:1px;height:1px;padding:0;margin:0;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0;}

#nx-app .rail .pmeta {margin-top:5px;}
#nx-app .pb-place {font-weight:800;color:var(--ink);}
#nx-app .rail-ft-legacy {border-top:1px solid var(--line);margin:0 4px;padding:var(--s3) 6px 4px}
#nx-app .inckey {display:grid;grid-template-columns:1fr 1fr;gap:3px 8px;margin:6px 0 7px}
#nx-app .inckey span {display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--ink-2);font-weight:600}
#nx-app .inckey i {width:11px;height:11px;border:1px solid var(--rim);display:block;flex:0 0 11px}
#nx-app .meta {font-size:11px;color:var(--muted-2);line-height:1.45}
#nx-app .rulenote {font-size:11px;color:var(--muted);line-height:1.45;margin-top:var(--s2);
  border-top:1px solid var(--line);padding-top:var(--s2)}
#nx-app .rulenote b {color:var(--ink-2);font-weight:700}
#nx-app .rail-links {display:grid;gap:5px;margin-top:var(--s2)}
#nx-app .rail-links a {text-decoration:none;border:1px solid var(--line-2);border-radius:var(--r-ctl);
  padding:6px 8px;background:var(--panel);color:var(--blue-deep);font-weight:700;font-size:11.5px}
#nx-app .rail-links a:hover {background:var(--blue-wash)}/* ---- centre pane ---- */
#nx-app .centre {position:relative;display:flex;flex-direction:column;background:var(--bg);padding:var(--s3) 20px var(--s2);min-width:0}
#nx-app .centre-top {display:flex;align-items:center;gap:var(--s3);flex:0 0 auto}
#nx-app .ctx {display:flex;flex-direction:column;gap:1px}
#nx-app .ctx b {font-size:13px;font-weight:700}
#nx-app .seg {display:flex;border:1px solid var(--line-2);border-radius:var(--r-ctl);background:var(--panel);overflow:hidden}
#nx-app .seg button {padding:5px 12px;font-size:12px;font-weight:700;color:var(--muted)}
#nx-app .seg button+button {border-left:1px solid var(--line-2)}
#nx-app .seg button.is-on {background:var(--ink-2);color:var(--panel)}
#nx-app .clegend {margin-left:auto;display:flex;align-items:center;gap:var(--s3);font-size:11px;color:var(--muted);
  border:1px solid var(--line);background:var(--panel);border-radius:var(--r-ctl);padding:5px 10px}
#nx-app .clegend span {display:inline-flex;align-items:center;gap:6px}
#nx-app .clegend i {display:block;flex:0 0 auto;border:1px solid var(--rim)}
#nx-app .k-permit {width:9px;height:9px;border-radius:50%;background:var(--blue)}
#nx-app .k-project {width:8px;height:8px;background:var(--violet);transform:rotate(45deg)}
#nx-app .clegend b {font-weight:inherit}
/* A dimmed marker kind dims its LABEL, never its swatch: DESIGN.md keeps legend
   swatches matching the canvas, and the word "(dimmed)" carries the state. */
/* `hidden` has to be restated here: `.clegend {display:flex}` and
   `.clegend span {display:inline-flex}` are author rules, and author CSS beats
   the UA's `[hidden]{display:none}`. Without this, syncMarkers() would set
   hidden on the chip of a kind the map no longer draws and the chip would stay
   on screen — a key for nothing (2026-09-11, when markers became each node's
   own). The old .is-dim rule went with the dimming it styled. */
#nx-app .clegend[hidden], #nx-app .clegend [hidden] {display:none}
#nx-app .graph-wrap {flex:1;min-height:0;display:grid;place-items:center;padding:var(--s2) 0}/* Drive the square off HEIGHT, not width. The centre column is wider than it
   is tall at 1440x900, so sizing on width produced an 820px square inside a
   713px wrap and clipped the bottom node clean off. *//* .graph-wrap is position:relative, so pinning the SVG to its box gives
   width/height:100% a DEFINITE parent to resolve against. In the grid+
   place-items:center arrangement the area was auto-height, so every % height
   silently fell back to the width and the bottom node was clipped off.
   The viewBox then fits itself via the default preserveAspectRatio. */
#nx-app #graph {position:absolute;inset:0;width:100%;height:100%;overflow:visible}
#nx-app .graph-note {flex:0 0 auto;text-align:center;font-size:11px;color:var(--muted);line-height:1.45;padding-bottom:2px}/* ---- orbit geometry ---- */
#nx-app .edge {stroke:var(--line);stroke-width:1}
#nx-app .edge.is-on {stroke:var(--chrome);stroke-width:2}
#nx-app .disc-bg {fill:var(--map-void)}
#nx-app .disc-rim {fill:none;stroke:var(--line-2);stroke-width:1}
#nx-app .tblock {fill:var(--panel)}
#nx-app .tblock-e {stroke:var(--line-2);stroke-width:1}
#nx-app .tb-eyebrow {font-size:11px;letter-spacing:.13em;font-weight:800;fill:var(--muted);text-anchor:middle}
#nx-app .tb-name {font-family:'Instrument Serif',serif;font-size:23px;fill:var(--ink);text-anchor:middle}
#nx-app .areachip-bg {fill:var(--panel);stroke:var(--line-2);stroke-width:1}
#nx-app .areachip-t {font-size:10.5px;letter-spacing:.13em;font-weight:800;fill:var(--ink-2);text-anchor:middle}
#nx-app .mroad {fill:none;stroke:var(--panel);stroke-width:5;stroke-linecap:round}
#nx-app .mroad.minor {stroke-width:2.6}
#nx-app .mbld {fill:var(--line);stroke:var(--line-2);stroke-width:.6}
#nx-app .mpoly {fill:var(--teal-wash)}
#nx-app .mpoly-e {fill:none;stroke:var(--teal-deep);stroke-width:2}
#nx-app .mplate {fill:var(--teal-wash);stroke:var(--teal-deep);stroke-width:1.6}
#nx-app .mground {fill:var(--wash)}
#nx-app .face-t {fill:var(--panel);stroke:var(--rim);stroke-width:.5}
#nx-app .face-f {fill:var(--wash);stroke:var(--rim);stroke-width:.5}
#nx-app .face-s {fill:var(--line-2);stroke:var(--rim);stroke-width:.5}
#nx-app .mk-permit {fill:var(--blue);stroke:var(--panel);stroke-width:1.6}
#nx-app .mk-project {fill:var(--violet);stroke:var(--panel);stroke-width:1.6}
#nx-app .narrow path {fill:var(--ink-2)}
#nx-app .narrow text {font-size:10px;fill:var(--muted);text-anchor:middle;font-weight:800}
#nx-app .centre-node {cursor:pointer;outline:none}
#nx-app .centre-node:focus-visible .disc-rim {stroke:var(--chrome);stroke-width:3}/* topic nodes — flat civic discs, hairline ring, drawn icon */
#nx-app .attribute {cursor:pointer;outline:none}
#nx-app .attribute .disc {fill:var(--panel);stroke:var(--line-2);stroke-width:1}
#nx-app .node-icon {fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;
  color:var(--ink-2);pointer-events:none}
#nx-app .attribute:hover .disc {fill:var(--wash-2);stroke:var(--ink-2)}
#nx-app .attribute:focus-visible .disc {stroke:var(--chrome);stroke-width:2.5}
#nx-app .attribute.is-on .disc {fill:var(--chrome);stroke:var(--chrome)}
#nx-app .attribute.is-on .node-icon {color:var(--panel)}
#nx-app .nlabel {font-size:13px;font-weight:700;fill:var(--ink-2);text-anchor:middle;pointer-events:none}
#nx-app .attribute.is-on .nlabel {fill:var(--ink)}
#nx-app .nvalue {font-size:12px;font-weight:700;fill:var(--muted);text-anchor:middle;pointer-events:none}
#nx-app .attribute.is-on .nvalue {fill:var(--chrome)}
#nx-app .nchip-bg {stroke-width:1;pointer-events:none}
#nx-app .nchip {font-size:10.5px;letter-spacing:.11em;font-weight:800;text-anchor:middle;pointer-events:none}
#nx-app .nchip-bg.obs {fill:var(--teal-wash);stroke:var(--teal-line)}
#nx-app .nchip.obs {fill:var(--teal-deep)}
#nx-app .nchip-bg.mod {fill:var(--blue-wash);stroke:var(--blue-line)}
#nx-app .nchip.mod {fill:var(--blue-deep)}
#nx-app .nchip-bg.prv {fill:var(--amber-wash);stroke:var(--amber-line)}
#nx-app .nchip.prv {fill:var(--amber)}
#nx-app .nchip-bg.crw {fill:var(--violet-wash);stroke:var(--violet-line)}
#nx-app .nchip.crw {fill:var(--violet)}/* ---- right panel ---- */
#nx-app .right {border-left:1px solid var(--line);overflow:auto;min-height:0;scrollbar-width:thin;background:var(--panel)}
#nx-app .right::-webkit-scrollbar {width:8px}
#nx-app .right::-webkit-scrollbar-thumb {background:var(--line-2);border-radius:var(--r-ctl)}
#nx-app .phead {padding:var(--s4) 20px var(--s3);border-bottom:1px solid var(--line)}
#nx-app .ptitle {display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s2);margin-top:3px}
#nx-app .ptitle h2 {font-family:'Instrument Serif',serif;font-weight:400;font-size:27px;line-height:1.08;letter-spacing:-.01em}
#nx-app .areachip {flex:0 0 auto;font-size:11px;font-weight:700;color:var(--ink-2);border:1px solid var(--line-2);
  border-radius:var(--r-ctl);padding:4px 8px;background:var(--wash-2);white-space:nowrap}
#nx-app .pmeta {display:flex;flex-wrap:wrap;gap:5px;margin-top:var(--s2)}
#nx-app .prov {display:inline-flex;align-items:center;height:19px;padding:0 7px;border-radius:var(--r-ctl);border:1px solid;
  font-size:9.5px;font-weight:800;letter-spacing:.11em;text-transform:uppercase;white-space:nowrap}
#nx-app .prov.obs {color:var(--teal-deep);border-color:var(--teal-line);background:var(--teal-wash)}
#nx-app .prov.mod {color:var(--blue-deep);border-color:var(--blue-line);background:var(--blue-wash)}
#nx-app .prov.prv {color:var(--amber);border-color:var(--amber-line);background:var(--amber-wash)}
#nx-app .prov.crw {color:var(--violet);border-color:var(--violet-line);background:var(--violet-wash)}
/* The card's own title said "Cambridge key map" directly under a heading that
   says "Choose neighborhood — on the map or in the list". Two labels for one
   object, ~24px apart, in the column whose scroll budget the picker needs.
   Copy rule 1; the footer still carries the source. */
#nx-app .mapcard {margin:var(--s3) 4px;border:1px solid var(--line);border-radius:var(--r-panel);background:var(--wash-2);padding:5px 10px 4px}
#nx-app .mapcard-hd {display:flex;align-items:baseline;justify-content:space-between;gap:var(--s2);margin-bottom:4px}
#nx-app .mapcard-hd b {font-size:12.5px;font-weight:700}
#nx-app .mapcard-hd span {font-size:11px;color:var(--muted-2)}
/* ONE SVG UNIT IS ONE CSS PIXEL, since 2026-09-10. renderMap() sets the viewBox
   to the measured box, so every size below is now the size it PAINTS. Before
   this, these were expressed in a 1060x760 space that the browser scaled by
   ~0.31, so `font-size:26px` painted at 8.0px and a 1.6 stroke painted at 0.5.
   Both readings looked fine in the stylesheet and neither was true on screen.
   scripts/checks/min-font.mjs measures the rendered box and holds .map-num
   inside a BAND — 11px floor, 20px ceiling — because this file can now get it
   wrong in both directions. */
/* 230px, chosen by sweep rather than by eye. The map is width-constrained
   above ~250px, so height buys click-target size right up to that point and
   nothing after it; 230 is where the smallest neighbourhood reaches 40.6px and
   only two of thirteen miss the 44px target — against three of thirteen in the
   old right-hand pane, where the numerals were also painting at 8.0px. Past
   250 the rail starts to overflow and the targets stop improving. */
#nx-app #cityMap {width:100%;height:230px;display:block}
#nx-app .map-poly {fill:var(--wash);stroke:var(--line-2);stroke-width:0.8;cursor:pointer}
#nx-app .map-poly:hover {fill:var(--panel);stroke:var(--ink-2);stroke-width:1.2}
#nx-app .map-poly:focus-visible {stroke:var(--chrome);stroke-width:1.8;outline:none}
#nx-app .map-poly.is-sel {fill:var(--teal-deep);stroke:var(--teal-deep);stroke-width:1.2}
/* The peer state is the other half of one selection: hovering a rail row lifts
   its polygon, hovering a polygon lifts its row. */
#nx-app .map-poly.is-peer {fill:var(--teal);stroke:var(--teal-deep);stroke-width:1.4}
#nx-app .map-num {font-size:11px;fill:var(--muted-2);text-anchor:middle;font-weight:700;pointer-events:none}
#nx-app .map-num.is-sel {fill:var(--panel)}
#nx-app .map-num.is-peer {fill:var(--teal-deep)}
#nx-app .nrow.is-peer {background:var(--teal-wash);border-color:var(--teal-line)}
#nx-app .mapcard-ft {display:flex;align-items:center;justify-content:space-between;gap:var(--s2);
  border-top:1px solid var(--line);margin-top:6px;padding-top:6px;font-size:11px;color:var(--muted-2)}
#nx-app .selkey {display:inline-flex;align-items:center;gap:6px;color:var(--ink-2);font-weight:700}
#nx-app .selkey i {width:10px;height:10px;background:var(--teal-deep);border:1px solid var(--rim);display:block}
/* THE TABS SCROLL, AND NOW THEY SAY SO  (2026-09-10)
   Measured at 1440: clientWidth 388 against scrollWidth 496 — 108px of tab row
   past the right edge, with `scrollbar-width:none` hiding the one affordance
   that would have said so. "Redevelopment" rendered as "Redevelopme" and
   "Sources" did not render at all, which reads as a clipped panel rather than a
   scrollable one, and sent an earlier round of this plan off to rebalance the
   whole grid to fix a width problem that was never a width problem.
   The scrollbar stays hidden — it is 15px of chrome on a 38px row — and a fade
   on the trailing edge carries the message instead. It is painted only while
   there is something past the edge, so it cannot lie in the other direction. */
#nx-app .tabs {display:flex;gap:2px;padding:0 var(--s4);border-bottom:1px solid var(--line);background:var(--panel);
  position:sticky;top:0;z-index:4;overflow-x:auto;scrollbar-width:none;
  -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 26px),transparent 100%);
  mask-image:linear-gradient(to right,#000 calc(100% - 26px),transparent 100%)}
#nx-app .tabs.is-end {-webkit-mask-image:none;mask-image:none}
#nx-app .tabs::-webkit-scrollbar {display:none}
#nx-app .tab {padding:10px 8px 8px;font-size:12.5px;font-weight:600;color:var(--muted);border-bottom:2px solid transparent;white-space:nowrap}
#nx-app .tab:hover {color:var(--ink-2)}
#nx-app .tab.is-on {color:var(--ink);font-weight:800;border-bottom-color:var(--chrome)}
#nx-app .pinbar {position:sticky;top:38px;z-index:3;display:flex;align-items:center;gap:var(--s2);
  padding:7px 20px;border-bottom:1px solid var(--line);background:var(--wash-2);font-size:11.5px;color:var(--muted)}
#nx-app .pb-state {font-size:9.5px;font-weight:800;letter-spacing:.11em;text-transform:uppercase;
  border:1px solid var(--chrome-line);background:var(--chrome-wash);color:var(--chrome);border-radius:var(--r-ctl);padding:2px 7px}
#nx-app .pinbar.is-preview .pb-state {border-color:var(--amber-line);background:var(--amber-wash);color:var(--amber)}
#nx-app .pb-topic {font-weight:700;color:var(--ink-2)}
#nx-app .pb-map {margin-left:auto;font-size:9.5px;font-weight:800;letter-spacing:.11em;text-transform:uppercase;white-space:nowrap;
  border:1px solid var(--chrome-line);background:var(--chrome-wash);color:var(--chrome);border-radius:var(--r-ctl);padding:2px 7px}
#nx-app .detail {padding:var(--s4) 20px 28px;display:flex;flex-direction:column;gap:var(--s3)}
#nx-app .sect h3 {font-family:'Instrument Serif',serif;font-weight:400;font-size:23px;line-height:1.14;letter-spacing:-.01em;margin-top:2px}
#nx-app .lede {font-size:12.5px;line-height:1.5;color:var(--muted);margin-top:6px}
#nx-app .hero {display:flex;align-items:flex-end;gap:var(--s3);border-bottom:1px solid var(--line);padding-bottom:var(--s3)}
#nx-app .hero .n {font-family:'Instrument Serif',serif;font-size:44px;line-height:.94;letter-spacing:-.015em}
#nx-app .hero .l {font-size:12px;color:var(--muted);padding-bottom:4px;line-height:1.35}
#nx-app .hero .r {margin-left:auto;text-align:right;padding-bottom:4px}
#nx-app .hero .r b {display:block;font-size:15px;font-weight:800}
#nx-app .hero .r span {font-size:11px;color:var(--muted-2)}
#nx-app .mgrid {display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s2)}
#nx-app .mc {border:1px solid var(--line);border-radius:var(--r-panel);background:var(--wash-2);padding:9px 10px;display:flex;flex-direction:column;gap:2px}
#nx-app .mc-k {font-size:11px;color:var(--muted);font-weight:600;line-height:1.25}
#nx-app .mc-v {font-size:19px;font-weight:800;letter-spacing:-.015em;line-height:1.1}
#nx-app .mc-s {font-size:10.5px;color:var(--muted-2);line-height:1.3}
#nx-app .card {border:1px solid var(--line);border-radius:var(--r-panel);background:var(--panel);padding:12px 14px 10px}
#nx-app .card-hd {display:flex;align-items:center;gap:var(--s2);margin-bottom:2px}
#nx-app .card-hd b {font-size:12.5px;font-weight:700}
#nx-app .card-hd .u {font-size:11px;color:var(--muted-2);font-weight:500}
#nx-app .card-hd .prov {margin-left:auto}
#nx-app .src {font-size:10.5px;color:var(--muted-2);line-height:1.4;border-top:1px solid var(--line);margin-top:9px;padding-top:6px}
#nx-app .cnote {font-size:11px;color:var(--muted);line-height:1.4;margin-top:6px}
#nx-app .callout {border:1px solid var(--blue-line);background:var(--blue-wash);border-radius:var(--r-panel);padding:9px 11px}
#nx-app .callout b {font-size:12px;font-weight:800;color:var(--blue-deep);display:block}
#nx-app .callout p {font-size:11.5px;color:var(--ink-2);line-height:1.45;margin-top:2px}
#nx-app .callout.warn {border-color:var(--amber-line);background:var(--amber-wash)}
#nx-app .callout.warn b {color:var(--amber)}
#nx-app .actions {display:flex;gap:var(--s2);flex-wrap:wrap}
#nx-app .btn-primary {display:inline-flex;align-items:center;height:32px;padding:0 12px;background:var(--chrome);
  color:var(--panel);border-radius:var(--r-ctl);font-size:12px;font-weight:700;text-decoration:none}
#nx-app .btn-quiet {display:inline-flex;align-items:center;height:32px;padding:0 12px;border:1px solid var(--line-2);
  border-radius:var(--r-ctl);font-size:12px;font-weight:700;color:var(--ink-2);text-decoration:none;background:var(--panel)}/* ---- charts ---- */
#nx-app .vb-plot {position:relative;height:132px;display:flex;align-items:flex-end;gap:5px;
  border-bottom:1px solid var(--line-2);padding-top:15px;margin-top:6px}
#nx-app .vb-grid {position:absolute;left:0;right:0;top:15px;bottom:0;pointer-events:none}
#nx-app .vb-grid i {position:absolute;left:0;right:0;height:1px;background:var(--line);display:block}
#nx-app .vb-col {flex:1;min-width:0;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;position:relative}
#nx-app .vb-v {font-size:11px;font-weight:700;color:var(--muted);margin-bottom:3px}
#nx-app .vb-v.is-top {color:var(--ink);font-weight:800}
#nx-app .vb-f {width:74%;background:var(--bar);display:block;min-height:1px}
#nx-app .vb-f.is-top {background:var(--bar-hi)}
#nx-app .vb-ax {display:flex;gap:5px;margin-top:5px}
#nx-app .vb-ax span {flex:1;min-width:0;text-align:center;font-size:10.5px;color:var(--muted-2);line-height:1.15}
#nx-app .vb-ax span.is-top {color:var(--ink-2);font-weight:700}
#nx-app .hb-rows {position:relative;margin-top:6px}
#nx-app .hb-grid {position:absolute;left:132px;right:44px;top:0;bottom:0;pointer-events:none}
#nx-app .hb-grid i {position:absolute;top:0;bottom:0;width:1px;background:var(--line);display:block}
#nx-app .hb-row {display:grid;grid-template-columns:124px 1fr 36px;gap:var(--s2);align-items:center;height:24px;position:relative}
#nx-app .hb-l {font-size:11.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#nx-app .hb-t {height:12px;background:var(--bar-track);display:block}
#nx-app .hb-t i {display:block;height:100%;background:var(--bar)}
#nx-app .hb-t i.is-top {background:var(--bar-hi)}
#nx-app .hb-v {text-align:right;font-size:11.5px;font-weight:700}
#nx-app .hb-ax {margin:3px 44px 0 132px;display:flex;justify-content:space-between;font-size:10.5px;color:var(--muted-2)}
#nx-app .dn-wrap {display:flex;align-items:center;gap:var(--s4);margin-top:var(--s2)}
#nx-app .dn {position:relative;width:138px;height:138px;flex:0 0 138px}
#nx-app .dn svg {display:block;width:100%;height:100%}
#nx-app .dseg {fill:none;stroke-width:15}
#nx-app .dn-c {position:absolute;inset:0;display:grid;place-items:center;align-content:center;text-align:center;pointer-events:none}
#nx-app .dn-c b {font-size:21px;font-weight:800;letter-spacing:-.01em;line-height:1}
#nx-app .dn-c i {font-style:normal;font-size:10.5px;color:var(--muted);line-height:1.2;margin-top:3px}
#nx-app .dn-leg {flex:1;min-width:0;display:grid;gap:6px}
#nx-app .lrow {display:grid;grid-template-columns:11px 1fr auto;gap:var(--s2);align-items:center;font-size:11.5px;color:var(--ink-2)}
#nx-app .lrow i {width:11px;height:11px;border:1px solid var(--rim);display:block}
#nx-app .lrow b {font-weight:800}
#nx-app .ss {margin-top:var(--s2)}
#nx-app .ss-dls {position:relative;height:15px}
#nx-app .ss-dls span {position:absolute;transform:translateX(-50%);font-size:11px;font-weight:700;color:var(--ink-2)}
#nx-app .ss-bar {display:flex;height:22px;border:1px solid var(--rim)}
#nx-app .ss-bar i {display:block;height:100%;border-right:1px solid var(--panel)}
#nx-app .ss-bar i:last-child {border-right:0}
#nx-app .ss-leg {display:grid;grid-template-columns:1fr 1fr;gap:5px var(--s4);margin-top:var(--s2)}
#nx-app .kpis {display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s2);margin-top:6px}
#nx-app .kpi {border:1px solid var(--line);border-radius:var(--r-panel);background:var(--wash-2);padding:9px 10px}
#nx-app .kpi b {display:block;font-family:'Instrument Serif',serif;font-weight:400;font-size:27px;line-height:1.02;letter-spacing:-.01em;color:var(--ink)}
#nx-app .kpi span {display:block;font-size:11px;color:var(--muted);margin-top:3px;line-height:1.3}
#nx-app .kpi u {display:block;text-decoration:none;font-size:10.5px;color:var(--muted-2);margin-top:2px}
#nx-app .ics {margin-top:var(--s3)}
#nx-app .ics-mark {position:relative;height:5px}
#nx-app .ics-mark i {position:absolute;top:0;height:3px;background:var(--ink);display:block}
#nx-app .ics-bar {display:flex;height:14px;border:1px solid var(--rim)}
#nx-app .ics-bar i {flex:1;border-right:1px solid var(--panel)}
#nx-app .ics-bar i:last-child {border-right:0}
#nx-app .ics-ax {display:flex;margin-top:4px}
#nx-app .ics-ax span {flex:1;font-size:10.5px;color:var(--muted-2);text-align:center}
#nx-app .ics-ax span.is-on {color:var(--ink);font-weight:800}
#nx-app .toolbar {display:flex;align-items:center;gap:var(--s2);flex-wrap:wrap}
#nx-app .pill {font-size:11px;border:1px solid var(--line-2);border-radius:var(--r-ctl);padding:4px 8px;color:var(--muted);background:var(--wash-2)}
#nx-app .records {display:grid;gap:6px}
#nx-app .rec {border:1px solid var(--line);border-radius:var(--r-panel);background:var(--wash-2);padding:9px 10px;
  display:grid;grid-template-columns:1fr auto;gap:var(--s2);align-items:start}
#nx-app .rec h5 {font-size:12px;font-weight:700}
#nx-app .rec p {font-size:11px;color:var(--muted);line-height:1.45;margin-top:4px}
#nx-app .tag {display:inline-block;margin:0 4px 3px 0;border:1px solid var(--line-2);border-radius:var(--r-ctl);
  padding:2px 6px;font-size:10.5px;font-weight:700;color:var(--ink-2);background:var(--panel)}
#nx-app .tag.live {border-color:var(--teal-line);background:var(--teal-wash);color:var(--teal-deep)}
#nx-app .tag.proj {border-color:var(--violet-line);background:var(--violet-wash);color:var(--violet)}
#nx-app .rec .date {font-size:11px;font-weight:700;color:var(--ink-2);white-space:nowrap;text-align:right}
#nx-app .empty {border:1px dashed var(--line-2);border-radius:var(--r-panel);padding:16px;text-align:center;
  color:var(--muted);font-size:11.5px;line-height:1.5;background:var(--wash-2)}
#nx-app .srcbox {display:grid;gap:9px}
#nx-app .srcrow {display:grid;grid-template-columns:104px 1fr;gap:var(--s2);font-size:11.5px;color:var(--muted);line-height:1.5}
#nx-app .srcrow b {color:var(--ink-2);font-weight:700}/* ---- ONE tooltip instance (research rule 6) ---- */
#nx-app .tip {position:fixed;z-index:99;background:var(--panel);border:1px solid var(--line-2);border-radius:var(--r-ctl);
  padding:6px 9px;box-shadow:var(--shadow);max-width:250px;pointer-events:none}
#nx-app .tip[hidden] {display:none}
#nx-app .tip b {display:block;font-size:12px;font-weight:800;line-height:1.25}
#nx-app .tip span {display:block;font-size:11.5px;color:var(--muted);line-height:1.3;margin-top:1px}/* ---- mobile frames ---- */
#nx-app .mob-row {display:flex;gap:var(--s6);flex-wrap:wrap}
#nx-app .mob {width:390px;border:1px solid var(--line-2);border-radius:var(--r-panel);overflow:hidden;
  background:var(--panel);box-shadow:var(--shadow)}
#nx-app .mob-cap {font-size:10.5px;letter-spacing:.11em;text-transform:uppercase;font-weight:700;color:var(--muted);margin-bottom:6px}
#nx-app .m-hdr {height:46px;display:flex;align-items:center;gap:var(--s2);padding:0 var(--s3);border-bottom:1px solid var(--line)}
#nx-app .m-hdr .mark {font-family:'Instrument Serif',serif;font-size:18px}
#nx-app .m-hdr .bsub {font-size:11px;color:var(--muted)}
#nx-app .m-place {background:var(--bg);padding:var(--s2) 0 var(--s2);display:grid;place-items:center}
#nx-app .m-place svg {display:block;width:268px;height:268px}
#nx-app .m-topics {display:flex;gap:6px;overflow-x:auto;padding:var(--s2) var(--s3);border-top:1px solid var(--line);scrollbar-width:none}
#nx-app .m-topics::-webkit-scrollbar {display:none}
#nx-app .m-topic {flex:0 0 auto;display:flex;flex-direction:column;align-items:center;gap:3px;min-width:76px;min-height:44px;
  border:1px solid var(--line-2);border-radius:var(--r-ctl);padding:6px 8px;background:var(--panel)}
#nx-app .m-topic svg {width:20px;height:20px;display:block}
/* Two lines, whole phrases (2026-09-12). The chip used to print only the first
   half of its label, so a phone reader saw "Rental or" and "Income &". Each half
   is now its own line and never wraps; the chip grows to fit and the strip
   already scrolls sideways. 11px is DESIGN.md's caption floor, up from 9.5px. */
#nx-app .m-topic span {display:block;font-size:11px;font-weight:700;color:var(--ink-2);text-align:center;line-height:1.2;white-space:nowrap}
#nx-app .m-topic.is-on {background:var(--chrome);border-color:var(--chrome)}
#nx-app .m-topic.is-on span {color:var(--panel)}
#nx-app .m-topic.is-on .node-icon {color:var(--panel)}
#nx-app .m-sheet {border-top:1px solid var(--line-2);background:var(--panel);padding:var(--s2) var(--s3) var(--s3)}
#nx-app .m-grab {width:36px;height:4px;border-radius:var(--r-ctl);background:var(--line-2);margin:0 auto 9px;display:block}
#nx-app .m-chips {display:flex;gap:6px;overflow-x:auto;padding-bottom:6px;margin-top:6px;scrollbar-width:thin}
#nx-app .m-chip {flex:0 0 auto;height:30px;padding:0 10px;border:1px solid var(--line-2);border-radius:var(--r-ctl);
  background:var(--panel);font-size:11.5px;font-weight:700;color:var(--ink-2);display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
#nx-app .m-chip i {width:9px;height:9px;border:1px solid var(--rim);display:block}
#nx-app .m-chip.is-on {background:var(--chrome);border-color:var(--chrome);color:var(--panel)}
#nx-app .m-fullhd {display:flex;align-items:center;gap:var(--s2);padding:9px var(--s3);border-bottom:1px solid var(--line)}
#nx-app .m-fullhd .x {width:28px;height:28px;border:1px solid var(--line-2);border-radius:var(--r-ctl);
  display:grid;place-items:center;color:var(--muted);flex:0 0 28px}
#nx-app .m-fullhd b {font-family:'Instrument Serif',serif;font-size:20px;font-weight:400;line-height:1.1}
#nx-app .m-full {height:620px;overflow-y:auto;scrollbar-width:thin}
#nx-app .m-full .detail {padding:var(--s3) var(--s3) var(--s5)}
#nx-app .m-full .mgrid {grid-template-columns:repeat(2,1fr)}
#nx-app .m-full .kpis {grid-template-columns:1fr}
#nx-app .m-full .dn-wrap {flex-direction:column;align-items:stretch}
#nx-app .m-full .dn {margin:0 auto}
#nx-app .m-full .hb-grid {left:104px;right:44px}
#nx-app .m-full .hb-row {grid-template-columns:96px 1fr 36px}
#nx-app .m-full .hb-ax {margin-left:104px}
#nx-app .m-full .ss-leg {grid-template-columns:1fr}/* ================================================================
   FUNCTIONAL BUILD — additions only. Every value below is a var()
   reference to the one token block above. No new colour is defined.
   ================================================================ *//* a fifth provenance state: the data is NOT HELD. It is never a value. */
#nx-app .prov.gap {color:var(--risk);border-color:var(--risk-line);background:var(--risk-wash)}
#nx-app .nchip-bg.gap {fill:var(--risk-wash);stroke:var(--risk-line)}
#nx-app .nchip.gap {fill:var(--risk)}
#nx-app .card.is-gap {border-left:3px solid var(--risk)}
#nx-app .card.is-gap .card-hd b {color:var(--risk)}
#nx-app .gapline {font-size:11.5px;color:var(--ink-2);line-height:1.5;margin-top:4px}
#nx-app .gapline b {color:var(--risk);font-weight:800}
#nx-app .nodata {font-size:11.5px;font-weight:700;color:var(--risk)}/* citywide scope selector in the rail */
#nx-app .cityrow {width:100%;display:grid;grid-template-columns:24px 1fr;align-items:center;gap:var(--s2);
  padding:7px 8px 7px 6px;border:1px solid var(--line-2);border-left:2px solid var(--line-2);
  border-radius:var(--r-ctl);text-align:left;background:var(--wash-2);margin:0 4px var(--s2)}
#nx-app .cityrow:hover {border-color:var(--ink-2)}
#nx-app .cityrow.is-on {background:var(--ink-2);border-color:var(--ink-2);border-left-color:var(--ink)}
#nx-app .cityrow .nnum {font-size:9.5px}
#nx-app .cityrow.is-on .nnum {background:var(--panel);border-color:var(--panel);color:var(--ink)}
#nx-app .cityrow .cr-t {font-size:12px;font-weight:800;line-height:1.2;color:var(--ink)}
#nx-app .cityrow .cr-s {display:block;font-size:10.5px;font-weight:600;color:var(--muted)}
#nx-app .cityrow.is-on .cr-t, #nx-app .cityrow.is-on .cr-s {color:var(--panel)}
#nx-app .nlist.is-dim .nrow {opacity:.62}/* citywide banner — the single marker that says "this is not one neighborhood" */
#nx-app .scopebar {display:flex;align-items:center;gap:var(--s2);padding:7px 20px;background:var(--ink-2);color:var(--panel);
  font-size:11px;font-weight:700;letter-spacing:.02em}
#nx-app .scopebar b {font-weight:800}
#nx-app .scopebar span {margin-left:auto;font-weight:600;color:var(--line-2);font-size:10.5px}
#nx-app .scopebar[hidden] {display:none}/* ---- the live map at the orbit centre ---- */
#nx-app .graph-wrap {position:relative}
#nx-app .disc-shell {position:absolute;border-radius:26px;overflow:hidden;background:var(--map-void);
  border:1px solid var(--line-2);z-index:2}
#nx-app .disc-map {position:absolute;inset:0}/* Mapbox controls inside a CIRCULAR clip.
   The logo CANNOT be removed — Mapbox's terms require the wordmark on a rendered
   map, and `attributionControl:false` (set in initMap) only drops the attribution
   TEXT, which we carry in the caption under the orbit instead. So the logo is
   made quiet and moved out of the way, not hidden.
   It used to sit at bottom-left, 31% up — floating in the middle of the map like
   a watermark, because that was as high as it had to be to clear the old title
   plate. The plate is gone now (the name moved outside the circle), so the logo
   drops to the bottom arc and reads as a caption. At 6% up the visible chord is
   ~194px, comfortably wider than the ~85px mark. */
#nx-app .disc-shell .mapboxgl-ctrl-bottom-left {left:50%;bottom:5%;transform:translateX(-50%) scale(.82);
  transform-origin:bottom center}
#nx-app .disc-shell .mapboxgl-ctrl-bottom-right {right:12%;bottom:5%;transform:scale(.82);transform-origin:bottom right}
#nx-app .disc-shell .mapboxgl-ctrl-top-right {top:9%;right:11%}
#nx-app .disc-shell .mapboxgl-ctrl-logo {opacity:.45}
#nx-app .disc-shell .mapboxgl-ctrl-logo:hover, #nx-app .disc-shell .mapboxgl-ctrl-logo:focus-visible {opacity:.9}/* stays reachable */
#nx-app .disc-plate {position:absolute;left:0;right:0;bottom:0;height:29%;background:var(--panel);
  border-top:1px solid var(--line-2);display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:1px;pointer-events:none;padding:0 8%}
#nx-app .disc-plate .pe {font-size:10px;letter-spacing:.13em;font-weight:800;color:var(--muted);text-transform:uppercase}
#nx-app .disc-plate .pn {font-family:'Instrument Serif',serif;font-size:22px;line-height:1.06;color:var(--ink);text-align:center}
#nx-app .disc-chip {position:absolute;top:5%;left:50%;transform:translateX(-50%);z-index:3;
  font-size:10px;letter-spacing:.13em;font-weight:800;color:var(--ink-2);background:var(--panel);
  border:1px solid var(--line-2);border-radius:var(--r-ctl);padding:3px 9px;pointer-events:none;white-space:nowrap}
#nx-app .disc-fallback {position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:6px;padding:12% 12% 32%;text-align:center;background:var(--wash-2)}
#nx-app .disc-fallback svg {width:100%;height:auto;max-height:56%}
#nx-app .disc-fallback p {font-size:10.5px;color:var(--muted);line-height:1.35}
#nx-app .disc-fallback p b {color:var(--amber);font-weight:800;display:block}
#nx-app .attrib {font-size:10.5px;color:var(--muted-2)}/* ---- record extensions ---- */
#nx-app .rec.wide {grid-template-columns:1fr}
#nx-app .units {display:inline-flex;align-items:baseline;gap:5px;font-size:11.5px;font-weight:800;color:var(--ink-2);
  border:1px solid var(--line-2);border-radius:var(--r-ctl);background:var(--panel);padding:2px 7px;margin:0 4px 3px 0}
#nx-app .units i {font-style:normal;color:var(--muted-2);font-weight:600}
#nx-app .units.up {border-color:var(--teal-line);background:var(--teal-wash);color:var(--teal-deep)}
#nx-app .units.down {border-color:var(--risk-line);background:var(--risk-wash);color:var(--risk)}
#nx-app .recfoot {font-size:10.5px;color:var(--muted-2);line-height:1.4;margin-top:5px;border-top:1px solid var(--line);padding-top:5px}/* ---- compact data tables (citywide roll-ups) ---- */
#nx-app .tbl {width:100%;border-collapse:collapse;font-size:11.5px;margin-top:6px}
#nx-app .tbl th {text-align:left;font-size:10px;letter-spacing:.11em;text-transform:uppercase;color:var(--muted);
  font-weight:800;padding:0 0 5px;border-bottom:1px solid var(--line-2)}
#nx-app .tbl th.r, #nx-app .tbl td.r {text-align:right}
#nx-app .tbl td {padding:5px 0;border-bottom:1px solid var(--line);color:var(--ink-2)}
#nx-app .tbl tr:last-child td {border-bottom:0}
#nx-app .tbl td.r {font-weight:700}
#nx-app .tbl tr.is-sel td {color:var(--ink);font-weight:800}
#nx-app .tbl td button {font-size:11.5px;color:var(--blue-deep);font-weight:700;text-align:left}
#nx-app .tbl td button:hover {text-decoration:underline}
#nx-app .tbl tfoot td {border-top:1px solid var(--line-2);border-bottom:0;font-weight:800;color:var(--ink);padding-top:6px}/* loading + failure states — never a blank box */
#nx-app .boot {position:absolute;inset:0;display:grid;place-items:center;background:var(--bg);z-index:9;text-align:center;padding:20px}
#nx-app .boot b {display:block;font-family:'Instrument Serif',serif;font-size:22px;font-weight:400}
#nx-app .boot span {display:block;font-size:12px;color:var(--muted);margin-top:4px}
#nx-app .boot.is-err b {color:var(--risk)}/* ---- Rocker 22:47: "the title of this should not be in that circle, but the
   title should probably be on top somewhere outside of the circle. So that we
   see: now we're at Cambridge Highland." ----
   The plate also ate the bottom 29% of the disc, so moving the name out buys
   back map area on the same change she asked for the map to be bigger. */
#nx-app .disc-plate {display:none}
#nx-app .ctx {gap:2px}
#nx-app .ctx b {font-family:'Instrument Serif',serif;font-size:29px;line-height:1.05;color:var(--ink);
  letter-spacing:-.01em}
#nx-app .ctx .eyebrow {font-size:10px}/* ================================================================
   APP SHELL — promoted from a 1440x900 mock frame to a real page.
   Everything above this comment is the design system as authored.
   Nothing above is edited; these rules only re-host it.
   ================================================================ *//* The spec pinned this to `height:900px` because it sat inside a fixed frame.
   As a page it takes the viewport. dvh (not vh) so mobile browser chrome
   collapsing does not leave a dead strip under the rail. *//* Rail is names + search ONLY now — Rocker 18:57, "we don't need any text there,
   we only need the neighborhood" — so it gives its width to the circle, which is
   literally what she asked for: "enlarging the entire circle, taking it from
   where it is all the way to the left side." */
#nx-app .app {
  height: 100dvh;
  width: 100%;
  grid-template-columns: minmax(300px, 330px) minmax(0, 1fr) minmax(340px, 26vw);
}/* Grid children must be allowed to shrink or their own overflow never engages. */
#nx-app .app > .rail, #nx-app .app > .centre, #nx-app .app > .right { min-height: 0; }
#nx-app .rail { overflow-y: auto; }

@media (max-width: 1240px) {
#nx-app .app { grid-template-columns: 288px minmax(0, 1fr) 330px; }
/* The rail is 288px here, so the key map draws 241px wide — and Cambridge is a
   wide city: at that width the drawing is 165px tall and a 230px box letterboxes
   65px of nothing. Those pixels were pushing the neighbourhood list below the
   fold, which is the one control on this tab that must never go there. Trimming
   the box to the height the drawing actually uses costs the map nothing.

   160 rather than 177 because 177 still left the first row TWO PIXELS below an
   800px window — measured, not guessed, by viewport-fit.mjs at 1100×800. Two
   pixels is enough: a picker the reader has to scroll to discover is not a
   picker. The drawing is width-constrained at this tier, so the 17px comes out
   of letterboxing again and the map does not get smaller. */
#nx-app #cityMap { height: 160px; }
/* The instruction goes at this tier too. It earns its line beside a 333px map
   at 1440; beside a 241px one, next to a list that is visibly a list, it is
   costing the first row its place on screen. */
#nx-app .rail-hd-how { display: none; }
#nx-app .steps { display: none; }            /* the 1-2-3 hint is the first thing to go */
}
@media (max-width: 1040px) {
#nx-app .app { grid-template-columns: 178px minmax(0, 1fr) 320px; }
#nx-app .tnote { display: none; }
/* THE KEY MAP STANDS DOWN AT THIS TIER. The rail is 178px here, which leaves
   the map ~131px of drawing: measured, the smallest neighbourhood is 15.9px
   across and all thirteen miss the 44px target. A map nobody can hit is not a
   smaller map, it is a decoration occupying the space the list needs — and the
   list is the accessible path to all thirteen places anyway. It also reclaims
   the 70px of rail overflow that was pushing the first row below the fold. */
#nx-app .mapcard { display: none; }
#nx-app .rail-hd-how { display: none; }
}/* ---- the small-screen contract, as a breakpoint rather than a mock-up ----
   The spec drew this as two 390px phone frames side by side. Same markup,
   same ids, same components — now it is simply what the page IS below 900px. */
#nx-app .mobile-app { display: none; }
@media (max-width: 900px) {
#nx-app .app { display: none; }
#nx-app .mobile-app {
    display: flex; flex-direction: column;
    height: 100dvh; background: var(--panel); overflow: hidden;
  }/* neutralise the phone-bezel styling the mock needed */
#nx-app .mobile-app .mob {
    width: 100%; border: 0; border-radius: 0; box-shadow: none;
    display: flex; flex-direction: column; flex: 1; min-height: 0;
  }
#nx-app .mobile-app .m-topics { flex: 0 0 auto; }
#nx-app .mobile-app .m-sheet { flex: 1 1 auto; overflow-y: auto; min-height: 0; }
#nx-app .mobile-app .m-place { flex: 0 0 auto; }/* Detail is promoted to a sheet OVER the place view, which is what the
     spec's "Fig. 3" was describing. It is hidden until a topic is pinned. */
#nx-app .m-detail {
    position: fixed; inset: 0; z-index: 40; background: var(--panel);
    display: flex; flex-direction: column;
    transform: translateY(100%); transition: transform .22s ease;
  }
#nx-app .m-detail.is-open { transform: translateY(0); }
#nx-app .m-detail .m-full { flex: 1; overflow-y: auto; min-height: 0; }
}
@media (prefers-reduced-motion: reduce) {
#nx-app .m-detail { transition: none; } }/* The boot overlay covered a framed disc; as a page it covers the viewport. */
#nx-app .boot { z-index: 30; }/* ================================================================
   THE APP HEADER — interim fix, 2026-08-16
   ----------------------------------------------------------------
   THE BUG. "Neighborhood analytics" in cambridge.html is a
   <span data-view> that setView() swaps in place. "Neighborhood
   Explorer" was an <a href> to THIS FILE — a standalone document
   with no app-header markup in it at all. So clicking it did not
   hide the header; it left the application, and the only route back
   was the browser Back button. The client reported that as "the
   header disappears", which is the honest description of what she
   saw. docs/build-plan-explorer-merge.md records the same
   disorientation from the 08-03 meeting.

   THE REAL FIX is the one that plan already specifies: port the
   orbit into cambridge.js as a real tab (Rocker 1:44:12, "this
   should be in the same file ... and just another tab"). That is a
   large job. THIS is the interim: give the standalone page the same
   header, so the six sections stay reachable from here and the
   application still looks like one product.

   THE DUPLICATION IS DELIBERATE AND TEMPORARY. These rules restate
   cambridge.css:1151-1190 (.tabs360 / .t360 / .hdr360 / .btn-donate)
   rather than linking that stylesheet, because cambridge.css is
   157KB of shell rules written against cambridge.html's DOM and
   would land on this page's own .rail / .toolbar / .brand class
   names. When the port lands, this block and its markup delete
   together. Keep the two in step until then.
   ================================================================ */
#nx-app .hdr360 {
  flex: 0 0 58px; height: 58px; z-index: 220;
  background: var(--panel); border-bottom: 1px solid var(--line);
  display: flex; align-items: center; gap: 14px; padding: 0 18px;
}/* .app / .mobile-app took the viewport with height:100dvh. As flex children
   under the header they take what is left of it instead, so the header can
   never overlap the orbit and the grid never overflows the page. */
#nx-app .app, #nx-app .mobile-app { height: auto; flex: 1 1 auto; min-height: 0; }
/* THE BRAND LOCKUP RULES THAT LIVED HERE ARE GONE  (2026-09-10)
   `#nx-app .hdr-brand`, `.rail-orbit`, `.rail-wm` and `.rail-prod` were ported
   from prototypes/neighborhood-explorer.html and prefixed with #nx-app by
   scripts/scope-explorer-css.py along with the other 444 selectors. They have
   never matched an element: the page's only .hdr-brand is the site header at
   cambridge.html:81, which is OUTSIDE #nx-app, and the prototype they came
   from is self-contained and loads no stylesheet.

   Dead is the least of it. The mark rule pointed at the retired 2026-08-16
   artwork via a `../` path, under the comment "the path is ../ because this
   page sits one directory down" — true of the prototype it was copied from,
   false of this file, which sits in the repo root. A CSS url() resolves
   against the STYLESHEET, so that path pointed above the site root. And
   .rail-prod carried a second copy of the 9.6px caption defect, at a selector
   no audit of the header would ever have found.

   Deleted rather than re-pointed: a rule that cannot match is not a rule, and
   keeping a corrected copy of it here is how the wrong path comes back.
   scripts/checks/brand.mjs scans this file for retired mark filenames. */
#nx-app .hdr360 .toolbar-right { display: flex; align-items: center; gap: 8px;
  flex: 0 0 auto; margin-left: auto; }
#nx-app .tabs360 { display: inline-flex; gap: 4px; margin-right: 10px; }
#nx-app .t360 { padding: 6px 4px 8px; font-size: 0.8rem; font-weight: 500; color: var(--muted);
  position: relative; text-decoration: none; }
#nx-app .t360-link:hover { color: var(--ink); }
#nx-app .t360.on { color: var(--ink); font-weight: 700; }
#nx-app .t360.on::after { content: ''; position: absolute; left: 4px; right: 4px; bottom: 0;
  height: 2px; border-radius: var(--r-ctl); background: var(--chrome); }
#nx-app .t360.off { color: var(--line-2); cursor: help; }
#nx-app .t360:focus-visible { outline: 2px solid var(--chrome); outline-offset: 2px;
  border-radius: var(--r-ctl); }
#nx-app .btn-news { padding: 7px 16px; font-size: 0.78rem; font-weight: 600;
  background: var(--bg); color: var(--ink); border: 1px solid var(--line);
  border-radius: 100px; font-family: 'Manrope', sans-serif; }
#nx-app .btn-news:hover { background: var(--chrome); color: var(--panel); border-color: var(--chrome); }/* r4-D7: Donate is a secondary outline. Burnt Orange is reserved for alerts
   and must never carry a primary CTA. */
#nx-app .btn-donate { padding: 8px 20px; font-size: 0.78rem; font-weight: 600;
  background: none; border: 1px solid var(--line-2); color: var(--ink);
  border-radius: 100px; font-family: 'Manrope', sans-serif; }
#nx-app .btn-donate:hover { background: var(--bg); }/* The view title bar loses its own wordmark now that the product name sits
   58px above it — the same duplicate-lockup fix cambridge.html made
   2026-08-15. With the 21px serif gone, 56px of row is no longer needed and
   the 12px goes back to the orbit, which is height-driven. */
#nx-app .app { grid-template-rows: 1fr 30px; }/* The header's own note strip, shown when a control leads nowhere yet. */
#nx-app .hdr-note {
  position: fixed; left: 50%; top: 68px; transform: translateX(-50%);
  z-index: 240; max-width: min(520px, calc(100vw - 32px));
  background: var(--ink); color: var(--panel); font-size: 12px; line-height: 1.4;
  padding: 9px 14px; border-radius: var(--r-ctl); box-shadow: var(--shadow);
}
#nx-app .hdr-note[hidden] { display: none; }

@media (max-width: 1240px) {
#nx-app .hdr360 .rail-prod { display: none; } }
@media (max-width: 900px)  {
#nx-app .hdr360 .hdr-brand > div:last-child { display: none; } }

/* ═══════════════════════════════════════════════════════════════════════════
   HOST INTEGRATION — the height context html/body used to provide.
   ═══════════════════════════════════════════════════════════════════════════
   The prototype was a whole document: `html,body{height:100%}` plus
   `body{overflow:hidden}` gave `.app` something to be 100% of, and its grid
   row `1fr` then sized the orbit. Those two rules are dropped by the scoper
   (they would have frozen this page's scroll), so the container has to supply
   the same context or the 830-unit orbit expands to its natural size and falls
   off the bottom of the tab.

   Not 100dvh: this sits below the site header, the first-visit strip and the
   tab's own heading. The subtraction is that chrome, and min-height is the
   floor at which the three panes still read. Both are deliberate numbers, not
   magic ones — change them together with .dash-head. */
#nx-app {
  display: block;
  height: calc(100dvh - 210px);
  min-height: 620px;
  container-type: inline-size;
}
#nx-app .app { height: 100%; }

/* The phone layout the prototype already shipped, which this application does
   not otherwise have anywhere. Same subtraction. */
@media (max-width: 900px) {
  #nx-app { height: auto; min-height: 0; }
  #nx-app .mobile-app { height: calc(100dvh - 180px); min-height: 520px; }
}

/* The variant-D console, kept and folded. See cambridge.html. */

/* The rail settled at 164px in the prototype's own later rule, which is too
   narrow for "Choose neighborhood" and "13 CDD areas" to sit on one baseline —
   they overlap. Stacked, they fit and read in the same order. */
#nx-app .rail-hd {
  flex-direction: column; align-items: flex-start; gap: 1px;
}

/* "Not held" — the tag for a question this dashboard cannot answer, as opposed
   to one it answers badly. The reviewer who ran the 2026-08-31 protocol called
   these the smartest thing on the site and the reason he trusted it; the limits
   card in the Permits tab is one of them, and it needs a tag of its own rather
   than borrowing the observed/modelled ones. Amber, and it carries a word —
   colour is never the only carrier. */
#nx-app .prov.nope {
  color: var(--amber); background: var(--amber-wash); border-color: var(--amber-line);
}
#nx-app .card.lim { border-left: 3px solid var(--amber); }

/* The orbit's layer chips + their live "Showing …" line. Scoped like everything
   else under #nx-app. These are the only key this tab has: #theme-legend lives
   inside .content-row and is display:none under `view-full`. */
#nx-app .nx-layers { display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
  margin-right: auto; }
#nx-app .nx-layers-k { font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  font-weight: 700; color: var(--muted); margin-right: 2px; }
#nx-app .nx-lay { font-size: 11.5px; font-weight: 700; padding: 4px 9px;
  border: 1px solid var(--line); border-radius: 999px; background: var(--panel);
  color: var(--ink-2); cursor: pointer; line-height: 1.4; }
#nx-app .nx-lay:hover { border-color: var(--chrome); color: var(--chrome); }
#nx-app .nx-lay.is-on { background: var(--chrome); border-color: var(--chrome); color: var(--panel); }
#nx-app .nx-lay:focus-visible { outline: 2px solid var(--chrome); outline-offset: 2px; }
#nx-app .nx-layers-now { font-size: 11px; color: var(--muted); margin-left: 4px; }

/* ═══════════════════════════════════════════════════════════════════════════
   THE CENTRE MAP — sized by its disc, expandable to the whole pane
   ═══════════════════════════════════════════════════════════════════════════
   BUG, 2026-09-05. The disc showed the WRONG NEIGHBOURHOOD: West Cambridge
   selected, "NORTH CAMBRIDGE" in the map; The Port selected, "WELLINGTON-" in
   the map — always the neighbour to the north. Measured on production:
   #discMap 229×171, the #map inside it 229×480. cambridge.css:636 gives #map
   `min-height: 480px` for the Zoning canvas, and inside a 171px disc min-height
   wins, so fitBounds() fit the neighbourhood into a viewport three-quarters of
   which was clipped away. The visible strip was the top of it. */
#nx-app #discMap #map {
  position: absolute; inset: 0; width: 100%; height: 100%;
  min-height: 0; border-radius: 0; box-shadow: none;
}

/* EXPAND. Rocker, 2026-08-03: "The map needs to be at least two to three
   times bigger than what currently it is." The ring geometry caps the resting
   disc (nothing may reach past 292 units from the centre or it lands on the
   diagonal nodes), so the answer is a second state: double-click the map, or
   the corner button, and it takes the whole centre pane — the way the Zoning
   map does. Escape or the same button brings the orbit back. */
#nx-app.is-expanded #graph { opacity: .08; pointer-events: none; transition: opacity .18s ease; }
#nx-app.is-expanded .disc-shell { border-radius: 14px !important; z-index: 3; }
#nx-app.is-expanded .disc-plate {
  height: auto; padding: 6px 14px; flex-direction: row; gap: 10px;
  justify-content: flex-start; border-top: 0; border-bottom: 1px solid var(--line-2);
  top: 0; bottom: auto; background: rgba(255,255,255,.92);
}
#nx-app.is-expanded .disc-plate .pn { font-size: 16px; }
#nx-app.is-expanded .disc-chip { top: auto; bottom: 10px; left: 14px; transform: none; }
#nx-app .disc-expand {
  position: absolute; right: 8px; bottom: 8px; z-index: 4;
  width: 32px; height: 32px; border-radius: 8px;
  border: 1px solid var(--line-2); background: var(--panel); color: var(--ink-2);
  display: grid; place-items: center; cursor: pointer; font-size: 15px; line-height: 1;
  box-shadow: 0 1px 3px rgba(0,0,0,.12);
}
#nx-app .disc-expand:hover { color: var(--chrome); border-color: var(--chrome); }
#nx-app .disc-expand:focus-visible { outline: 2px solid var(--chrome); outline-offset: 2px; }
#nx-app.is-expanded .disc-expand { right: 12px; bottom: 12px; }

/* ── LEGIBILITY ─────────────────────────────────────────────────────────────
   Node labels were 13 SVG units in an 830-unit viewBox — about 8px on screen
   at the size the orbit actually renders. DESIGN.md's floor is 11px for
   captions and 14px for body. Sized so they clear 11px at the smallest
   rendered scale; renderGraph spaces the three lines to match. */
#nx-app .nlabel { font-size: 19px; }
#nx-app .nvalue { font-size: 16px; }

/* THE RAIL TOOK THE CENTRE'S DEAD SPACE  (2026-09-10)
   The rail settled at 164px in an earlier round, which wrapped
   "Wellington-Harrington" onto two lines, then at 204px, which fitted the
   names. Today the key map moved in beside the list, and 204px is not a width
   a thirteen-polygon map of Cambridge can be read at: measured, the smallest
   neighbourhood renders 11.9px across and all thirteen miss the 44px target.

   The width comes from the CENTRE, not from the right pane, and it is free.
   The orbit is a SQUARE driven off height — 558px inside an 807px column at
   1440 — so roughly 249px of that column is letterboxing either side of the
   drawing. Measured across a sweep, the orbit does not begin to shrink until
   the rail passes ~420px. The right pane keeps its width because it needs it:
   its six tabs already scroll 108px with the scrollbar deliberately hidden. */
@media (min-width: 1241px) {
  #nx-app .app { grid-template-columns: minmax(360px, 380px) minmax(0, 1fr) minmax(352px, 27vw); }
}

/* THE KEY MAP GIVES WAY TO THE PICKER ON SHORT WINDOWS  (2026-09-13)
   viewport-fit.mjs at two of the launch plan's viewports, before this rule:
     1536×864  the rail hides 427px in its scrollers, over the 400px budget
     1280×620  the first row starts at y=644 in a 620px window; 0 of 13 rows
   The rail's height is the window's: #nx-app is calc(100dvh - 210px) with a
   620px floor, and the rail is that minus the 30px independence strip, so
   rail height = max(590, window − 240). Above 1240px everything else in the
   rail is fixed, and three windows (950, 900, 864) measure the same line:
     hidden = 821px + map height − rail height
   where 821 is the furniture that does not move (12 padding, 60 head, 63 map
   card chrome, 60 All Cambridge, 27 search, 248 list floor, 118 place and
   provenance, 8 padding) plus the 225px the list scrolls at its floor.
   Holding hidden at 380 (20px inside the budget) gives
     map height ≤ rail height − 441 = 100dvh − 681px,
   so the map keeps its full 230px from a 911px window up and shrinks below.
   Under an 821px window it would be under 140px, where the smallest
   neighbourhoods are far below the 44px target, and at 620 even no map would
   be needed to reach four rows. There it stands down, as it already does at
   ≤1040 and for the same reason: the list is the accessible path to all
   thirteen places. The 1041–1240 tier's furniture puts four rows on screen
   only from about a 750px window, so it stands down under that too.
   If the rail's furniture changes, re-measure: viewport-fit.mjs will say. */
@media (min-width: 1241px) {
  #nx-app #cityMap { height: min(230px, calc(100vh - 681px)); height: min(230px, calc(100dvh - 681px)); }
}
@media (min-width: 1241px) and (max-height: 820px), (min-width: 1041px) and (max-width: 1240px) and (max-height: 749px) {
  #nx-app .mapcard { display: none; }
}
#nx-app .nlist { overflow-x: hidden; }
#nx-app .nrow { min-width: 0; }

/* Three numbered instructions in the top bar — "1 Select a neighborhood ·
   2 Pan and zoom · 3 Hover a topic" — are chrome that explains chrome. If the
   controls need a manual they have failed; they moved behind the ? button. */
#nx-app .steps { display: none; }

/* The header row above the orbit carried the selected place, five layer
   chips, the 2D/3D switch and the legend on one line, and the place name
   wrapped to two. Let the row wrap; the name keeps its line. */
#nx-app .centre-top { flex-wrap: wrap; row-gap: 8px; }
#nx-app .centre-top .ctx { min-width: 200px; }

/* ================================================================
   WHAT THE PINNED TOPIC PAINTS + THE MAP KEY  (2026-09-10)
   ================================================================
   Two docked strips, both OUTSIDE #graph. That placement is the whole design:
   the orbit is rebuilt wholesale on every pin and is pointer-events:none while
   the map is expanded, so anything anchored to a node dies on contact with the
   interaction it exists to serve. Docked, these survive both.
   ================================================================ */
#nx-app .nx-paints { display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  margin-top:5px; padding:4px 8px; border:1px solid var(--line);
  border-radius:var(--r-ctl); background:var(--wash-2); }
#nx-app .nx-paints[hidden] { display:none; }
#nx-app .nxp-k { font-size:11.5px; color:var(--muted); }
#nx-app .nxp-v { font-size:12.5px; font-weight:800; color:var(--ink); }
#nx-app .nxp-modes { display:flex; gap:3px; flex-wrap:wrap; }
/* 44px is the touch target in DESIGN.md; these are real controls, not chips. */
#nx-app .nxp-mode { font-size:11px; font-weight:700; min-height:26px; padding:4px 8px;
  border:1px solid var(--line-2); border-radius:var(--r-ctl); background:var(--panel);
  color:var(--muted); cursor:pointer; }
#nx-app .nxp-mode:hover { border-color:var(--chrome); color:var(--chrome); }
#nx-app .nxp-mode.is-on { background:var(--chrome); border-color:var(--chrome); color:var(--panel); }
#nx-app .nxp-mode:focus-visible { outline:2px solid var(--chrome); outline-offset:2px; }

/* The fifth provenance kind, added today. The DOM chips had words for
   observed, modeled, private and crowdsourced, and no word for "we did this
   arithmetic ourselves from published counts" — which is exactly what a
   density is. Calling it observed would have been a lie about who did the
   work, and the whole point of painting proxies is that they say so. */
#nx-app .prov.der { color:var(--blue-deep); border-color:var(--blue-line);
  background:var(--blue-wash); }

/* COMPACT ON PURPOSE. This strip and the disclosure line above it cost the
   orbit 77px of height when they first landed — the orbit is a square driven
   off the centre column's remaining height, so every pixel here comes straight
   off the drawing. One row per surface, laid out across rather than down, and
   the caveat folds into the same flow instead of claiming its own block. */
#nx-app .nx-legend { margin:6px 0 0; padding:5px 9px; border:1px solid var(--line);
  border-radius:var(--r-panel); background:var(--panel); display:flex;
  flex-direction:column; gap:2px; }
#nx-app .nx-legend[hidden] { display:none; }
#nx-app .nxl-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  font-size:11.5px; line-height:1.5; color:var(--ink-2); }
#nx-app .nxl-sw { display:inline-flex; border-radius:3px; overflow:hidden;
  border:1px solid var(--line-2); flex:0 0 auto; }
#nx-app .nxl-sw i { width:17px; height:11px; display:block; }
#nx-app .nxl-dock { width:56px; height:13px; border-radius:3px;
  background:linear-gradient(90deg, var(--wash), var(--chrome)); }
#nx-app .nxl-t { display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; }
#nx-app .nxl-topic { font-size:11px; color:var(--muted); }
#nx-app .nxl-ck { font-size:11px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
#nx-app .nxl-choose { gap:6px; }
#nx-app .nxl-t b { font-size:12px; font-weight:800; color:var(--ink); }
#nx-app .nxl-u { font-size:11px; color:var(--muted); }
#nx-app .nxl-b { font-size:11px; color:var(--muted); font-variant-numeric:tabular-nums; }
#nx-app .nxl-who { margin-left:auto; font-size:11px; font-weight:700; color:var(--muted-2); }
#nx-app .nxl-gap .nxl-t b { font-weight:700; color:var(--muted); }
/* The caveat is one sentence on the figure — DESIGN.md copy rule 9. */
#nx-app .nxl-note { margin:0; font-size:11px; line-height:1.4; color:var(--muted); }
