/* ============================================================================
   CAMBRIDGE 360 — THE ONE TOKEN BLOCK   (design substrate, 2026-08-03)
   ----------------------------------------------------------------------------
   B6 fix. Before this, cambridge.css inherited styles.css's warm-cream Atlanta
   tokens (--ink #1a1714, --bg #f6f4f0, --accent #c0532c …) AND hard-coded ~105
   civic literals of its own. Two neutrals, two greens, two accents, ad-hoc
   radii — the root cause of "not cohesive".
   Every value below is sourced from a literal that was ALREADY in this file
   (the civic grammar of DESIGN.md); nothing is invented and none of the warm
   Atlanta values survive. Nothing below this block may contain a raw hex.
   The only permitted exceptions are DATA colours — a swatch that must match a
   published City map — and each one carries a comment saying so.
   ========================================================================== */
:root {
  /* ── NEUTRAL RAMP · one family, slate. Replaces both the warm Atlanta
     neutrals and the six near-duplicate greys this file used to carry. ── */
  --n-000: #ffffff;
  --n-025: #f7f9fb;
  --n-050: #f4f6f9;   /* page background (DESIGN.md bg) */
  --n-100: #eef2f6;   /* wash / inset surfaces */
  --n-150: #e3e8ee;   /* hairlines (DESIGN.md panel border) */
  --n-200: #cfd8e0;
  --n-250: #c6cdd5;
  --n-300: #b3bcc5;
  --n-400: #97a4b1;
  --n-500: #8593a2;
  --n-550: #7a8794;
  --n-600: #586b7d;   /* secondary text (DESIGN.md muted) */
  --n-700: #46596b;
  --n-800: #3d4854;
  --n-850: #26313b;
  --n-900: #1e2732;   /* primary text (DESIGN.md ink) */
  --n-1000: #000000;
  --ink-rgb: 30, 39, 50;      /* --n-900, for alpha compositing */
  --n-500-rgb: 133, 147, 162;
  --white-rgb: 255, 255, 255;

  /* ── TEAL · WAS the accent until 2026-08-16. NO LONGER CHROME.
     What still reads these tokens is only (a) semantic verdicts — pass /
     eligible / low-risk / approved — and (b) the Updates + Finding chart
     series. Neither is interface furniture, and both are a separate decision.
     Now that chrome has left teal, the verdicts are the only green in the
     shell and read better for it.
     Teal also remains the selection colour ON THE MAP CANVAS
     (VARIABLE_COLORS.selection, cambridge.js:183). Selection is blue in the
     DOM and teal on the canvas on purpose — see THE FORM LAW in the chrome
     block below. ── */
  --c-teal: #2b8577;
  --c-teal-deep: #1f5b52;
  --c-teal-wash: #e9f2f0;
  --teal-rgb: 43, 133, 119;
  --teal-deep-rgb: 31, 91, 82;
  --c-green-ok: #2f7a54;

  /* ── BLUE · civic information. Also the single income family (see
     VARIABLE_COLORS in cambridge.js — the JS ramp is these same steps). ── */
  --c-blue: #2f6db0;
  --c-blue-bright: #2f8fc0;
  --c-blue-mid: #54799a;
  --c-blue-deep: #1c5c84;
  --c-blue-ink: #2c3e5e;
  --c-blue-wash: #edf3f8;
  --blue-rgb: 47, 109, 176;
  --blue-deep-rgb: 28, 92, 132;

  /* ── CHROME ACCENT · Atlas Nr. 51 Spinellblau. THE interface accent since
     2026-08-16, replacing teal. This 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 that the PDR commits to. Spinellblau measures 6.66:1 on the same
     field and 7.22:1 for white-on-fill (Mapbox contrast tool, 2026-08-16).

     No new hex was introduced. --c-blue-deep was ALREADY #1c5c84, and ten
     focus rings and checkboxes had drifted onto it on their own before this
     landed. The three names below exist so that "chrome" is greppable, and so
     that a later edit to the income family cannot silently restyle the
     interface.

     ── THE FORM LAW (read before using either blue) ──
     Chrome blue #1c5c84 and the income ramp's darkest step #1c4f84
     (VARIABLE_COLORS.income, cambridge.js:113/:125/:130) are 1.16:1 apart —
     the same colour to a viewer. That would break this file's own ONE COLOUR
     PER VARIABLE law (cambridge.js:89) if the two ever met. They never meet,
     because they are separated by FORM, not by hue:
       · chrome blue — a 2px underline, a focus ring, a filled control with
                       white text, or a wash at <= 12%. DOM chrome only.
       · income blue — a map fill or a legend swatch. Never chrome.
     Chrome blue is never PAINTED INTO the map as data: no fill, no stroke, no
     legend swatch. Where chrome must sit over the map at all — the parcel
     popup, the layer dock, the solar bar — it does so on its own opaque white
     card, and that card is what separates it from the income fills underneath
     (PDR 2.4, "nothing floats on the map").
     ONE DOCUMENTED EXCEPTION: the .hood-donut selection ring, which has no
     card. It is disclosed with its measurements at that rule, and it needs a
     form fix, not a hue fix.
     The map's OWN selection highlight (VARIABLE_COLORS.selection,
     cambridge.js:183) therefore stays TEAL and did not move: it is painted
     into the canvas on top of income blue, which is the one place chrome blue
     may not go. Selection in the DOM (rows, bars, chips) is chrome blue
     instead, because there it sits on top of a TEAL chart series and must not
     share that hue either. Same principle, opposite answer, different
     neighbours.
     The map's own selection highlight therefore stays TEAL: it sits directly
     on top of income blue, where blue-on-blue would be unreadable. Selection
     in the DOM (rows, bars, donut rings) is chrome blue instead, because there
     it sits on top of a TEAL chart series and must not share that hue either.
     Do NOT resolve this by moving the income ramp — its stops carry documented
     provenance and are reproduced in prototypes/reports.html and
     neighborhood-explorer.html.

     ── DO NOT "CLEAN UP" --c-teal-deep ──
     #1f5b52 is also a DATA value: it is the dark end of the assessed-value
     ramp, written as a literal at cambridge.js:828, :3466 and :3468. Retiring
     the token here would not touch those literals (correct), and repointing
     the token would silently restyle a published legend (not correct). ── */
  --c-chrome:      var(--c-blue-deep);    /* #1c5c84 Spinellblau (Nr. 51) */
  --c-chrome-wash: var(--c-blue-wash);    /* #edf3f8 */
  --chrome-rgb:    var(--blue-deep-rgb);  /* 28, 92, 132 */

  /* ── AMBER · alerts and highlights only (DESIGN.md r4-D7). --c-amber moved
     from #a9781f to Civic Burnt Orange #B45309 on 2026-08-16, for the 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.
     The rest of the family deliberately did NOT move. --c-amber-deep #8a5e12
     is the text companion and already measures 5.26:1, so it was never the
     failure; -mid / -bright / -wash-2 are the SUN AND SOLAR gradient
     (.ss-track, .ss-sun), which is a data surface, not chrome. Heat stays
     orange-only and shares nothing with blue. ── */
  --c-amber: #B45309;
  --c-amber-deep: #8a5e12;
  --c-amber-mid: #d97706;
  --c-amber-bright: #fbbf24;
  --c-amber-wash: #fbf3e4;
  --c-amber-wash-2: #fef3c7;
  --c-orange-soft: #f29b6e;
  --amber-rgb: 180, 83, 9;   /* #B45309 Civic Burnt Orange */
  --amber-mid-rgb: 217, 119, 6;
  --amber-bright-rgb: 251, 191, 36;

  /* ── RISK ── */
  --c-risk: #c0504d;
  --c-risk-deep: #a5403d;
  --c-risk-soft: #ef5350;
  --c-risk-line: #e6c9c8;
  --c-risk-wash: #fbeaea;
  --risk-rgb: 192, 80, 77;

  /* ── VIOLET · under review / model flag ── */
  --c-violet: #6d3f9c;
  --c-violet-light: #9660be;
  --c-violet-soft: #8479a0;
  --c-violet-muted: #5b4a70;
  --c-violet-deep: #4a2a6b;
  --c-violet-wash: #e2d9ef;
  --c-violet-wash-2: #f6f3fa;
  --violet-rgb: 185, 140, 214;

  /* ── SPACING · one 4/8 scale, nothing off it ── */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;

  /* ── RADII · exactly two. (pill/circle are shapes, not sizes.) ── */
  --r-panel: 12px;
  --r-control: 8px;
  --r-pill: 999px;

  /* ── SHADOWS · nothing heavier than 0 1px 2px ── */
  --shadow-1: 0 1px 2px rgba(var(--ink-rgb), 0.06);
  --shadow-2: 0 1px 2px rgba(var(--ink-rgb), 0.14);
  --shadow-inset: inset 0 1px 2px rgba(var(--ink-rgb), 0.08);

  /* ── OVERRIDE the inherited styles.css warm-cream tokens. cambridge.css
     loads after styles.css, so these win everywhere on this page and the
     shared components stop speaking Atlanta. ── */
  --bg: var(--n-050);
  --surface: var(--n-000);
  --card-bg: var(--n-050);
  --ink: var(--n-900);
  --muted: var(--n-600);
  --line: var(--n-150);
  /* --primary is THE primary-control alias and moved to chrome with the rest
     of the furniture. Inside this file it feeds six call sites, all of them
     chrome (.ss-date.active, .pc-tab.active, .pc-source, .pc-source-kind,
     .vt-btn.active, .map-search button). It also reaches two styles.css
     components that render on this page — .btn-news:hover (Join / Log in) and
     .toast — which are chrome too, and would otherwise have been the last two
     teal controls in the shell. .btn-demo's --primary fill never applies here:
     cambridge.css:1134 overrides Donate to an outline (r4-D7). */
  --primary: var(--c-chrome);
  /* --green stays TEAL on purpose: after this migration its only readers are
     verdicts (.pc-status.status-ok, .rt-pass, .rt-io-good, .redev-low) plus
     two links still queued for round 2 (.pc-expand summary, .pc-share). */
  --primary-light: var(--c-teal);
  --green: var(--c-teal);
  /* --accent is the ALERT alias, not an accent in the chrome sense. All 17 of
     its readers are alert/fail/highlight surfaces, so the amber -> Burnt
     Orange swap carries them correctly and DESIGN.md's r4-D7 rule ("amber =
     alerts & highlights only, never the Donate button") survives untouched.
     The one reader that was NOT an alert — .rail-toggle:focus-visible — was
     moved to --c-chrome; see the note there. */
  --accent: var(--c-amber);
  --amber: var(--c-amber);
  --red: var(--c-risk);
  --sidebar-bg: var(--c-blue-ink);
  --shadow-sm: var(--shadow-1);
  --shadow-md: var(--shadow-1);
  --shadow-lg: var(--shadow-2);
  --shadow-glow-green: var(--shadow-1);
  --shadow-glow-amber: var(--shadow-1);
  --shadow-glow-red: var(--shadow-1);

  /* ── LAYOUT · the shell's measurements, named (2026-09-12) ──────────────────
     The numbers the shell is laid out on, in one place, so a layout change edits
     a variable instead of hunting literals. Not theme tokens: the dark scope
     does not and must not re-declare them.
       --hdr-h    header height. PUBLISHED by the ResizeObserver right after the
                  header in cambridge.html; the values here and in the tier
                  overrides below are only fallbacks for a browser without one.
       --rail-w   left rail width (and the margin .main reserves for it)
       --panel-w  right contextual panel width on the Zoning view
       --gutter   space between header items
       --edge     header inline padding
     TIER BREAKPOINTS. A media query cannot read a custom property, so these are
     written literally in the rules below and listed here once:
       ≥1150px     one header row. The next tier starts at max-width: 1149.98px
       1150–901px  header: identity + account row, then the tabs in a scrolling row
       ≤1400px     the header strapline (.rail-prod) goes
       ≤1250px     --rail-w 232px
       ≤1150px     --panel-w 300px (panel rule, older than the header tier)
       ≤1024px     the rail is an overlay and defaults closed (railDefault)
       ≤900px      header: brand, Join, Menu; then search; the tabs live in the
                   menu sheet. The parcel panel goes under the map.
       ≤560px      phone: the intro strip, search and About pin bar go full width */
  --hdr-h: 58px;
  --rail-w: 252px;
  --panel-w: 336px;
  --gutter: 14px;
  --edge: 18px;

  /* Lets the UA paint scrollbars, form controls and the canvas backstop to
     match. Without it a dark page keeps white scrollbars. */
  color-scheme: light;
}
/* Tier values for the layout variables above. --hdr-h here is the fallback
   height of the header at each tier, not a measurement: 118px is identity row
   plus tabs row, 171px is what a 390px screen needed before the menu sheet. */
@media (max-width: 1250px) { :root { --rail-w: 232px; } }
@media (max-width: 1150px) { :root { --panel-w: 300px; } }
@media (max-width: 1149.98px) { :root { --hdr-h: 118px; --gutter: 10px; --edge: 12px; } }
@media (max-width: 560px) { :root { --hdr-h: 171px; --gutter: 8px; --edge: 10px; } }

/* ============================================================================
   DARK · the SAME token names, re-declared   (2026-08-17)
   ----------------------------------------------------------------------------
   Light is the product default and stays that way (DESIGN.md, 2026-07-17:
   "Light civic theme is the default; dark is a secondary variant only"). This
   scope is opt-in via <html data-theme="dark">, set before first paint by the
   inline script in cambridge.html and persisted at localStorage cam360.theme.

   Not one rule below the token blocks changed to make this work, and no token
   was renamed. That is the whole return on "THE ONE TOKEN BLOCK": the file
   already reads every colour through the neutral ramp and six semantic
   families, so dark is a re-declaration of the LEAVES. The aliases
   (--bg, --ink, --primary, --accent, --c-chrome …) are var() references and
   re-resolve on their own — repeating them here would create two places to
   forget. The three exceptions that DO need re-stating are the shadows; see
   the note at the bottom of this block.

   ── THE RAMP INVERTS, THE ROLES DO NOT ──
   In light, a higher --n-* number is darker. In dark it is lighter. What does
   NOT move is what each step MEANS: --n-000 is always the raised surface,
   --n-050 always the page recessed behind it, --n-900 always primary text,
   --n-600 always secondary text. Every rule in this file keeps working because
   every rule was written against the role, not the value.
     · --n-000 #3a4f5e  Atlas Nr. 50, the surface   (cards, header, rail)
     · --n-050 #324049  Atlas Nr. 60, the page      (sits BELOW the surface,
       exactly as light's #f4f6f9 sits below #ffffff)
     · --n-900 #e6ebee  primary text
     · --n-150           hairlines, #d2dadf at reduced alpha
   The two "rgb" companions invert with them, and this is deliberate:
   --white-rgb is not literally white, it is THE SURFACE — its nine call sites
   are all card fills at alpha 0.85-0.99, so pointing it at #3a4f5e carries
   every floating card into dark at once. --ink-rgb is THE TEXT: its 58 call
   sites are low-alpha washes and hairlines (0.03-0.16) plus a few dimmed text
   colours (0.47-0.75), and both readings stay correct once it is light.

   ── THE ACCENT IS DERIVED, NOT CHOSEN. DO NOT "SIMPLIFY" IT BACK. ──
   Spinellblau #1c5c84 is the light accent and it CANNOT come along: measured
   1.48:1 on #324049 (Mapbox contrast tool, 2026-08-16). Unusable, not merely
   dim.
   The trap on the way out is that this theme has TWO grounds, and the darker
   one flatters. #7db8e0 clears the page at 4.99:1 and FAILS the surface at
   3.99:1 — and the surface is where the chrome actually sits, on cards. So the
   rule is: THE ACCENT MUST CLEAR THE LIGHTER OF THE TWO GROUNDS.
   #95c6e9 clears both — 4.69:1 on #3a4f5e, 5.87:1 on #324049.
   Anyone tempted to fold this back to Spinellblau, or to stop at #7db8e0
   because it "passes", should re-measure against #3a4f5e first.

   ── THE ALERT IS DERIVED THE SAME WAY ──
   Civic Burnt Orange #B45309 measures 1.70:1 on #3a4f5e. Walking the same hue
   up in lightness: #d97706 = 2.68, #e8933f = 3.53, #f0a868 = 4.27 (still
   short), #f4b17c = 4.65 — the first step that clears. On the page it measures
   5.81:1. #f4b17c is HSL(26.5, 85%, 72%) against Burnt Orange's HSL(26, 90%,
   37%): the hue is held and only lightness moves, which is the same operation
   that took #1c5c84 (HSL 203, 65%, 31%) to #95c6e9 (HSL 205, 66%, 75%).
   It lands at 4.65 against the accent's 4.69 on the same ground, so neither
   chrome colour outshouts the other.

   ── EVERY OTHER FAMILY MIRRORS ITS LIGHT CONTRAST, IT DOES NOT INVENT ONE ──
   Measured on the surface it sits on (#3a4f5e dark, #ffffff light):
     token            dark              light            verdict
     --n-600          #bdcad5  5.11     #586b7d  5.51    AA both
     --n-500          #93a7b7  3.44     #8593a2  3.14    sub-AA in BOTH — this
                      step was never body text, it is a tertiary/decorative
                      grey, and dark is the better of the two
     --c-amber-deep   #f0c68e  5.36     #8a5e12  5.69    AA both
     --c-risk         #f4aba8  4.57     #c0504d  4.67    AA both
     --c-violet       #d9c2f0  5.25     #6d3f9c  7.39    AA both
     --c-green-ok     #7fd3a8  4.79     #2f7a54  5.21    AA both
     --c-teal         #75c8b8  4.35     #2b8577  4.45    sub-AA in BOTH, by
                      design: teal's AA failure is precisely why chrome left it
                      on 2026-08-16 (see the TEAL note above). Dark reproduces
                      the light shortfall rather than inventing a new one; if
                      teal is ever fixed, fix it in both scopes together.
   The rule this table encodes: NO DARK TOKEN MAY BE WORSE THAN ITS LIGHT
   COUNTERPART. Two are equal-and-still-short and say so out loud.

   ── --c-teal-deep IS SAFE TO RE-DECLARE HERE ──
   The TEAL note above warns that #1f5b52 is also the dark end of the
   assessed-value ramp. That ramp is written as LITERALS in cambridge.js
   (:828, :3466, :3468), not through this token — as that note itself records,
   "retiring the token here would not touch those literals". Re-pointing it in
   this scope therefore restyles the verdict chrome and leaves the published
   legend alone, which is the outcome we want.
   ========================================================================== */
:root[data-theme="dark"] {
  color-scheme: dark;

  /* ── NEUTRAL RAMP · inverted. Same slate family, same roles. ── */
  --n-000: #3a4f5e;   /* SURFACE — Atlas Nr. 50 (cards, header, rail) */
  --n-025: #364a58;   /* surface, one step recessed */
  --n-050: #324049;   /* PAGE — Atlas Nr. 60 */
  --n-100: #2b3841;   /* wash / inset — one step below the page */
  /* HAIRLINES — Atlas #d2dadf at reduced alpha, kept as an alpha rather than
     flattened to a hex so it composites correctly over BOTH grounds, which are
     5 points of lightness apart. 0.22 not 0.18: four of the 42 call sites use
     this token as a fill (.hdr-sep, .bo-toggle, .cl-minbtn:hover, the map
     placeholder), and those need to read as well as the 38 borders do. */
  --n-150: rgba(210, 218, 223, 0.22);
  --n-200: #4e6373;
  --n-250: #566c7d;
  --n-300: #647b8d;
  --n-400: #7d93a4;
  --n-500: #93a7b7;   /* tertiary / decorative — 3.44:1, see table above */
  --n-550: #9db0bf;
  --n-600: #bdcad5;   /* SECONDARY TEXT — 5.11:1 on the surface */
  --n-700: #c4d0da;
  --n-800: #d2dbe3;
  --n-850: #dee5eb;
  --n-900: #e6ebee;   /* PRIMARY TEXT — 7.11:1 on surface, 8.90:1 on page */
  --n-1000: #ffffff;
  --ink-rgb: 230, 235, 238;   /* = --n-900. "ink" means text, not "dark". */
  --n-500-rgb: 147, 167, 183;
  --white-rgb: 58, 79, 94;    /* = --n-000. "white" means surface. */

  /* ── TEAL · verdicts + the Updates/Finding chart series ── */
  --c-teal: #75c8b8;
  --c-teal-deep: #4ea596;
  --c-teal-wash: #2c4a48;
  --teal-rgb: 117, 200, 184;
  --teal-deep-rgb: 78, 165, 150;
  --c-green-ok: #7fd3a8;

  /* ── BLUE · civic information. NOTE: the income ramp on the MAP does not
     read these — it is VARIABLE_COLORS in cambridge.js, and the map stays
     light. These are the DOM's blues only. ── */
  --c-blue: #7fb2e0;
  --c-blue-bright: #8ec8e8;
  --c-blue-mid: #9fb7cc;
  --c-blue-deep: #95c6e9;     /* THE DARK ACCENT — derived above, do not fold */
  --c-blue-ink: #cdd9e6;
  --c-blue-wash: #2c4256;
  --blue-rgb: 127, 178, 224;
  --blue-deep-rgb: 149, 198, 233;

  /* ── AMBER · alerts and highlights only (DESIGN.md r4-D7) ── */
  --c-amber: #f4b17c;         /* Civic Burnt Orange, lifted — derived above */
  --c-amber-deep: #f0c68e;
  --c-amber-mid: #e8a05a;
  --c-amber-bright: #fbd07a;
  --c-amber-wash: #43382a;
  --c-amber-wash-2: #4a3c25;
  --c-orange-soft: #f3ab84;
  --amber-rgb: 244, 177, 124;
  --amber-mid-rgb: 232, 160, 90;
  --amber-bright-rgb: 251, 208, 122;

  /* ── RISK ── */
  --c-risk: #f4aba8;
  --c-risk-deep: #f8c3c1;
  --c-risk-soft: #ef8b87;
  --c-risk-line: #6b4a4a;
  --c-risk-wash: #452e2e;
  --risk-rgb: 244, 171, 168;

  /* ── VIOLET · under review / model flag ── */
  --c-violet: #d9c2f0;
  --c-violet-light: #e4d4f5;
  --c-violet-soft: #b9abd0;
  --c-violet-muted: #a394bb;
  --c-violet-deep: #c0a3e0;
  --c-violet-wash: #3d3350;
  --c-violet-wash-2: #342c44;
  --violet-rgb: 217, 194, 240;

  /* ── SHADOWS · the one group that CANNOT ride the aliases. All three are
     built from rgba(var(--ink-rgb), a), and --ink-rgb is now LIGHT — leaving
     them alone would turn every drop shadow into a white halo. Re-stated on
     black, and deeper, because a 6%-black shadow is invisible on #324049. ── */
  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.30);
  --shadow-2: 0 1px 2px rgba(0, 0, 0, 0.45);
  --shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.35);
}

/* ── THE MAP STAYS LIGHT. THIS IS CARTOGRAPHY, NOT A COMPROMISE. ─────────────
   There is deliberately no Mapbox style swap in the dark scope, and there must
   not be one.

   1. EVERY SEQUENTIAL RAMP ENCODES MAGNITUDE BY LIGHTNESS AGAINST ITS GROUND.
      Flip the ground and the reading inverts. Income measures 1.06 -> 7.76
      ascending against a light basemap; against a dark one the same five stops
      measure 9.28 -> 1.27, i.e. DESCENDING — the top step, the one the whole
      layer is about, becomes the least visible thing on the map.
   2. RE-TUNING THE RAMPS IS NOT AVAILABLE EITHER. Five of the map's colours are
      the City of Cambridge's official CDD district colours, which DESIGN.md
      forbids restyling. A dark basemap would require exactly the restyle that
      rule prohibits.

   Two things in cambridge.js are written for a light basemap and are LEFT
   ALONE on purpose: the 7 hard-coded white text halos (:717, :942, :2161,
   :4747, :5080, :8712, :8820) and applyBasemapTint() (:8698). They are correct
   as they stand and only become wrong if someone breaks rule 1 above.

   The DOM chrome that floats over the map does follow the theme — the legend
   card, the layer dock, the parcel card. Those are interface, not cartography,
   and they read the tokens like everything else. What stays put inside them
   are the LEGEND SWATCHES: those are painted from VARIABLE_COLORS by JS and
   must keep matching the map exactly. ── */

/* Legend and dock swatches are map-data colours sitting on a card that just
   went dark. The fills may not move (they have to match the canvas), so the
   dark end of a ramp gets a hairline ring instead — it separates the swatch
   from the card without touching a single published value. */
:root[data-theme="dark"] .legend-dot,
:root[data-theme="dark"] .cl-sw {
  box-shadow: 0 0 0 1px rgba(210, 218, 223, 0.35);
}

/* ── ON-CHROME TEXT · why seven `color: white` became `color: var(--n-000)` ──
   In light both are #ffffff, so this changed nothing — verified pixel-identical.
   In dark it is the difference between working and not: every one of the seven
   sits on a FILL that inverts (--primary -> #95c6e9, --accent -> #f4b17c,
   --green -> #75c8b8, --ink -> #e6ebee), and white on a light fill measures
   1.5-1.7:1. --n-000 is "the surface", so it lands on #3a4f5e — 4.69:1 on the
   accent, the same pair the accent was derived for.
   The seven: .btn-news:hover and .btn-demo (absorbed shell), .toast, and four
   in the parcel card — .pc-close:hover, .pc-source-kind, .pc-btn-approve:hover,
   .pc-btn-flag:hover. Fourteen call sites in this file already read
   var(--n-000) for exactly this; these seven had simply not been converted. */

/* The toast's shadow is the one shadow NOT on the --shadow-* scale — its
   0 8px 24px geometry is inherited from styles.css and kept — so it does not
   ride the re-declaration above and needs re-stating for the same reason the
   scale did: built on --ink-rgb, which is now light, it renders as a white
   halo. */
:root[data-theme="dark"] .toast { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); }

/* Logo: both colourways ship. The ink mark is invisible on a #3a4f5e header,
   so the white one takes over — same artwork, same alpha channel, RGB only.
   Verified for the 2026-08-21 mark as well as the one it replaced: the two
   files are 203x195 with the same 9,922 opaque pixels, so this is a repaint
   and not a second drawing that could drift from the first.
   THIS LINE IS HALF OF A SWAP. Its partner is .rail-orbit near line 1535, and
   the two live 1,000 lines apart in different blocks. That distance is the
   whole reason scripts/checks/brand.mjs exists — the sister repo shipped the
   light half of exactly this change and left the dark half on the retired
   mark, and the client is who noticed. */
:root[data-theme="dark"] .rail-orbit {
  background-image: url('assets/logo/citi360-mark-white.png?v=95f09983');
}

/* Theme cross-fade. The transition is live ONLY while .theme-anim sits on
   <html> (added by setTheme, removed 200ms later) — a standing transition on
   every element would tax every hover and every map-driven repaint for the
   sake of one deliberate click.
   Under prefers-reduced-motion this media query never matches, so the switch
   is instantaneous and nothing needs to opt out. */
@media (prefers-reduced-motion: no-preference) {
  html.theme-anim, html.theme-anim * {
    transition: background-color 180ms linear, border-color 180ms linear,
                color 180ms linear, fill 180ms linear !important;
  }
}

/* ============================================================================
   ABSORBED SHELL  ·  the last of styles.css, brought home (2026-08-17)
   ----------------------------------------------------------------------------
   cambridge.html used to load styles.css — 1,327 lines of the ATLANTA demo's
   warm-cream design system — purely for the handful of rules below. That file
   carries ~47 raw hexes and ~135 raw rgba() literals no token swap can reach,
   which is the root cause the PDR (§7.10) filed as "not cohesive", and it is
   why a dark mode would have been a two-file problem. The <link> is gone; this
   block is what it was actually contributing.

   ── WHAT WAS MEASURED, NOT ASSUMED ──
   Of the 371 selectors in styles.css, 23 matched anything in this page's DOM
   (probed live with querySelectorAll against every rule in the sheet). Of those
   23, three groups turned out to contribute NOTHING and were dropped rather
   than copied:
     · :root — all 19 of its custom properties are re-declared in the token
       block above, so not one value ever reached an element here.
     · .map-legend — all 11 declarations are overridden by our own .map-legend
       (see "MAP LEGEND" below); position/bottom/left/background/backdrop-filter/
       border/radius/padding/z-index/shadow/min-width, the whole rule.
     · .legend-title — same, all 5 declarations overridden.
   One further declaration was dropped on its own: styles.css gave .toolbar
   `background: rgba(246,244,240,0.85)`, Atlanta warm cream. .hdr360 has painted
   over it with var(--n-000) since this header existed, so it was dead pixels
   AND a foreign colour. Its sibling `backdrop-filter` is kept below.

   ── WHY THIS BLOCK SITS HERE, DIRECTLY UNDER :root ──
   styles.css loaded BEFORE cambridge.css, so every rule in it lost ties to
   every rule in this file. Keeping these at the TOP of this file reproduces
   that cascade exactly: .sidebar.rail still beats .sidebar, .hdr360 still
   beats .toolbar, .rail-foot still beats .sidebar-footer. Do not move this
   block downward, and do not append to it — new rules belong further down.

   Rewritten against the token block above: no raw hex, and the white alphas
   go through --white-rgb like the rest of the file.
   ========================================================================== */

/* Reset. The single highest-leverage thing styles.css was doing here: without
   it 1,488 of this page's 1,527 elements change box model or metrics. */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  height: 100%; width: 100%;
  font-family: 'Manrope', sans-serif;
  background: var(--bg); color: var(--ink);
  overflow: hidden;
}

/* Shell frame. .sidebar.rail and .main re-state width/margin further down
   (252px, and 232px under 1250px); these are the base values they override. */
.sidebar {
  position: fixed; left: 0; top: 0; bottom: 0;
  width: 220px;
  background: var(--sidebar-bg);
  display: flex; flex-direction: column;
  z-index: 100;
  border-right: 1px solid rgba(var(--white-rgb), 0.04);
}
.sidebar-footer {
  padding: 16px 22px;
  font-size: 0.68rem;
  color: rgba(var(--white-rgb), 0.25);
  border-top: 1px solid rgba(var(--white-rgb), 0.06);
}
.main { margin-left: 220px; height: 100vh; display: flex; flex-direction: column; }

@media (max-width: 1024px) {
  .sidebar { transform: translateX(-100%); }
  .main { margin-left: 0; }
}

/* Header base. .hdr360 restyles nearly all of it; what survives from here is
   justify-content, flex-shrink and the backdrop-filter. */
.toolbar {
  height: 56px; padding: 0 24px;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-between;
  flex-shrink: 0;
  backdrop-filter: blur(12px);
}
.toolbar-right { display: flex; align-items: center; gap: 12px; }

/* Header buttons: .btn-demo is Join, filled chrome with surface-coloured text;
   .btn-donate turns the same geometry into an outline for Donate (r4-D7, amber
   is for alerts); .btn-news is Log in, hidden until the forum opens. */
.btn-news {
  padding: 7px 16px;
  font-size: 0.78rem; font-weight: 600;
  background: var(--card-bg); color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 100px;
  cursor: pointer;
  font-family: 'Manrope', sans-serif;
  transition: all 0.2s;
}
.btn-news:hover { background: var(--primary); color: var(--n-000); border-color: var(--primary); }
.btn-demo {
  padding: 8px 20px;
  font-size: 0.78rem; font-weight: 600;
  background: var(--primary); color: var(--n-000);
  border: none;
  border-radius: 100px;
  cursor: pointer;
  font-family: 'Manrope', sans-serif;
  transition: opacity 0.2s;
}
.btn-demo:hover { opacity: 0.85; }

/* #map — this file re-states width/height/min-height at "LAYOUT" below; the
   radius and the shadow are the two that only ever came from here. 12px is
   --r-panel to the pixel. */
#map { width: 100%; height: 100%; border-radius: var(--r-panel); min-height: 400px; box-shadow: var(--shadow-lg); }

.mapboxgl-ctrl-logo { display: none !important; }

/* Toast. --primary is chrome Spinellblau, so the fill was already civic; the
   drop shadow was NOT — styles.css wrote it as rgba(27,58,45,0.3), Atlanta's
   dark green, the only literal in this block that had no token to land on.
   Geometry and alpha are kept to the number and only the hue moves onto ink,
   which is the smallest faithful translation available. The toast renders at
   opacity 0 until .show, so nothing on first paint depends on it.
   The label reads var(--n-000) rather than the literal `white` styles.css
   used: identical in light, and the only correct answer in dark, where
   --primary is a LIGHT blue and white-on-it measures 1.70:1. See the
   ON-CHROME TEXT note in the dark scope. */
.toast {
  position: fixed; bottom: 24px; right: 24px;
  background: var(--primary); color: var(--n-000);
  padding: 14px 22px;
  border-radius: 12px;
  font-size: 0.85rem; font-weight: 500;
  box-shadow: 0 8px 24px rgba(var(--ink-rgb), 0.3);
  transform: translateY(100px); opacity: 0;
  transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s;
  z-index: 9999;
  max-width: 380px;
  font-family: 'Manrope', sans-serif;
}
.toast.show { transform: translateY(0); opacity: 1; }
/* ═══ end absorbed shell ═══ */

/* ── NUMBER DISCIPLINE (research rule 7). Every number-bearing surface reads
   in lining tabular figures so columns align and a digit never changes width
   between two states. Paired with fmt() in cambridge.js, which is the only
   place a number is formatted. ── */
.pc-hero-val, .pc-kv strong, .pc-lk-row strong, .es-cap b, .es-rank, .es-donut b,
.stat-val, .dash-stat b, .dash-tile b, .tv-table td, .tv-table th,
.rules-matrix td, .rm-post, .rm-pre, .tl-row, .ul-val, .isp-key b, .cl-count,
.pm-bv, .ss-display, .ss-hour, .es-lgt, .bo-val, .dnut-val, .hood-detail td,
.hood-detail b, .stat b, .metric-val, .num {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}

/* ============================================
   POLYMETRON · Cambridge demo · v2 (UX redesign)
   Map = hero. Floating parcel card. Solar slider.
   ============================================ */

/* ── LAYOUT: full-width map canvas ─────────── */
.view-dashboard.active { display: flex !important; flex-direction: column !important; }

.map-canvas {
  position: relative;
  flex: 1;
  margin: 16px 24px 24px;
  border-radius: var(--r-panel);
  overflow: hidden;
  background: var(--n-150);
  box-shadow: var(--shadow-lg);
}
#map { width: 100%; height: 100%; min-height: 480px; }

.map-watermark {
  position: absolute; top: 54px; left: 26px;
  font-size: 0.62rem; letter-spacing: 0.22em;
  color: rgba(var(--ink-rgb),0.12); font-weight: 700;
  pointer-events: none; z-index: 5;
}

/* NOTE: no backdrop-filter on any panel that floats OVER the map canvas.
   Chromium's backdrop promotion fails intermittently over a heavy WebGL scene
   (deck.gl + city 3D) under GPU memory pressure and paints the element as an
   OPAQUE WHITE RECTANGLE with the DOM fully intact — the "panel went blank"
   reports (2026-07-25, twice). Solid rgba backgrounds cannot fail that way.
   Panels off the canvas (.cpanel, .table-view, .rail-disc) may keep blur. */

/* ── MAP STATUS PILL (loading indicator) ───── */
.map-status {
  position: absolute; top: 18px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 8px;
  background: rgba(var(--white-rgb),0.97); 
  border: 1px solid rgba(var(--ink-rgb),0.06); border-radius: var(--r-pill);
  padding: 7px 16px 7px 14px; z-index: 10;
  font-size: 0.74rem; font-weight: 600; color: var(--ink);
  box-shadow: var(--shadow-1);
  letter-spacing: 0.01em;
}
.map-status.error {
  background: rgba(var(--amber-rgb),0.1); color: var(--accent);
  border-color: rgba(var(--amber-rgb),0.25);
}

/* ── DEFAULT-STATE HINT CARD (top-right) ───── */
.map-hint {
  position: absolute; top: 18px; right: 24px;
  width: 280px; padding: 16px 18px;
  background: rgba(var(--white-rgb),0.97); 
  border: 1px solid rgba(var(--ink-rgb),0.06); border-radius: var(--r-panel);
  box-shadow: var(--shadow-1);
  z-index: 10;
  font-family: 'Manrope', sans-serif;
}
.hint-eyebrow {
  font-size: 0.62rem; letter-spacing: 0.14em; font-weight: 700;
  color: var(--c-chrome); margin-bottom: 6px;
}
.hint-title {
  font-family: 'Instrument Serif', serif;
  font-size: 1.15rem; line-height: 1.2; color: var(--ink);
  margin-bottom: 4px;
}
.hint-sub {
  font-size: 0.75rem; color: var(--muted); line-height: 1.45;
  margin-bottom: 10px;
}
.hint-tryline {
  font-size: 0.72rem; color: var(--n-800); line-height: 1.5;
  padding-top: 10px; border-top: 1px dashed rgba(var(--ink-rgb),0.08);
}
/* Moves with .hint-eyebrow above: this card IS the parcel pane's empty state
   (see the note at .pc-hint), and leaving one teal word inside an otherwise
   Spinellblau card would read as a defect rather than a distinction. */
.hint-tryline em { color: var(--c-chrome); font-style: normal; font-weight: 700; }

/* ── MAP LEGEND (bottom-left) ──────────────── */
.map-legend {
  position: absolute; bottom: 110px; left: 24px;
  backdrop-filter: none; -webkit-backdrop-filter: none;  /* overrides styles.css blur — see note above */
  background: rgba(var(--white-rgb),0.97); 
  border: 1px solid rgba(var(--ink-rgb),0.06); border-radius: var(--r-panel);
  padding: 10px 14px; z-index: 10;
  box-shadow: var(--shadow-1);
  min-width: 170px;
}
.legend-title {
  font-size: 0.6rem; font-weight: 700; letter-spacing: 0.14em;
  color: var(--muted); margin-bottom: 6px;
}
.legend-items { display: flex; flex-direction: column; gap: 3px; }
.legend-item {
  display: flex; align-items: center; gap: 8px;
  font-size: 0.72rem; color: var(--ink);
}
.legend-dot {
  width: 8px; height: 8px; border-radius: 50%;
  flex-shrink: 0;
}

/* ── SOLAR TIME · A TRANSPORT BAR IN THE BOTTOM BAND'S RIGHT CELL ──────────
   History, because it has moved three times and each move was a correction:
     · a 440x331 column centred on the canvas — 24.7% of the map, 35.9% with
       the build-out block open, sitting on the map's optical centre, which is
       exactly where flyToAndSelect() lands the parcel you searched for;
     · then (2026-08-07) one 56px horizontal strip absolutely positioned on the
       canvas's bottom edge — far better, but still ON the map;
     · now (2026-08-13) IN THE BOTTOM BAND, as .map-footer, beside the legend.
       Rocker: "what is currently floating on the map, we put down there. So
       their shadow tool is always with us… because the shadow is so important,
       I would put the shadow analysis in there."

   So: position:static. The `position:absolute; left/right/bottom:12px` block
   that lived here is GONE, and with it .map-canvas.has-solarbar — with nothing
   over the canvas the Mapbox scale/attribution controls have nothing to step
   over, and --solarbar-h has no reader left.

   56 = 1 border + 4 pad + 46 control + 4 pad + 1 border. Every control is the
   bar's full inner height — a 46px hit area, clear of DESIGN.md's 44px floor —
   while painting only its middle 32px: `background-clip: content-box` over a
   7px vertical padding does that without a fat bar. Focus rings use
   outline-offset:-7px so the ring lands on the paint, not the hit area.

   The card look changes with the position: --r-panel (the radius the legend and
   the selection strip beside it already wear) and no shadow, because a drop
   shadow is what a floating thing needs and this is docked. Solid --n-000
   rather than a 96% white, for the same reason: nothing shows through it now. */
.solar-slider {
  position: static; inset: auto; transform: none; width: auto; max-width: none;
  min-height: 56px; padding: 4px 12px;
  display: flex; flex-wrap: wrap; align-items: center; align-content: center;
  column-gap: 10px; row-gap: 6px;
  background: var(--n-000, #fff);
  border: 1px solid var(--n-150, #e3e8ee); border-radius: var(--r-panel);
  box-shadow: none;
  z-index: auto;
}
/* 46 + 4+4 padding + 2 border = the 56 above, exactly, in border-box.
   The controls are 46px tall (DESIGN.md's floor is 44) and paint only their
   middle 32px, so the bar reads thin while the targets stay big. */
.solar-slider > * { height: 46px; }
/* The scrubber is the ONLY item allowed to wrap. Its 300px basis is the point
   below which a single row stops being honest: squeezing the rail to 40px to
   keep one line is worse than two lines. Below that the bar becomes
   [dates · all hours · play] over [scrubber · readout · ⋯] — ~108px, still a
   third of the old panel, still nowhere near the map's centre. Done with
   flex-basis rather than a media query on purpose: the bar's width depends on
   the rail being open, the right panel being open AND the viewport, and a
   viewport query can only see the third of those. That is precisely the bug in
   the `max-width: calc(100vw - 360px)` rule this replaced. */
.ss-scrub { flex: 1 1 300px; min-width: 0; display: flex; align-items: center; gap: 10px; }

/* Date chips — segmented 4-up. The grey group pill is a pseudo-element so the
   buttons can keep the full 46px hit area inside a 32px-tall visual group. */
.ss-dates {
  position: relative; flex: 0 0 auto;
  display: flex;
}
.ss-dates::before {
  content: ''; position: absolute; left: 0; right: 0; top: 7px; bottom: 7px;
  background: var(--n-100, #eef2f6); border-radius: var(--r-control);
  pointer-events: none;
}
.ss-date {
  position: relative; z-index: 1; height: 46px;
  padding: 7px 9px; border: 0; background: transparent; background-clip: content-box;
  border-radius: var(--r-control); cursor: pointer;
  font-family: 'Manrope', sans-serif; font-size: 0.75rem; font-weight: 600;
  color: var(--n-600, #586b7d); white-space: nowrap;
  transition: background-color 0.15s, color 0.15s;
}
.ss-date:hover { color: var(--n-900, #1e2732); background: var(--n-150, #e3e8ee); background-clip: content-box; }
.ss-date.active {
  background: var(--c-chrome); background-clip: content-box;
  color: var(--n-000, #fff); font-weight: 700;
}

/* Butterfly mode — every hour 8:00–16:00 as one overlaid fan. */
.ss-bf {
  flex: 0 0 auto;
  padding: 7px 12px; border: 0; background: var(--n-100, #eef2f6); background-clip: content-box;
  border-radius: var(--r-control); cursor: pointer;
  font-family: 'Manrope', sans-serif; font-size: 0.75rem; font-weight: 600;
  color: var(--n-700, #46596b); white-space: nowrap;
}
.ss-bf:hover { background: var(--n-150, #e3e8ee); background-clip: content-box; }
.ss-bf[aria-pressed="true"] {
  background: var(--c-blue-deep, #1c5c84); background-clip: content-box; color: var(--n-000, #fff);
}
.solar-slider.bf-on .ss-track { opacity: 0.35; pointer-events: none; }

/* Play — a 64px TRANSPORT BUTTON, not a full-width CTA. Spinellblau is the
   primary accent; spending it full-bleed inside a map overlay declared this
   the most important action on screen. Amber while running (the existing
   aria-pressed swap), which is the one place amber is allowed: a live-state
   highlight. */
.ss-play {
  flex: 0 0 auto; min-width: 64px;
  padding: 7px 12px; border: 0; background: var(--c-chrome); background-clip: content-box;
  border-radius: var(--r-control); cursor: pointer;
  font-family: 'Manrope', sans-serif; font-size: 0.75rem; font-weight: 700;
  color: var(--n-000, #fff); white-space: nowrap;
}
.ss-play:hover { filter: brightness(1.12); }
.ss-play[aria-pressed="true"] { background: var(--c-amber-deep, #8a5e12); background-clip: content-box; }

/* Scrubber — the widest thing on the bar, which is right: it is the control
   people actually drag. Ticks ride ON the gradient rail, so they cost zero
   height (they used to hang 12px below it). */
.ss-track { flex: 1 1 auto; min-width: 60px; position: relative; height: 46px;
  display: flex; align-items: center; }
.ss-range {
  width: 100%; margin: 0;
  -webkit-appearance: none; appearance: none;
  height: 20px; border-radius: var(--r-pill);
  background: linear-gradient(to right,
    var(--c-blue-ink) 0%,        /* pre-dawn deep blue */
    var(--c-orange-soft) 12%,       /* sunrise warm */
    var(--c-amber-bright) 30%,       /* morning amber */
    var(--c-amber-wash-2) 50%,       /* noon high */
    var(--c-amber-bright) 70%,       /* afternoon amber */
    var(--c-orange-soft) 88%,       /* sunset warm */
    var(--c-blue-ink) 100%       /* dusk deep blue */
  );
  outline: none; cursor: pointer;
  box-shadow: var(--shadow-inset);
}
.ss-ticks {
  position: absolute; left: 0; right: 0; top: 13px; height: 20px;
  pointer-events: none;
}
/* 11px, full opacity — DESIGN.md's caption floor. Was 0.56rem (≈9px) at
   opacity .6, which broke both the size floor and 4.5:1. Ink on the amber
   rail is ~7:1; the white halo carries it over the deep-blue ends. */
.ss-tick {
  position: absolute; top: 50%; transform: translateX(-50%) translateY(-50%);
  font-size: 11px; font-weight: 700; letter-spacing: 0.02em;
  color: var(--n-900, #1e2732);
  text-shadow: 0 0 3px rgba(var(--white-rgb),0.95), 0 0 2px rgba(var(--white-rgb),0.95);
}

/* Sun thumb — amber gradient + soft glow */
.ss-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 24px; height: 24px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 35% 35%, var(--c-amber-wash-2) 0%, var(--c-amber-bright) 45%, var(--c-amber-mid) 100%);
  border: 2px solid white;
  cursor: grab;
  box-shadow: 0 0 0 1px rgba(var(--amber-mid-rgb), 0.4), var(--shadow-2);
  transition: transform 0.12s, box-shadow 0.12s;
}
.ss-range::-webkit-slider-thumb:hover {
  transform: scale(1.14);
  box-shadow: 0 0 0 1px rgba(var(--amber-mid-rgb), 0.6), var(--shadow-2);
}
.ss-range::-webkit-slider-thumb:active { cursor: grabbing; transform: scale(1.06); }

.ss-range::-moz-range-thumb {
  width: 24px; height: 24px; border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, var(--c-amber-wash-2) 0%, var(--c-amber-bright) 45%, var(--c-amber-mid) 100%);
  border: 2px solid white;
  cursor: grab;
  box-shadow: 0 0 0 1px rgba(var(--amber-mid-rgb), 0.4), var(--shadow-2);
}

/* Readout — one 13px inline slot. It was a 1.12rem Instrument Serif two-line
   hero; tabular-nums so it cannot jitter the layout while you scrub. */
.ss-display {
  flex: 0 0 auto; min-width: 134px; height: 46px;
  display: flex; align-items: center; justify-content: flex-end; text-align: right;
  font-family: 'Manrope', sans-serif; font-size: 13px; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--n-800, #3d4854);
  white-space: nowrap; letter-spacing: 0.01em;
}

/* "⋯" — speed and record, the two things nobody sets mid-task. */
.ss-more-wrap { position: relative; flex: 0 0 auto; display: flex; }
.ss-more-btn {
  width: 36px; height: 46px; padding: 7px 0;
  border: 0; background: var(--n-100, #eef2f6); background-clip: content-box;
  border-radius: var(--r-control); cursor: pointer;
  font-size: 1rem; line-height: 1; color: var(--n-700, #46596b);
}
.ss-more-btn:hover { background: var(--n-150, #e3e8ee); background-clip: content-box; }
.ss-more-btn[aria-expanded="true"] { background: var(--n-200, #cfd8e0); background-clip: content-box; }
.ss-pop {
  position: absolute; bottom: calc(100% + 10px); right: 0; z-index: 3;
  width: 236px; padding: 12px;
  background: var(--n-000, #fff); border: 1px solid var(--n-150, #e3e8ee);
  border-radius: var(--r-panel); box-shadow: var(--shadow-2);
}
.ss-pop[hidden] { display: none; }
.ss-pop-row { display: flex; align-items: center; gap: 8px; }
.ss-pop-row label {
  font-size: 11px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--n-600, #586b7d); flex: 0 0 auto;
}
.ss-pop-row input[type="range"] { flex: 1 1 auto; min-width: 0; accent-color: var(--c-chrome); height: 18px; }
/* A bare speed slider with no readout — you could not tell what you had set. */
.ss-speed-val { flex: 0 0 auto; font-size: 12px; font-weight: 700;
  font-variant-numeric: tabular-nums; color: var(--n-800, #3d4854); min-width: 34px; text-align: right; }
.ss-rec {
  display: block; width: 100%; margin-top: 10px;
  font-family: 'Manrope', sans-serif; font-size: 12px; font-weight: 700;
  border: 1px solid var(--n-150, #e3e8ee); background: var(--n-000, #fff); color: var(--c-risk);
  padding: 8px; border-radius: var(--r-control); cursor: pointer;
}
.ss-rec:hover { background: var(--c-risk-wash, #fbeaea); }
.ss-rec.on { background: var(--c-risk); border-color: var(--c-risk); color: var(--n-000, #fff); }
.ss-rec:disabled { opacity: 0.5; cursor: default; }
/* 11px floor — was 0.56rem (≈9px). */
.ss-recnote { font-size: 11px; color: var(--n-600, #586b7d); line-height: 1.45; margin-top: 8px; }
.ss-recnote:empty { display: none; }

/* Visible focus everywhere. The -12px offset draws the ring on the 32px
   visual rather than around the invisible 56px hit area. */
.solar-slider button:focus-visible {
  outline: 2px solid var(--c-chrome); outline-offset: -7px;
}
.ss-pop button:focus-visible, .ss-pop input:focus-visible {
  outline: 2px solid var(--c-chrome); outline-offset: 2px;
}
.ss-range:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 3px; }

/* The day sweep is user-initiated and has a pause control, so it is not
   auto-playing motion; the decorative transitions still stand down. */
@media (prefers-reduced-motion: reduce) {
  .solar-slider *, .ss-range::-webkit-slider-thumb { transition: none !important; }
  .ss-range::-webkit-slider-thumb:hover { transform: none; }
}

/* ── FLOATING PARCEL CARD (top-right, on click) */
.parcel-card {
  position: absolute; top: 18px; right: 24px;
  width: 420px;
  max-height: calc(100% - 36px);
  background: rgba(var(--white-rgb),0.97); backdrop-filter: blur(24px);
  border: 1px solid rgba(var(--ink-rgb),0.06); border-radius: var(--r-panel);
  box-shadow: var(--shadow-2);
  z-index: 20;
  display: flex; flex-direction: column;
  font-family: 'Manrope', sans-serif;
  overflow: hidden;
  animation: pcSlideIn 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes pcSlideIn {
  from { opacity: 0; transform: translateX(12px) scale(0.97); }
  to   { opacity: 1; transform: translateX(0) scale(1); }
}

.pc-close {
  position: absolute; top: 12px; right: 12px;
  width: 28px; height: 28px; border-radius: 50%;
  background: rgba(var(--ink-rgb),0.05); border: none;
  font-size: 1.3rem; cursor: pointer; color: var(--muted);
  display: flex; align-items: center; justify-content: center;
  font-family: sans-serif; z-index: 2;
  transition: all 0.15s;
}
.pc-close:hover { background: var(--ink); color: var(--n-000); }

/* Header */
.pc-header { padding: 18px 22px 14px; padding-right: 50px; }
.pc-address {
  font-family: 'Instrument Serif', serif;
  font-size: 1.3rem; font-weight: 400; color: var(--ink);
  line-height: 1.2;
}
.pc-meta {
  font-size: 0.76rem; color: var(--muted);
  margin-top: 4px; letter-spacing: 0.01em;
}

/* Hero metric */
.pc-hero {
  margin: 0 22px 14px; padding: 12px 2px 14px;
  border-bottom: 1px solid var(--line, rgba(var(--ink-rgb),0.1));
}
.pc-hero-label {
  font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase;
  font-weight: 700; color: var(--muted);
}
.pc-hero-value {
  font-size: 1.6rem; font-weight: 700; color: var(--ink);
  letter-spacing: -0.01em; font-variant-numeric: tabular-nums;
  white-space: nowrap; line-height: 1.15; margin-top: 3px;
}
.pc-hero-note { font-size: 0.66rem; color: var(--muted); margin-top: 3px; }

/* §19.52 status — one calm row: status icon + verdict, color only on the icon */
.pc-status {
  margin: 0 22px 14px; padding: 11px 13px;
  border: 1px solid var(--line, rgba(var(--ink-rgb),0.1)); border-radius: var(--r-panel);
  background: var(--n-000); display: flex; gap: 11px; align-items: flex-start;
}
.pc-status-ic { width: 20px; height: 20px; flex: 0 0 auto; margin-top: 1px;
  fill: none; stroke: var(--muted); stroke-width: 1.6;
  stroke-linejoin: round; stroke-linecap: round; }
.pc-status.status-fail .pc-status-ic { stroke: var(--accent); }
.pc-status.status-ok   .pc-status-ic { stroke: var(--green); }
.pc-status-body { min-width: 0; }
#pc-status-text {
  font-size: 0.74rem; font-weight: 700; letter-spacing: 0.01em;
  color: var(--ink); display: block;
}
.pc-status-detail { font-size: 0.72rem; color: var(--muted);
  margin-top: 3px; line-height: 1.45; }

/* Tabs */

.pc-tabs {
  display: flex; gap: 0;
  border-bottom: 1px solid rgba(var(--ink-rgb),0.08);
  padding: 0 22px;
}
.pc-tab {
  padding: 11px 13px;
  font-size: 0.9rem; font-weight: 600; letter-spacing: 0.01em;
  background: none; border: none;
  color: var(--muted); cursor: pointer;
  border-bottom: 2px solid transparent;
  font-family: 'Manrope', sans-serif;
  transition: color 0.15s, border-color 0.2s;
}
.pc-tab:hover { color: var(--ink); }
.pc-tab.active {
  color: var(--ink); border-bottom-color: var(--primary);
}

/* Tab content — grows with the viewport and scrolls */
.pc-content {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: 14px 22px 16px;
  max-height: calc(100vh - 300px);
}
.pc-pane { display: none; animation: paneFade 0.2s ease; }
.pc-pane.active { display: block; }
@keyframes paneFade { from { opacity: 0; } to { opacity: 1; } }

.pc-kv {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 7px 0; border-bottom: 1px solid rgba(var(--ink-rgb),0.05);
  font-size: 0.86rem;
}
.pc-kv:last-of-type { border-bottom: none; }
.pc-kv span { color: var(--muted); }
.pc-kv strong { color: var(--ink); font-weight: 600; }
.pc-kv-strong { padding-top: 8px; margin-top: 4px; border-top: 1px solid rgba(var(--ink-rgb),0.08); }
.pc-kv-strong:first-of-type { border-top: 1px solid rgba(var(--ink-rgb),0.08); }

.pc-expand {
  margin-top: 12px;
  font-size: 0.72rem;
}
.pc-expand summary {
  cursor: pointer; padding: 6px 0;
  color: var(--green); font-weight: 600;
  list-style: none; outline: none;
}
/* The rule above sets `outline: none` and never replaced it, so all three folds
   in this card were operable from the keyboard with NO visible focus indicator
   — against DESIGN.md's "visible focus rings everywhere". Same ring the rest of
   the shell uses (.method-about > summary, .t360[data-view]); nothing changes
   at rest. */
.pc-expand summary:focus-visible { outline: 2px solid var(--c-chrome);
  outline-offset: 2px; border-radius: var(--r-control); }
.pc-expand summary::-webkit-details-marker { display: none; }
.pc-expand summary::before {
  content: '+ '; font-weight: 700; margin-right: 2px;
}
.pc-expand[open] summary::before { content: '− '; }
.pc-expand-body {
  padding: 8px 12px;
  background: rgba(var(--ink-rgb),0.03);
  border-radius: var(--r-control);
  color: var(--n-800); line-height: 1.55;
}
.pc-expand-body code {
  font-family: 'SF Mono', Menlo, monospace; font-size: 0.7rem;
  background: rgba(var(--ink-rgb),0.06); padding: 1px 4px; border-radius: var(--r-control);
}

.pc-note {
  margin-top: 12px; padding: 12px 14px;
  background: rgba(var(--amber-rgb),0.05);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--r-control) var(--r-control) 0;
  font-size: 0.74rem; color: var(--n-800); line-height: 1.55;
}
.pc-note strong { color: var(--accent); }

/* Sources */
.pc-source {
  display: block; padding: 10px 12px; margin-bottom: 6px;
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--r-control); text-decoration: none;
  transition: all 0.15s;
}
.pc-source:hover {
  border-color: var(--primary); background: white;
  transform: translateX(-2px);
  box-shadow: var(--shadow-1);
}
.pc-source-kind {
  display: inline-block; padding: 2px 7px;
  background: var(--primary); color: var(--n-000);
  font-size: 0.58rem; font-weight: 700; letter-spacing: 0.08em;
  border-radius: var(--r-control); margin-bottom: 5px;
}
.pc-source-title {
  display: block; font-size: 0.78rem; color: var(--ink);
  font-weight: 500; line-height: 1.4;
}

/* Footer (actions) */
.pc-footer {
  display: flex; gap: 8px;
  padding: 12px 22px;
  border-top: 1px solid rgba(var(--ink-rgb),0.08);
  background: rgba(var(--ink-rgb),0.02);
}
.pc-btn {
  flex: 1; padding: 9px 12px;
  display: flex; align-items: center; justify-content: center; gap: 6px;
  font-size: 0.78rem; font-weight: 600;
  border: 1px solid; background: white;
  border-radius: var(--r-control); cursor: pointer;
  font-family: 'Manrope', sans-serif;
  transition: all 0.18s;
}
.pc-btn:hover { transform: translateY(-1px); }
.pc-btn:active { transform: translateY(0); }
.pc-btn-approve {
  color: var(--green); border-color: rgba(var(--teal-rgb),0.4);
}
.pc-btn-approve:hover {
  background: var(--green); color: var(--n-000); border-color: var(--green);
}
.pc-btn-flag {
  color: var(--accent); border-color: rgba(var(--amber-rgb),0.5);
}
.pc-btn-flag:hover {
  background: var(--accent); color: var(--n-000); border-color: var(--accent);
}

.pc-log {
  padding: 8px 22px 12px;
  font-size: 0.66rem; color: var(--muted);
  max-height: 60px; overflow-y: auto;
}
.pc-log-entry {
  padding: 3px 0; border-bottom: 1px dashed rgba(var(--ink-rgb),0.06);
}
.pc-log-entry:last-child { border-bottom: none; }

/* Layer-key block inside the Solar tab — clarifies ShadeMap vs impact heatmap */
.pc-layer-key {
  margin: 12px 0 10px;
  padding: 10px 12px;
  background: rgba(var(--ink-rgb),0.03);
  border-radius: var(--r-control);
  font-size: 0.7rem;
}
.pc-lk-row {
  display: grid;
  grid-template-columns: 14px 1fr;
  gap: 6px 8px;
  align-items: baseline;
  padding: 4px 0;
}
.pc-lk-row strong { color: var(--ink); font-weight: 600; grid-column: 2; }
.pc-lk-detail {
  grid-column: 2; color: var(--muted);
  font-size: 0.65rem; line-height: 1.4;
  margin-top: -1px;
}
.pc-lk-swatch {
  width: 12px; height: 12px; border-radius: var(--r-control);
  grid-row: 1; grid-column: 1;
  margin-top: 2px;
}
.pc-lk-context {
  background: linear-gradient(135deg, rgba(var(--ink-rgb),0.65), rgba(var(--ink-rgb),0.4));
  border: 1px solid rgba(var(--ink-rgb),0.55);
}
.pc-lk-impact {
  background: linear-gradient(135deg, var(--c-risk-soft), var(--c-amber-bright));
  border: 1px solid rgba(var(--amber-rgb),0.55);
}

/* Pre / Post envelope mini-legend (inside Envelope tab) */
.pc-mini-legend {
  display: flex; gap: 10px; margin-bottom: 12px;
  padding: 8px 10px;
  background: rgba(var(--ink-rgb),0.03);
  border-radius: var(--r-control);
  font-size: 0.66rem; color: var(--muted);
  letter-spacing: 0.04em;
}
.mini-chip { display: inline-flex; align-items: center; gap: 5px; }
.chip-swatch {
  display: inline-block; width: 10px; height: 10px; border-radius: var(--r-control);
  border: 1.5px solid;
}
.chip-pre  { background: rgba(var(--n-500-rgb),0.18); border-color: rgba(var(--n-500-rgb),0.7); }
.chip-post { background: rgba(var(--blue-rgb),0.18);  border-color: rgba(var(--blue-rgb),0.9); }

/* ── RESPONSIVE — collapse parcel card on narrow ── */
@media (max-width: 1280px) {
  .map-canvas { margin: 12px 16px 16px; }
  .parcel-card { width: 340px; }
  /* The `.solar-slider { width: 480px; max-width: calc(100vw - 320px) }` that
     lived here was deleted 2026-08-07: it WIDENED the panel as the map got
     narrower, so coverage rose on small screens. The bar has no width of its
     own at all now — it fills .map-footer, the bottom band's right cell, and
     the band's own 1100px breakpoint is what handles narrow. */
}
@media (max-width: 1024px) {
  .parcel-card { width: 320px; }
  .map-hint { width: 240px; }
}

/* ── HIDE old dark-panel styles that no longer apply ── */
.detail-panel.dark-panel { display: none !important; }
.dash-split { display: none !important; }

/* ── Rules tab: pre/post matrix + crucial-rules watch + redev alert ── */
.rules-caption { font-size: 0.82rem; color: var(--muted); line-height: 1.5; margin-bottom: 10px; }
.rules-caption strong { color: var(--ink); }

.rules-watch { margin-bottom: 14px; padding: 0; }
.rw-title {
  font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase;
  font-weight: 700; color: var(--muted); margin-bottom: 6px;
}
.rw-row { display: grid; grid-template-columns: 62px 1fr; gap: 10px; align-items: baseline;
  padding: 6px 0; font-size: 0.78rem; color: var(--n-800); line-height: 1.45;
  border-top: 1px solid var(--line, rgba(var(--ink-rgb),0.08)); }
.rw-tag {
  font-size: 0.56rem; letter-spacing: 0.06em; font-weight: 700; text-transform: uppercase;
  color: var(--muted); }

.rules-matrix { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.rules-matrix .rm-head th {
  text-align: left; font-size: 0.6rem; letter-spacing: 0.06em;
  color: var(--muted); font-weight: 700; padding: 4px 6px; border-bottom: 1px solid var(--line);
}
.rules-matrix td { padding: 5px 6px; border-bottom: 1px solid rgba(var(--ink-rgb),0.05); vertical-align: top; }
.rules-matrix .rm-k { color: var(--muted); width: 34%; }
.rules-matrix .rm-pre { color: var(--n-500); width: 30%; }
.rules-matrix .rm-post { color: var(--ink); font-weight: 600; }
.rules-matrix tr.rm-changed .rm-post { color: var(--accent); }
.rules-matrix tr.rm-changed .rm-k { color: var(--ink); font-weight: 600; }
.rules-foot { margin-top: 8px; font-size: 0.72rem; color: var(--muted); line-height: 1.5; }
.rules-foot strong { color: var(--n-500); }

.redev-alert { margin-top: 14px; padding: 12px 13px; border-radius: var(--r-panel);
  border: 1px solid var(--line, rgba(var(--ink-rgb),0.1)); background: var(--n-000); }
.redev-head { font-size: 0.78rem; color: var(--ink); display: flex; align-items: center; gap: 9px; font-weight: 600; }
.redev-ic { width: 18px; height: 18px; flex: 0 0 auto; fill: none; stroke: var(--muted);
  stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.redev-high .redev-ic     { stroke: var(--accent); }
.redev-elevated .redev-ic { stroke: var(--c-amber); }
.redev-low .redev-ic      { stroke: var(--green); }
.redev-level { font-weight: 800; letter-spacing: 0.03em; }
.redev-high .redev-level     { color: var(--accent); }
.redev-elevated .redev-level { color: var(--c-amber); }
.redev-low .redev-level      { color: var(--green); }
.redev-mg { margin-left: auto; font-size: 0.54rem; letter-spacing: 0.09em; text-transform: uppercase;
  font-weight: 700; color: var(--n-400); }
.redev-reasons { margin: 9px 0 0; padding-left: 16px; }
.redev-reasons li { font-size: 0.76rem; color: var(--n-800); line-height: 1.5; margin: 3px 0; }
.redev-rule { margin-top: 9px; font-size: 0.64rem; color: var(--muted); font-style: italic; line-height: 1.45; }
.redev-sale { margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--line, rgba(var(--ink-rgb),0.08));
  font-size: 0.76rem; line-height: 1.5; color: var(--ink); }
.redev-sale.redev-sale-off { color: var(--muted); font-style: italic; }

/* ── Inclusionary-eligible (>5,000 SF) highlight toggle in the legend ── */
.legend-toggle {
  display: flex; align-items: center; gap: 8px;
  margin-top: 8px; padding-top: 8px;
  border-top: 1px solid rgba(var(--ink-rgb),0.08);
  cursor: pointer; user-select: none;
}
.legend-toggle input { accent-color: var(--c-blue-deep); margin: 0; cursor: pointer; width: 13px; height: 13px; }
.lt-swatch { width: 12px; height: 12px; border-radius: var(--r-control); background: rgba(var(--blue-deep-rgb),0.34); border: 1px solid var(--c-blue-deep); flex: 0 0 auto; }
.lt-text { font-size: 0.72rem; color: var(--ink); line-height: 1.2; }
.lt-text small { display: block; font-size: 0.6rem; color: var(--muted); margin-top: 1px; letter-spacing: 0.01em; }

/* ── C-1 character-vs-capacity charts (Hero Finding panel) ── */
.c1-profile { margin-bottom: 16px; }
.c1-eyebrow { font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 700; color: var(--accent); margin-bottom: 8px; }
.c1-stats { display: flex; gap: 8px; margin-bottom: 14px; }
.c1-stat { flex: 1; background: rgba(var(--ink-rgb),0.03); border: 1px solid rgba(var(--ink-rgb),0.06); border-radius: var(--r-control); padding: 8px 6px; text-align: center; }
.c1-stat b { display: block; font-family: 'Instrument Serif', serif; font-size: 1.5rem; color: var(--ink); line-height: 1; font-variant-numeric: tabular-nums; }
.c1-stat span { font-size: 0.58rem; color: var(--muted); }
.c1-stat-alarm { background: rgba(var(--amber-rgb),0.06); border-color: rgba(var(--amber-rgb),0.18); }
.c1-stat-alarm b { color: var(--accent); }
.c1-chart { margin-bottom: 14px; }
.c1-chart-h { font-size: 0.72rem; font-weight: 700; color: var(--ink); margin-bottom: 7px; display: flex; justify-content: space-between; align-items: baseline; }
.c1-chart-h small { font-size: 0.62rem; font-weight: 400; color: var(--muted); }
.c1-bar-row { display: grid; grid-template-columns: 52px 1fr 30px; align-items: center; gap: 8px; margin: 3px 0; }
.c1-bar-lab { font-size: 0.65rem; color: var(--muted); text-align: right; }
.c1-bar-track { display: block; height: 11px; background: rgba(var(--ink-rgb),0.05); border-radius: var(--r-control); overflow: hidden; }
.c1-bar-fill { display: block; height: 100%; min-width: 2px; background: var(--n-400); border-radius: var(--r-control); transition: width .6s ease; }
.c1-bar-fill.c1-emph { background: var(--accent); }
.c1-bar-val { font-size: 0.63rem; color: var(--ink); font-variant-numeric: tabular-nums; text-align: right; }
.c1-capacity { margin-top: 8px; font-size: 0.68rem; color: var(--c-blue-deep); background: rgba(var(--blue-deep-rgb),0.08); border-left: 3px solid var(--c-blue-deep); padding: 6px 10px; border-radius: 0 var(--r-control) var(--r-control) 0; }
.c1-capacity b { color: var(--c-blue-deep); }
.c1-punch { font-size: 0.75rem; font-weight: 600; color: var(--accent); line-height: 1.45; padding: 10px 12px; background: rgba(var(--amber-rgb),0.06); border-radius: var(--r-control); margin-bottom: 4px; }

/* historic/conservation toggle swatch */
.lt-swatch-historic { background: rgba(var(--amber-rgb),0.16); border: 1px dashed var(--c-amber-deep); }

/* ── 2D / 3D view toggle (top-left) ── */
.view-toggle {
  position: absolute; top: 16px; left: 24px; z-index: 11;
  display: flex; gap: 2px;
  background: rgba(var(--white-rgb),0.97); 
  border: 1px solid rgba(var(--ink-rgb),0.06); border-radius: var(--r-control); padding: 3px;
  box-shadow: var(--shadow-1);
}
.vt-btn {
  padding: 5px 12px; font-size: 0.74rem; font-weight: 600;
  border: none; background: none; color: var(--muted); cursor: pointer;
  border-radius: var(--r-control); font-family: 'Manrope', sans-serif; transition: background .15s, color .15s;
}
.vt-btn:hover { color: var(--ink); }
.vt-btn.active { background: var(--primary); color: var(--n-000); }

/* ── Setback envelope schematic (Envelope tab) ── */
.pc-setback { margin: 6px 0; padding: 8px 0 4px; }
.sb-title { font-size: 0.72rem; font-weight: 600; color: var(--ink); margin-bottom: 6px; }
.sb-title small { font-weight: 400; color: var(--muted); font-size: 0.62rem; }
.sb-svg { width: 100%; max-width: 250px; height: auto; display: block; }
.sb-lot { fill: rgba(var(--ink-rgb),0.04); stroke: var(--n-300); stroke-width: 1.2; }
.sb-build { fill: rgba(var(--blue-deep-rgb),0.14); stroke: var(--c-blue-deep); stroke-width: 1.3; stroke-dasharray: 3 2; }
.sb-lab { font-size: 8.5px; fill: var(--muted); text-anchor: middle; font-family: 'Manrope', sans-serif; font-weight: 600; }
.sb-build-lab { fill: var(--c-blue-deep); }

/* ── Rule-trace recommendation scorecard (the differentiator) ── */
.pc-ruletrace { margin: 0 22px 12px; padding: 0; }
.rt-head { display: flex; align-items: baseline; gap: 9px; margin-bottom: 8px; }
.rt-title { font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 700; color: var(--muted); }
.rt-conf { margin-left: auto; font-size: 0.64rem; color: var(--muted); }
.rt-summary { display: flex; gap: 16px; align-items: center; }
.rt-stat { display: inline-flex; align-items: center; gap: 6px; font-size: 0.76rem; font-weight: 600; color: var(--ink); }
.rt-stat svg { width: 14px; height: 14px; flex: 0 0 auto; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.rt-stat.rt-pass svg { stroke: var(--green); }
.rt-stat.rt-fail svg { stroke: var(--accent); }
.rt-stat.rt-na svg { stroke: var(--muted); }

.rt-details { margin-top: 9px; }
.rt-details summary { cursor: pointer; font-size: 0.72rem; color: var(--accent); font-weight: 600; list-style: none; outline: none; }
.rt-details summary::-webkit-details-marker { display: none; }
.rt-details summary::before { content: '+ '; font-weight: 700; }
.rt-details[open] summary::before { content: '\2212 '; }
.rt-list { margin-top: 8px; display: flex; flex-direction: column; gap: 5px;
  border-top: 1px solid var(--line, rgba(var(--ink-rgb),0.08)); padding-top: 8px; }
.rt-row { display: grid; grid-template-columns: 14px 1fr; gap: 3px 8px; font-size: 0.76rem; padding-bottom: 2px; }
.rt-ic { width: 13px; height: 13px; grid-row: 1 / span 2; align-self: start; margin-top: 2px; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.rt-pass .rt-ic { stroke: var(--green); } .rt-fail .rt-ic { stroke: var(--accent); } .rt-na .rt-ic { stroke: var(--muted); }
.rt-k { color: var(--ink); font-weight: 600; grid-column: 2; }
.rt-n { color: var(--muted); font-size: 0.68rem; grid-column: 2; line-height: 1.35; }
/* benefits / trade-offs — labeled lines, color only on the sign glyph */
.rt-io { display: flex; gap: 8px; align-items: baseline; margin-top: 10px;
  font-size: 0.76rem; color: var(--n-800); line-height: 1.5; }
.rt-io-sign { flex: 0 0 auto; font-weight: 800; font-size: 0.8rem; line-height: 1; margin-top: 1px; }
.rt-io-good .rt-io-sign { color: var(--green); }
.rt-io-bad .rt-io-sign { color: var(--accent); }
.rt-io-lab { font-size: 0.58rem; letter-spacing: 0.08em; text-transform: uppercase;
  font-weight: 700; color: var(--muted); margin-right: 2px; }

/* ── City Layers panel (registry-generated) ── */
/* Superseded by the .city-layers rule further down (the dock rebuild). Only the
   geometry props are kept dead here deliberately — the old `overflow-y: auto`
   was removed because a second overflow declaration on this element is what
   made the scroll-container bug hard to see. */
.city-layers { width: 218px; max-height: 46vh; padding: 10px 12px 8px; }
.cl-group { border-top: 1px solid rgba(var(--ink-rgb),0.07); }
.cl-group:first-of-type { border-top: 0; }
.cl-group summary {
  cursor: pointer; list-style: none; outline: none;
  font-size: 0.66rem; letter-spacing: 0.08em; text-transform: uppercase;
  font-weight: 700; color: var(--muted); padding: 7px 0 5px;
  display: flex; align-items: center; gap: 6px;
}
.cl-group summary::-webkit-details-marker { display: none; }
.cl-group summary::before { content: '▸'; font-size: 0.6rem; transition: transform .15s; }
.cl-group[open] summary::before { transform: rotate(90deg); }
.cl-item { display: flex; align-items: center; gap: 8px; padding: 4px 0 5px 4px; cursor: pointer; user-select: none; }
.cl-item input { accent-color: var(--c-blue-deep); margin: 0; width: 13px; height: 13px; cursor: pointer; flex: 0 0 auto; }
.cl-sw { width: 12px; height: 12px; border-radius: var(--r-control); flex: 0 0 auto; }
.cl-sw-none { background: rgba(var(--ink-rgb),0.08); }
.cl-tx { font-size: 0.74rem; color: var(--ink); line-height: 1.15; }
.cl-tx small { display: block; font-size: 0.6rem; color: var(--muted); margin-top: 1px; }
.cl-legend { display: flex; flex-wrap: wrap; gap: 3px 10px; padding: 2px 0 7px 25px; }
.cl-lg { font-size: 0.62rem; color: var(--muted); display: inline-flex; align-items: center; gap: 5px; }
.cl-lg i { width: 9px; height: 9px; border-radius: var(--r-control); border: 1px solid rgba(var(--ink-rgb),0.12); }

/* ── Address search (top-left, beside the view toggle) ── */
.map-search {
  position: absolute; top: 16px; left: 148px; z-index: 11;
  display: flex; align-items: center; gap: 7px;
  background: rgba(var(--white-rgb),0.97); 
  border: 1px solid rgba(var(--ink-rgb),0.06); border-radius: var(--r-control);
  padding: 5px 6px 5px 11px; width: 264px;
  box-shadow: var(--shadow-1);
  color: var(--muted);
}
.map-search input {
  flex: 1; border: none; background: none; outline: none; min-width: 0;
  font-family: 'Manrope', sans-serif; font-size: 0.78rem; color: var(--ink);
}
.map-search input::placeholder { color: var(--muted); }
.map-search button {
  border: none; background: var(--primary); color: var(--n-000); cursor: pointer;
  width: 24px; height: 24px; border-radius: var(--r-control); font-size: 0.8rem; line-height: 1;
  flex: 0 0 auto;
}
.map-search button:hover { filter: brightness(1.15); }
.map-search input:focus-visible { outline: 2px solid var(--c-blue-deep); outline-offset: 2px; border-radius: var(--r-control); }
@media (max-width: 1100px) { .map-search { width: 200px; } }

/* ── Map / Table mode toggle + accessible parcel table (R1 #5) ── */
.mode-toggle { left: auto; right: 24px; top: 62px; }
.table-view[hidden] { display: none; }   /* `display:flex` below would defeat [hidden] */
.table-view {
  position: absolute; inset: 16px 24px 16px 24px; z-index: 18;
  background: rgba(var(--white-rgb),0.99);
  border: 1px solid rgba(var(--ink-rgb),0.08); border-radius: var(--r-panel);
  display: flex; flex-direction: column; overflow: hidden;
  box-shadow: var(--shadow-2);
}
.tv-bar { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid rgba(var(--ink-rgb),0.08); }
.tv-bar input {
  flex: 0 1 320px; font-family: 'Manrope', sans-serif; font-size: 0.82rem;
  padding: 7px 12px; border: 1px solid rgba(var(--ink-rgb),0.14); border-radius: var(--r-control); outline: none; color: var(--ink);
}
.tv-bar input:focus-visible { outline: 2px solid var(--c-blue-deep); outline-offset: 1px; }
.tv-count { font-size: 0.72rem; color: var(--muted); }
.tv-scroll { flex: 1; overflow: auto; }
.tv-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.tv-table thead th {
  position: sticky; top: 0; background: var(--n-050); z-index: 1;
  text-align: left; border-bottom: 1px solid rgba(var(--ink-rgb),0.12); padding: 0;
}
.tv-table thead button {
  width: 100%; text-align: left; padding: 9px 12px; border: none; background: none; cursor: pointer;
  font-family: 'Manrope', sans-serif; font-size: 0.66rem; letter-spacing: 0.08em;
  text-transform: uppercase; font-weight: 700; color: var(--muted);
}
.tv-table thead button:hover { color: var(--ink); }
.tv-table thead button:focus-visible { outline: 2px solid var(--c-blue-deep); outline-offset: -2px; }
.tv-table tbody td { padding: 7px 12px; border-bottom: 1px solid rgba(var(--ink-rgb),0.05); color: var(--ink); }
.tv-table tbody tr { cursor: pointer; }
.tv-table tbody tr:hover { background: rgba(var(--blue-deep-rgb),0.06); }
.tv-table tbody tr:focus-visible { outline: 2px solid var(--c-blue-deep); outline-offset: -2px; background: rgba(var(--blue-deep-rgb),0.06); }
.tv-num { font-variant-numeric: tabular-nums; text-align: right; }
.tv-elig { color: var(--c-green-ok); font-weight: 700; text-align: center; }
.tv-foot { padding: 9px 16px; font-size: 0.68rem; color: var(--muted); border-top: 1px solid rgba(var(--ink-rgb),0.08); }

/* ── Share link on the parcel card (R1 #6 deep links) ── */
.pc-share {
  margin-top: 7px; padding: 3px 0; border: none; background: none; cursor: pointer;
  font-family: 'Manrope', sans-serif; font-size: 0.68rem; font-weight: 600;
  color: var(--green); text-align: left;
}
.pc-share:hover { text-decoration: underline; }
.pc-share:focus-visible { outline: 2px solid var(--c-blue-deep); outline-offset: 2px; border-radius: var(--r-control); }

/* setback consequence note (cofounder r3: 10 ft gap + roof greenspace) */
.sb-note { margin-top: 7px; font-size: 0.72rem; color: var(--n-800); line-height: 1.5;
  padding: 8px 11px; background: rgba(var(--amber-rgb),0.07); border-left: 3px solid var(--c-amber);
  border-radius: 0 var(--r-control) var(--r-control) 0; }
.sb-note b { color: var(--c-amber-deep); }

/* r3 corrections: ordinance version, capacity note, model-generated alert tag */
.ord-ver { margin-left: 12px; font-size: 0.66rem; letter-spacing: 0.05em; color: var(--muted);
  border-left: 1px solid rgba(var(--ink-rgb),0.15); padding-left: 12px; }
.cap-note { margin: 6px 0 8px; font-size: 0.66rem; color: var(--muted); font-style: italic; }
.redev-mg { margin-left: auto; font-size: 0.56rem; letter-spacing: 0.06em; text-transform: uppercase;
  font-weight: 700; color: var(--muted); background: rgba(var(--ink-rgb),0.06); padding: 2px 7px; border-radius: var(--r-control); }
.redev-rule { margin-top: 7px; font-size: 0.66rem; color: var(--muted); font-style: italic; }

/* ── Cambridge 360 light rail (mockup shell) ── */
.sidebar.rail { background: var(--n-000); border-right: 1px solid var(--n-150); color: var(--n-900); }
.rail-brand { display: flex; gap: 10px; align-items: center; padding: 16px 16px 12px; border-bottom: 1px solid var(--n-100); }
/* THE REAL MARK, not an approximation of it (2026-08-16). This was a
   conic-gradient ring — amber -> teal -> blue behind a radial mask — that had
   been standing in for the actual City 360 logo: a three-node network glyph
   inside concentric orbital arcs. The gradient was the last piece of chrome
   still carrying the retired teal, so replacing it and recolouring it were the
   same job; it got replaced.
   The only logo files in the repo were WHITE-on-transparent
   (next/public/city360/logo/*-white.png), which is invisible on this light
   header — that is why they could never simply be dropped in. The ink file is
   a COLOURWAY of that same artwork, not a redraw: the original alpha channel
   is untouched and only RGB was repainted to Atlas ink #393f44. The square
   mark was cropped out of the portrait lockup at the natural gap between the
   glyph and the "CITY 360" wordmark (alpha rows 762-845), then centred.

   AND THEN THE MARK ITSELF WAS REJECTED (2026-08-18). All of the above was
   careful work on the wrong artwork. The stakeholder read it at the size this
   header actually paints and said: "your logo looks quite weak (and small),
   compared to others… try for something more wheel like with strong
   intersecting but simplified circles". He was describing a measurable
   property, not a taste: that glyph is a nest of concentric HAIRLINE arcs
   around three outlined nodes, drawn 738px wide, and at 30px every one of
   those strokes lands under a pixel and silts up into grey. The replacement
   is two thick intersecting arcs with the 360 set inside them and one solid
   dot — four shapes, all of which survive 30px. It shipped on the sister site
   www.360citi.org on 2026-08-21 and this is the dashboard catching up to it.
   Same relationship as before between the pair: citi360-mark.png and
   citi360-mark-white.png are 203x195 with identical alpha (9,922 opaque
   pixels each), so the white one is a repaint and not a second drawing.
   THE BOX DOES NOT CHANGE. This base rule's 26px never applies on this page —
   `.hdr360 .rail-orbit` overrides it to 30px and that override is what paints
   — and neither number moves. Both are `contain`, so a 203x195 mark
   letterboxes to 30x28.8 inside the same 30x30 the 738x738 square one
   occupied, and no row reflows. Measured 2026-09-10: still 30x30.

   MARK ONLY, never the lockup: the header is a 58px horizontal strip and the
   canonical lockup is portrait — mark stacked over wordmark over a ruled
   place line. Do not force the portrait one in here. The two lines of text to
   the right ARE this page's lockup, assembled the way City360Lockup.tsx
   assembles its own and for its reasons: the mark is the image, the words are
   real text, so they stay crisp, stay selectable, stay translatable, and the
   NEXT mark change is one url() rather than a re-export.
   Painted as a background-image rather than an <img> because it is decorative
   and because a background cannot reflow: the header has close to zero slack
   at 1440, and a swapped <img> with a different intrinsic aspect would move
   the row on its own. A background of a fixed box cannot, whatever lands in
   it — which is why this line is safe to change and .rail-prod below is not.
   The box keeps its exact former size, so this swap costs the header nothing.
   (The header's no-clip floor DOES move in this change, by 22px, but that is
   the 11px type floor paying for itself two rules down — not this url(). The
   number this comment used to quote, 1395, was stale: measured 1375 before
   the change on 2026-09-10 and 1397 after.)
   url() resolves against this stylesheet, so it also works for the
   prototypes/ pages that load cambridge.css from a subdirectory. */
.rail-orbit { width: 26px; height: 26px; flex: 0 0 auto;
  background: url('assets/logo/citi360-mark.png?v=7731b890') center / contain no-repeat; }

/* THE LETTERSPACING IS THE LOCKUP. City360Lockup.tsx says it outright and it
   is the one thing that must survive the port to a header strip: "360CITI" is
   set at 0.22em and the place wider still, and that is what makes two words
   read as a mark instead of a heading that happens to be bold. 0.05em — what
   was here — is just a slightly airy label.
   ADAPTED, NOT COPIED. The sister lockup is stacked, so it can drop the place
   to 0.62em of the wordmark's size and buy its 0.42em tracking back in width.
   On one horizontal line there is no smaller size to spend: 0.62em of 11.5px
   is 7px, which is below this design system's floor and unreadable besides.
   So the place stays at the wordmark's size and the hierarchy moves onto
   WEIGHT (800 vs 600) with tracking a step wider than the wordmark's. Same
   two-part relationship, one line instead of three.

   The middot replaces the ruled line the stacked lockup draws either side of
   the place. It zeroes its own tracking — the wordmark's 0.22em would land
   between the dot and the space after it and push the dot left of centre —
   and takes its gaps from the real spaces around it in the markup rather than
   from margins. Those spaces are load-bearing: the dot is `aria-hidden`, and
   with the spans butted together the accessible name concatenates to
   "360CITICambridge". With them it reads "360CITI Cambridge".
   The place is uppercased in CSS and sentence-case in the markup, which is
   how .rail-prod below has always worked: the DOM keeps the word a reader can
   copy and a screen reader can pronounce, and the caps are presentation. */
.rail-wm { font-weight: 800; font-size: 0.72rem; letter-spacing: 0.22em; color: var(--n-900); }
.rail-wm-dot { letter-spacing: 0; color: var(--n-500); font-weight: 600; }
.rail-wm-place { font-weight: 600; letter-spacing: 0.34em; text-transform: uppercase; }

/* 11px, WRITTEN IN PX ON PURPOSE — the one value in this block that is a floor
   and not a ratio. DESIGN.md: "captions never below 11px and never for
   load-bearing content". This line was 0.6rem = 9.60px, which broke the floor
   by 1.4px while carrying the PRODUCT NAME — the most load-bearing three
   words on the page after the city's. In rem it would be 0.6875rem, correct
   only for as long as nobody touches the root size; a floor that moves when
   something else moves is not a floor, so it says 11px.

   THE 1.4px COSTS 22px OF HEADER, and that is the honest trade. Measured at
   1440 on 2026-09-10: 27 uppercase characters need 189.8px at 11px where they
   needed 167.8px at 9.6px, so the brand block goes 207.8 -> 229.8px. It comes
   out of .map-search, which is `flex: 0 1 320px` and was already rendering at
   270px; it shrinks to 260 and its placeholder still fits. Nothing clips. The
   header's no-clip floor moves 1375 -> 1397px, still 43px inside the 1440
   this shell is designed at. Part of the width budget this header has been
   enjoying was borrowed from the defect.
   Tracking is 0.06em rather than the 0.12em DESIGN.md gives 11px section
   labels, because the other half of that 0.12em is another 18px of header and
   this is the second line of a lockup, not a section label. If the header
   ever does need those 22px back, take them from this line's SIZE only over
   somebody's explicit objection: 9.6px is what the review was looking at. */
.rail-prod { font-size: 11px; color: var(--n-500); letter-spacing: 0.06em; text-transform: uppercase; margin-top: 2px; }
.rail-scroll { flex: 1; overflow-y: auto; padding: 10px 12px; }
.rail-h { font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 700;
  color: var(--n-500); margin: 12px 4px 4px; }
.rail-h small { text-transform: none; letter-spacing: 0; font-weight: 400; }
.rail-h:first-child { margin-top: 2px; }
.rail-item { display: block; width: 100%; text-align: left; padding: 7px 10px; border: none;
  background: none; border-radius: var(--r-control); font-family: 'Manrope', sans-serif; font-size: 0.78rem;
  color: var(--n-800); cursor: pointer; }
.rail-item:hover { background: var(--n-050); }
.rail-item.active { background: var(--c-chrome-wash); color: var(--c-chrome); font-weight: 700; }
.rail-src { padding: 4px 4px 0; }
.rail-src-row { display: flex; justify-content: space-between; align-items: baseline;
  padding: 4px 6px; font-size: 0.68rem; color: var(--n-800); border-bottom: 1px solid var(--n-100); }
.rail-src-row small { color: var(--n-500); font-size: 0.6rem; }
.rail-src-note { font-size: 0.6rem; color: var(--n-500); padding: 6px; }
.rail-foot { color: var(--n-500) !important; border-top: 1px solid var(--n-100) !important; font-size: 0.62rem !important; }

/* header tabs */
.tabs360 { display: inline-flex; gap: 4px; margin-right: 10px; }
.t360 { padding: 6px 4px 8px; font-size: 0.8rem; font-weight: 500; color: var(--n-600);
  position: relative; }
.t360.on { color: var(--n-900); font-weight: 700; }
.t360.on::after { content: ''; position: absolute; left: 4px; right: 4px; bottom: 0;
  height: 2px; border-radius: var(--r-control); background: var(--c-chrome); }
/* .t360.off is gone (2026-09-12). Its last wearers were the Projects items,
   and Projects left every header this day; neither reports.html nor the
   explorer prototype links this stylesheet, so nothing else could use it. */
.t360[data-view] { cursor: pointer; }
.t360[data-view]:hover { color: var(--n-900); }
.t360[data-view]:focus-visible { outline: 2px solid var(--c-blue-deep); outline-offset: 2px; border-radius: var(--r-control); }
.cp-tab[hidden] { display: none; }

/* ═══════════════════════════════════════════════
   CAMBRIDGE 360 v2 — mockup-parity shell
   header brand + search · icon rail · right cpanel
   ═══════════════════════════════════════════════ */

/* ── Fixed full-width header, at every width (2026-09-12) ─────────────────
   One 58px row from 1150px up. Below that the header is as tall as its rows,
   and nothing under it assumes a number: the ResizeObserver right after the
   header in cambridge.html publishes --hdr-h, which .main and the rail read.
   Fixed rather than sticky at every tier, because an in-flow sticky header
   plus a padding-top of its own height would count the header twice. */
.hdr360 { position: fixed; top: 0; left: 0; right: 0; height: 58px; z-index: 220;
  background: var(--n-000); border-bottom: 1px solid var(--n-150); display: flex; align-items: center;
  gap: var(--gutter); padding: 0 var(--edge); }
/* a.btn-donate sets display, which would otherwise defeat the hidden attribute */
.hdr360 [hidden] { display: none !important; }
.hdr-brand { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
/* 30px is the SHIPPED size of the brand box — the base rule's 26px never
   applies on this page. Kept to the pixel so the header does not reflow. The
   radial mask that used to punch the gradient ring's hole is gone: the real
   mark has a solid hub circle at its centre, and masking would delete it. */
.hdr360 .rail-orbit { width: 30px; height: 30px; }
.hdr-sep { width: 1px; height: 30px; background: var(--n-150); margin: 0 4px; }
.hdr-prod-name { font-weight: 800; font-size: 0.86rem; color: var(--n-900); letter-spacing: 0.01em; }
.hdr-prodwrap .rail-prod { margin-top: 1px; }
@media (max-width: 1280px) { .hdr-sep, .hdr-prodwrap { display: none; } }

/* search moves into the header. It takes what the row has left, up to 420px,
   and never less than 240px, which still shows its whole placeholder. */
.hdr360 .map-search { position: static; flex: 0 1 420px; min-width: 240px; width: auto; margin: 0;
  box-shadow: none; background: var(--n-050); border-color: var(--n-150); }
/* The cluster (tabs, and Donate when it exists) is pushed to the right edge;
   Join follows it as the header's last item. */
.hdr360 .toolbar-right { flex: 0 0 auto; gap: 8px; margin-left: auto; }
/* r4-D7: Donate is a secondary outline — amber is reserved for alerts. It is a
   link to the website's donate page, so it needs the box a <button> gets free. */
.btn-donate { background: none; border: 1px solid var(--n-150); color: var(--n-900); }
.btn-donate:hover { background: var(--n-050); opacity: 1; }
.hdr360 .btn-donate { display: inline-flex; align-items: center; text-decoration: none; }
/* Join is .btn-demo: the header's one filled control, chrome with
   surface-coloured text (white in light; see ON-CHROME TEXT for dark). */
.hdr360 .btn-join { flex: 0 0 auto; }
.hdr360 .btn-demo:focus-visible, .hdr-menu-btn:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; }
/* Menu exists only at ≤900px, where it opens .toolbar-right as a sheet. Same
   chrome as Controls beside it; open, it takes the rail's active-item wash. */
.hdr-menu-btn { display: none; align-items: center; justify-content: center; gap: 7px; flex: 0 0 auto;
  min-height: 44px; min-width: 44px; padding: 0 12px; border: 1px solid var(--n-150);
  border-radius: var(--r-control); background: var(--n-000); color: var(--n-900); cursor: pointer;
  font-family: 'Manrope', sans-serif; font-size: 0.875rem; font-weight: 700; }
.hdr-menu-btn:hover { background: var(--n-025); border-color: var(--n-200); }
.hdr-menu-btn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.hdr360.is-menu-open .hdr-menu-btn { background: var(--c-chrome-wash); color: var(--c-chrome); }

/* ── Rail and content sit under the header, however tall it is (--hdr-h) ── */
.sidebar.rail { top: var(--hdr-h); }
.main { padding-top: var(--hdr-h); }
/* ≤1400px: the strapline is the first thing to go. The wordmark already names
   the product, and at 1280 the row needs the width more than the lockup needs
   its second line. (It went at ≤560 before 2026-09-12.) */
@media (max-width: 1400px) { .hdr-brand .rail-prod { display: none; } }
/* Below 1300px, when Donate is rendered, Controls goes icon-only. Measured at
   1150 with Donate: the one row was 33px wider than the window, and the word
   "Controls" is about 60px. Without Donate the label fits at every one-row
   width, so it stays. The button's aria-label keeps its name. has-donate is
   set by setupHeaderAccount() in cambridge.js. */
@media (max-width: 1299.98px) { .hdr360.has-donate .rail-toggle .rt-label { display: none; } }

.rail-analyze { margin: 12px 12px 2px; padding: 10px 12px; background: var(--n-050);
  border: 1px solid var(--n-150); border-radius: var(--r-panel); }
.ra-label { font-size: 0.66rem; letter-spacing: 0.1em; text-transform: uppercase;
  font-weight: 800; color: var(--n-900); }
.rail-analyze .ra-sub { display: block; margin-top: 3px; font-size: 0.62rem; color: var(--n-600); }

/* rail items get icons */
.rail-item { display: flex; align-items: center; gap: 9px; }
.rail-item svg { width: 15px; height: 15px; flex: 0 0 auto; fill: var(--n-500); }
.rail-item.active svg { fill: var(--c-chrome); }
.rail-item span { flex: 1; }
/* .rail-item.soon and its "In design" toast went 2026-09-12: no rail item has
   carried the class, so the rules and the branch in setupRail() were dead. */

/* ── Content row: map + right contextual panel ── */
.content-row { display: flex; flex: 1; min-height: 0; }
.content-row .map-canvas { margin: 14px 14px 18px 20px; }

.cpanel { width: var(--panel-w); flex: 0 0 auto; margin: 14px 20px 18px 0;
  background: var(--n-000); border: 1px solid var(--n-150); border-radius: var(--r-panel);
  display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--shadow-md); }
.cp-tabs { display: flex; align-items: center; gap: 2px; padding: 8px 10px;
  border-bottom: 1px solid var(--n-100); flex: 0 0 auto; }
.cp-tab { border: none; background: none; padding: 6px 12px; border-radius: var(--r-control);
  font-family: 'Manrope', sans-serif; font-size: 0.76rem; font-weight: 700;
  color: var(--n-600); cursor: pointer; }
.cp-tab:hover { background: var(--n-050); }
.cp-tab.active { background: rgba(var(--chrome-rgb),0.1); color: var(--c-chrome); }
.cp-collapse { margin-left: auto; border: none; background: none; color: var(--n-500);
  font-size: 0.9rem; cursor: pointer; padding: 4px 8px; border-radius: var(--r-control); }
.cp-collapse:hover { background: var(--n-050); color: var(--n-900); }
.cp-body { flex: 1; min-height: 0; overflow-y: auto; }
.cp-pane { display: none; padding: 14px 16px 18px; }
.cp-pane.active { display: block; }
body.cp-hidden .cpanel { display: none; }

.cp-open { position: absolute; top: 18px; right: 18px; z-index: 12;
  border: 1px solid var(--n-150); background: rgba(var(--white-rgb),0.94); color: var(--n-900);
  font-family: 'Manrope', sans-serif; font-size: 0.72rem; font-weight: 700;
  padding: 7px 12px; border-radius: var(--r-pill); cursor: pointer;
  box-shadow: var(--shadow-1); }
.cp-open:hover { background: var(--n-000); }

/* section heads inside panes */
.cp-h { font-size: 0.64rem; letter-spacing: 0.12em; text-transform: uppercase;
  font-weight: 800; color: var(--n-600); margin: 2px 2px 10px; }
.cp-h-gap { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--n-100); }

/* map alert (model-generated, with rule) */
.cp-alert { background: rgba(var(--amber-rgb),0.08); border: 1px solid rgba(var(--amber-rgb),0.3);
  border-radius: var(--r-panel); padding: 11px 12px; margin-bottom: 12px; }
.cp-alert-head { display: flex; align-items: center; gap: 8px; font-size: 0.74rem;
  font-weight: 800; color: var(--c-amber-deep); }
.cp-alert-body { font-size: 0.74rem; line-height: 1.5; color: var(--n-800); margin-top: 6px; }
.cp-alert-rule { font-size: 0.62rem; font-style: italic; color: var(--c-amber-deep); margin-top: 6px; }
.cp-alert-go { margin-top: 8px; border: none; background: var(--c-amber); color: var(--n-000);
  font-family: 'Manrope', sans-serif; font-size: 0.7rem; font-weight: 700;
  padding: 6px 11px; border-radius: var(--r-control); cursor: pointer; }
.cp-alert-go:hover { opacity: 0.88; }

/* news rows with initials chips */
.cp-news { display: flex; gap: 10px; align-items: flex-start; padding: 9px 6px;
  border-bottom: 1px solid var(--n-100); text-decoration: none; border-radius: var(--r-control); }
.cp-news:hover { background: var(--n-025); }
.cp-chip { width: 30px; height: 30px; border-radius: var(--r-control); flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  font-size: 0.62rem; font-weight: 800; color: var(--n-000); letter-spacing: 0.02em; }
.cp-chip-cc { background: var(--c-blue); } .cp-chip-pd { background: var(--c-chrome); }
.cp-chip-cd { background: var(--c-risk); } .cp-chip-rt { background: var(--c-amber); }
.cp-news-text { min-width: 0; }
.cp-news-text strong { display: block; font-size: 0.74rem; line-height: 1.35;
  color: var(--n-900); font-weight: 700; }
.cp-news-text small { display: block; font-size: 0.62rem; color: var(--n-500); margin-top: 2px; }

.cp-weekly { display: block; width: 100%; margin-top: 10px; padding: 9px 12px;
  border: 1px dashed var(--n-250); background: var(--n-025); border-radius: var(--r-control);
  font-family: 'Manrope', sans-serif; font-size: 0.7rem; font-weight: 600;
  color: var(--n-800); cursor: pointer; text-align: left; }
.cp-weekly:hover { border-color: var(--c-chrome); color: var(--c-chrome); }
/* The sale report and the About correction and accessibility routes are links
   to www.360citi.org/contact now (2026-09-12), in the same dashed box. */
a.cp-weekly { text-decoration: none; }

/* honest metric cards — value · label · scope note, no sparklines */
.cp-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.cpm { border: 1px solid var(--n-150); border-radius: var(--r-panel); padding: 10px 11px;
  display: flex; flex-direction: column; gap: 2px; }
.cpm-v { font-size: 1.22rem; font-weight: 800; letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums; }
.cpm-teal { color: var(--c-teal-deep); } .cpm-blue { color: var(--c-blue); }
.cpm-amber { color: var(--c-amber-deep); } .cpm-risk { color: var(--c-risk); font-size: 1.02rem; }
.cpm-l { font-size: 0.68rem; font-weight: 700; color: var(--n-900); }
.cpm-s { font-size: 0.6rem; color: var(--n-500); line-height: 1.35; }
.cp-mnote { font-size: 0.62rem; color: var(--n-500); font-style: italic; margin-top: 8px; }

/* parcel pane: hint becomes the empty state, card fills the pane */
.cpanel .map-hint { position: static; width: auto; box-shadow: none;
  backdrop-filter: none; background: var(--n-025); border: 1px dashed var(--n-250); }
.cpanel .parcel-card { position: static; width: auto; max-height: none;
  box-shadow: none; border: none; border-radius: 0; backdrop-filter: none;
  background: none; animation: none; overflow: visible; }
.cpanel .pc-content { max-height: none; overflow-y: visible; }
.cpanel .pc-header { padding-left: 2px; padding-top: 4px; }
.cpanel .pc-close { top: 0; right: 0; }

/* finding pane */
.cp-find { font-size: 0.8rem; line-height: 1.6; color: var(--n-800); }
.cp-find p { margin: 0 0 10px; }
.cp-find ul { padding-left: 18px; margin: 10px 0; }
.cp-find-close { font-weight: 700; color: var(--c-teal-deep); }
.cp-find-foot { font-size: 0.62rem; color: var(--n-500); border-top: 1px solid var(--n-100);
  padding-top: 10px; margin-top: 12px; }

/* ≤1150px the panel is 300px: --panel-w in the :root tier block. */

/* ═══ Layer dock v2 — scroll list · eye · lock · chips · minimize (r2) ═══ */
/* overflow:clip, NOT hidden. `hidden` makes this box a scroll CONTAINER: it
   shows no scrollbar, but the browser still scrolls it to reveal a focused
   descendant. Clicking a layer row low in the list therefore scrolled the whole
   panel — the header slid out of view and an empty region opened below the
   footer, with no scrollbar to get back. That is the "panel goes blank when I
   turn a layer on" report (measured: clicking the last row set scrollTop to 931
   and opened a 932px void). `clip` is not a scroll container, so focus-reveal
   cannot move it; .cl-list below still scrolls normally and every row stays
   reachable. */
.city-layers { width: 252px; max-height: min(64vh, calc(100vh - 230px));
  display: flex; flex-direction: column; padding: 0; overflow: clip; }
.cl-head { display: flex; align-items: center; justify-content: space-between;
  padding: 9px 12px 7px; border-bottom: 1px solid var(--n-100); flex: 0 0 auto; }
.cl-head .legend-title { margin: 0; }
.cl-minbtn { border: none; background: var(--n-050); color: var(--n-600); width: 22px; height: 22px;
  border-radius: var(--r-control); cursor: pointer; font-size: 0.95rem; line-height: 1; }
.cl-minbtn:hover { background: var(--n-150); color: var(--n-900); }
.city-layers.cl-minimized { max-height: none; }
.city-layers.cl-minimized .cl-body { display: none; }
.cl-body { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.cl-chips { display: flex; flex-wrap: wrap; gap: 4px; padding: 8px 10px 2px; flex: 0 0 auto; }
.cl-chip { border: 1px solid var(--n-150); background: var(--n-000); color: var(--n-600); font-size: 0.6rem;
  font-weight: 700; padding: 3px 8px; border-radius: var(--r-pill); cursor: pointer;
  font-family: 'Manrope', sans-serif; }
.cl-chip.active { background: rgba(var(--chrome-rgb),0.1); border-color: var(--c-chrome); color: var(--c-chrome); }
.cl-tools { display: flex; gap: 5px; padding: 7px 10px; flex: 0 0 auto; }
.cl-bulk { border: 1px solid var(--n-150); background: var(--n-000); color: var(--n-800); font-size: 0.62rem;
  font-weight: 700; padding: 4px 8px; border-radius: var(--r-control); cursor: pointer;
  font-family: 'Manrope', sans-serif; flex: 0 0 auto; }
.cl-bulk:hover { background: var(--n-050); }
.cl-search { flex: 1; min-width: 0; border: 1px solid var(--n-150); border-radius: var(--r-control);
  padding: 4px 8px; font-family: 'Manrope', sans-serif; font-size: 0.66rem; color: var(--ink);
  background: var(--n-025); outline: none; }
.cl-search:focus { border-color: var(--c-chrome); }
/* ── SUN SHADOWS · the GPU-shade kill switch ────────────────────────────────
   Above .cl-list rather than inside it, because it is not a layer: it is a
   property of the light, and applyPreset() clears every row inside the list on
   topic entry. A wash + top/bottom hairline separates it from the rows without
   giving it a card of its own — it is a setting, not a fifth chip group.
   No raw hex below: every value is a :root token with a fallback, per the
   file's discipline. */
.cl-sun { flex: 0 0 auto; padding: 7px 12px 8px; background: var(--n-025, #f7f9fb);
  border-top: 1px solid var(--n-100, #eef2f6); border-bottom: 1px solid var(--n-100, #eef2f6); }
.cl-sun-t { display: flex; align-items: flex-start; gap: 7px; cursor: pointer;
  font-size: 0.7rem; font-weight: 700; color: var(--n-800, #3d4854); }
.cl-sun-t input { margin: 2px 0 0; flex: 0 0 auto; accent-color: var(--c-chrome); }
.cl-sun-t small { display: block; font-weight: 500; font-size: 0.58rem;
  color: var(--n-500, #8593a2); line-height: 1.35; }
/* The live state line. Reads as running commentary, not as a warning — the
   three reasons it prints (off, sun down, below zoom 16) are all normal. */
.cl-sun-note { margin: 5px 0 0 23px; font-size: 0.57rem; line-height: 1.4;
  color: var(--n-600, #586b7d); }
/* The standing caveat sits under the live line at lower contrast, the same
   relationship the .cl-legend rows have to their .cl-row above them. `overstates`
   is bolded in the markup and must stay legible at this size. */
.cl-sun-caveat { color: var(--n-550, #7a8794); }
.cl-sun-caveat b { color: var(--n-800, #3d4854); font-weight: 700; }

.cl-list { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 8px 4px; }
.cl-row { display: flex; align-items: center; gap: 7px; padding: 5px 2px; border-radius: var(--r-control); }
.cl-row:hover { background: var(--n-025); }
.cl-row.locked { background: rgba(var(--amber-rgb),0.06); }
.cl-eyew { display: flex; cursor: pointer; flex: 0 0 auto; }
.cl-eyew input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.cl-eye { width: 16px; height: 16px; }
.cl-eye .eo, .cl-eye .ec { fill: var(--n-500); }
.cl-eyew input:checked + .cl-eye .eo { fill: var(--c-blue-deep); }
.cl-eyew input:checked + .cl-eye .ec { display: none; }
.cl-eyew input:not(:checked) + .cl-eye .eo { display: none; }
.cl-eyew input:focus-visible + .cl-eye { outline: 2px solid var(--c-blue-deep); border-radius: var(--r-control); }
.cl-row .cl-tx { flex: 1; min-width: 0; }
.cl-lock { border: none; background: none; padding: 2px; cursor: pointer; flex: 0 0 auto;
  border-radius: var(--r-control); }
.cl-lock svg { width: 13px; height: 13px; display: block; }
.cl-lock .lk-open { fill: var(--n-250); } .cl-lock .lk-shut { display: none; fill: var(--c-amber); }
.cl-lock[aria-pressed="true"] .lk-open { display: none; }
.cl-lock[aria-pressed="true"] .lk-shut { display: block; }
.cl-lock:hover { background: var(--n-100); }
.cl-foot { padding: 5px 12px 8px; font-size: 0.56rem; color: var(--n-500); border-top: 1px solid var(--n-100);
  flex: 0 0 auto; }
.cl-legend { padding-left: 31px; }

/* ═══ Project popup — status · units · before/proposed compare ═══ */
/* deck.gl's overlay canvas is inserted into the map container and was painting
   over popups (they render but appear "buried" under the 3D model). Lift the
   whole popup above the canvas + the floating panels. */
.mapboxgl-popup { z-index: 60; }
.mapboxgl-popup-content { font-family: 'Manrope', sans-serif; border-radius: var(--r-panel);
  box-shadow: var(--shadow-2); padding: 14px 14px 12px; }
.pj-pop { width: 264px; }
.pj-addr { font-weight: 800; font-size: 0.88rem; color: var(--n-900); }
/* Two sources naming one lot. Sits directly under the tracker's address so the
   disagreement is read as one fact, not two competing claims. See
   addressConflict() in cambridge.js. */
.pj-addr-alt { font-size: 0.66rem; line-height: 1.45; color: var(--n-600);
  border-left: 2px solid var(--line); padding-left: 7px; margin: 4px 0 6px; }
.pj-addr-alt b { color: var(--n-900); font-weight: 700; }
.pj-sub { font-size: 0.66rem; color: var(--n-500); margin: 2px 0 7px; }
.pj-status { display: inline-block; color: var(--n-000); font-size: 0.6rem; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase; padding: 3px 8px; border-radius: var(--r-control);
  margin-bottom: 8px; }
.pj-cmp, .pj-one { position: relative; width: 100%; height: 150px; border-radius: var(--r-control);
  overflow: hidden; background: var(--n-100); margin-bottom: 8px; }
.pj-cmp img, .pj-one img { position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; }
.pj-top { position: absolute; inset: 0; }
.pj-tag { position: absolute; top: 6px; background: rgba(var(--ink-rgb),0.72); color: var(--n-000);
  font-size: 0.54rem; font-weight: 700; letter-spacing: 0.08em; padding: 2px 6px;
  border-radius: var(--r-control); pointer-events: none; }
.pj-cmp input[type=range] { position: absolute; left: 0; right: 0; bottom: 4px; width: 100%;
  margin: 0; accent-color: var(--n-000); cursor: ew-resize; height: 18px; }
.pj-kv { font-size: 0.74rem; color: var(--n-900); }
.pj-dates { font-size: 0.64rem; color: var(--n-600); margin-top: 3px; }
/* Ordinance era — which zoning regime this site actually belongs to. Stated
   before the photos so a pre-2025 filing can never read as an Ord 2025-1 outcome. */
.pj-era { margin: 7px 0; padding: 6px 8px; border-radius: var(--r-control); font-size: 0.64rem;
  line-height: 1.4; color: var(--n-800); background: var(--n-100); border-left: 3px solid var(--n-250); }
.pj-era b { display: block; font-size: 0.63rem; letter-spacing: .04em;
  text-transform: uppercase; margin-bottom: 2px; }
.pj-era-pre { background: var(--n-100); border-left-color: var(--n-800); }
.pj-era-pre b { color: var(--n-800); }
.pj-era-straddle { background: var(--c-amber-wash); border-left-color: var(--c-amber); }
.pj-era-straddle b { color: var(--c-amber-deep); }
.pj-era-post { background: var(--c-blue-wash); border-left-color: var(--c-blue); }
.pj-era-post b { color: var(--c-blue-deep); }
.pj-era-unknown b { color: var(--n-600); }
.pj-era-ev { display: block; margin-top: 4px; font-size: 0.58rem; color: var(--n-550); }
.pj-parcel { display: block; width: 100%; margin-top: 9px; border: none; background: var(--c-chrome);
  color: var(--n-000); font-family: 'Manrope', sans-serif; font-size: 0.7rem; font-weight: 700;
  padding: 7px 10px; border-radius: var(--r-control); cursor: pointer; }
.pj-parcel:hover { opacity: 0.9; }
.pj-src { font-size: 0.58rem; color: var(--n-500); margin-top: 7px; }
.pj-src a { color: var(--c-blue); }


/* ═══ r4: first-visit orientation bar (D5) + parcel guide state ═══ */
.intro-bar { display: flex; align-items: center; gap: 10px; padding: 7px 20px;
  background: rgba(var(--chrome-rgb),0.08); border-bottom: 1px solid var(--n-150);
  font-size: 0.78rem; color: var(--n-900); flex: 0 0 auto; }
.intro-bar strong { color: var(--c-chrome); }
.intro-bar button { margin-left: auto; border: none; background: none; color: var(--n-500);
  font-size: 1.05rem; cursor: pointer; padding: 2px 8px; border-radius: var(--r-control); }
.intro-bar button:hover { background: var(--n-100); color: var(--n-900); }
.intro-bar[hidden] { display: none; }
.hint-steps { margin: 8px 0 10px; display: flex; flex-direction: column; gap: 7px; }
.hs { display: flex; gap: 9px; font-size: 0.75rem; color: var(--n-800); line-height: 1.45; }
.hs b { flex: 0 0 auto; width: 18px; height: 18px; border-radius: 50%; background: var(--c-chrome);
  color: var(--n-000); font-size: 0.62rem; display: flex; align-items: center; justify-content: center; }


/* ═══ Permit activity + modeled outlook (Updates) ═══ */
.act-bars { display: flex; align-items: flex-end; gap: 2px; height: 64px; }
.act-bar { flex: 1; background: var(--c-teal); border-radius: var(--r-control) var(--r-control) 0 0; min-width: 2px; }
.act-bar.fc { background: rgba(var(--teal-rgb),0.12); border: 1px dashed var(--c-teal); border-bottom: none; }
.act-x { display: flex; justify-content: space-between; font-size: 0.56rem; color: var(--n-500); margin-top: 3px; }


/* ═══ QA round: theme legend · table exit · toggle above table ═══ */
.theme-legend { position: absolute; bottom: 16px; right: 16px; left: auto; top: auto; z-index: 14;
  background: rgba(var(--white-rgb),0.96); border: 1px solid var(--n-150); border-radius: var(--r-panel);
  padding: 8px 11px; max-width: 250px; max-height: 42vh; overflow-y: auto;
  box-shadow: var(--shadow-1);
  display: flex; flex-direction: column; gap: 4px; }

/* Rows flow top-to-bottom then wrap into a new COLUMN, capped at 3 rows tall.
   A 7-entry legend was 7 rows deep and ate the bottom of the map; it is now
   3 rows by 3 columns. Height is bounded by the cap, not by the longest layer. */
.tl-rows { display: grid; grid-auto-flow: column; grid-template-rows: repeat(3, auto);
  gap: 1px var(--s-4); justify-content: start; }
/* One or two entries should not open a second column. */
.tl-group:has(.tl-rows > :nth-child(-n+2):last-child) .tl-rows { grid-auto-flow: row; }
.theme-legend[hidden] { display: none; }

/* ── TYPE, ONE STEP UP (Rocker 2026-08-03: "the legend is too small like that
   in proportion… legibility is the key thing here"). One step, ~9%, applied to
   the whole tl- family at once so the legend keeps its internal proportions —
   bumping only the rows would have made the group headers read as captions of
   the swatches instead of headings over them. The swatch grows with the text
   for the same reason: a symbol that no longer matches its label's weight is
   the thing she was actually pointing at. Sizes stay on the file's ladder
   (0.55 / 0.58 / 0.6 / 0.63 / 0.66 / 0.68 / 0.72). The bottom strip's height
   caps are raised by the same ~9% — see the L3 legend-strip rules near the end
   of this file, or the strip starts clipping rows it used to show. ── */
.tl-title { font-size: 0.66rem; letter-spacing: 0.1em;
  font-weight: 800; color: var(--n-600); margin-bottom: 2px; }
.tl-row { display: inline-flex; align-items: center; gap: 6px; font-size: 0.72rem; color: var(--n-800); }
.tl-row i { width: 13px; height: 13px; border-radius: var(--r-control); border: 1px solid rgba(var(--ink-rgb),0.12); flex: 0 0 auto; }

/* ── LEGEND GROUPS (B4). One group per active layer, so every symbol on the
   map is decoded and it is obvious which layer owns which swatch. ── */
.tl-title .tl-count { font-weight: 700; color: var(--n-400); margin-left: var(--s-2);
  letter-spacing: 0.04em; font-variant-numeric: tabular-nums lining-nums; }
.tl-groups { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-5); align-items: flex-start; }
.tl-group { display: flex; flex-direction: column; gap: 2px; min-width: 0;
  padding-left: var(--s-3); border-left: 1px solid var(--n-150); }
.tl-group:first-child { padding-left: 0; border-left: none; }
.tl-gname { font-size: 0.63rem; letter-spacing: 0.06em;
  font-weight: 800; color: var(--n-900); margin-bottom: 1px; }
.tl-gsub { font-size: 0.6rem; color: var(--n-400); font-style: italic; max-width: 240px;
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
.tl-group .tl-chip { align-self: flex-start; margin-top: 2px; }
.mode-toggle { z-index: 45; }
.tv-close { margin-left: auto; border: 1px solid var(--n-150); background: var(--n-000); color: var(--n-900);
  font-family: 'Manrope', sans-serif; font-size: 0.72rem; font-weight: 700;
  padding: 6px 12px; border-radius: var(--r-control); cursor: pointer; }
.tv-close:hover { background: var(--n-050); }


/* ═══ D1 parcel popup + D2 info tab ═══ */
.parcel-pop .mapboxgl-popup-content { padding: 12px 13px 11px; border-radius: var(--r-panel); }
.pp-addr { font-weight: 800; font-size: 0.86rem; color: var(--n-900); }
.pp-sub { font-size: 0.66rem; color: var(--n-500); margin: 2px 0 8px; }
.pp-tax { display: flex; justify-content: space-between; align-items: baseline; font-size: 0.72rem; color: var(--n-600); }
.pp-tax strong { color: var(--c-teal-deep); font-size: 0.82rem; font-variant-numeric: tabular-nums; }
.pp-verdict { margin-top: 7px; font-size: 0.64rem; font-weight: 700; padding: 4px 8px; border-radius: var(--r-control); line-height: 1.3; }
.pp-ok { background: rgba(var(--teal-rgb),0.1); color: var(--c-teal-deep); }
.pp-warn { background: rgba(var(--risk-rgb),0.1); color: var(--c-risk-deep); }
.pp-full { display: block; width: 100%; margin-top: 9px; border: none; background: var(--c-chrome);
  color: var(--n-000); font-family: 'Manrope', sans-serif; font-size: 0.72rem; font-weight: 700;
  padding: 7px 10px; border-radius: var(--r-control); cursor: pointer; }
.pp-full:hover { opacity: 0.9; }
.inf-permits { margin-top: 10px; }
.inf-load, .inf-empty { font-size: 0.72rem; color: var(--n-500); font-style: italic; padding: 4px 0; }
.inf-plabel { font-size: 0.6rem; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 800;
  color: var(--n-600); margin-bottom: 6px; display: flex; justify-content: space-between; }
.inf-plabel span { letter-spacing: 0; text-transform: none; font-weight: 400; color: var(--n-500); }
.inf-permit { border-left: 2px solid var(--n-150); padding: 4px 0 5px 9px; margin-bottom: 5px; }
.inf-prow { display: flex; justify-content: space-between; font-size: 0.74rem; color: var(--n-900); }
.inf-prow strong { font-weight: 700; }
.inf-prow span { color: var(--n-500); font-size: 0.66rem; }
.inf-pmeta { font-size: 0.64rem; color: var(--n-600); margin-top: 1px; }

.pc-share + .pc-share { margin-left: 10px; }
a.pc-share { text-decoration: none; display: inline-block; }

/* ═══════════════════════════════════════════════════════════════════════════
   TESTED FINDINGS (findings.js) — hero F3 + supporting ledger, provenance
   chips, methodology, Projects locked state. Palette matches the civic idioms
   already in this file (teal var(--c-teal) / dark-teal var(--c-teal-deep) · ink var(--n-900) · muted
   var(--n-500)/var(--n-600) · border var(--n-150) · hairline var(--n-100)).
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── provenance chips — the four-tier legend; `tested` is our new tier ── */
.prov-chip { display: inline-block; font-size: 0.52rem; font-weight: 800; letter-spacing: 0.09em;
  text-transform: uppercase; padding: 2px 6px; border-radius: var(--r-control); line-height: 1.4;
  vertical-align: middle; white-space: nowrap; border: 1px solid transparent; }
.prov-observed { color: var(--n-600); background: rgba(var(--ink-rgb),0.06); }
.prov-tested   { color: var(--c-teal-deep); background: rgba(var(--teal-rgb),0.09); border-color: rgba(var(--teal-rgb),0.55); }
.prov-modeled  { color: var(--c-amber-deep); background: rgba(var(--amber-rgb),0.12); }
.prov-illus    { color: var(--n-600); background: var(--n-050); border: 1px dashed var(--n-250); }

/* ── findings root / loading ── */
.findings-root { }
.fnd-loading { font-size: 0.72rem; color: var(--n-500); font-style: italic; padding: 8px 2px; }

/* ── hero (F3) ── */
.fnd-hero { }
.fnd-kicker { display: flex; align-items: center; gap: 8px; margin: 2px 0 9px; }
.fnd-kicker-txt { font-size: 0.58rem; font-weight: 800; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--n-500); }
.fnd-hero-head { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400;
  font-size: 1.5rem; line-height: 1.12; letter-spacing: 0.005em; color: var(--n-900); margin: 0 0 6px; }
.fnd-hero-sub { font-size: 0.72rem; color: var(--n-600); line-height: 1.45; margin-bottom: 12px; }

/* ── hero chart — axes + zero baseline, single teal hue, no decorative sparkline ── */
.fnd-chart { margin: 2px 0 10px; }
.fnd-chart-head { font-size: 0.56rem; letter-spacing: 0.08em;
  font-weight: 700; color: var(--n-500); margin-bottom: 14px; }
.fnd-plot { position: relative; height: 132px; padding-top: 18px; box-sizing: border-box;
  border-bottom: 1.5px solid var(--n-200); }        /* solid line = the zero baseline */
.fnd-bars { display: flex; gap: 3px; height: 100%; align-items: stretch; }
.fnd-col { flex: 1 1 0; min-width: 0; height: 100%; display: flex; flex-direction: column;
  justify-content: flex-end; align-items: center; }
.fnd-bar { width: 100%; max-width: 20px; min-height: 2px; background: var(--c-teal);
  border-radius: var(--r-control) var(--r-control) 0 0; }
.fnd-bar.partial { background: rgba(var(--teal-rgb),0.14); border: 1px dashed var(--c-teal); border-bottom: none; }
.fnd-bar-val { font-size: 0.58rem; font-weight: 800; color: var(--c-teal-deep); line-height: 1;
  margin-bottom: 3px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.fnd-baseaxis { display: flex; align-items: center; justify-content: space-between; margin-top: 3px; }
.fnd-zero { font-size: 0.56rem; font-weight: 700; color: var(--n-500); font-variant-numeric: tabular-nums; }
.fnd-partkey { display: inline-flex; align-items: center; gap: 5px; font-size: 0.54rem; color: var(--n-500); }
.fnd-partkey-sw { width: 11px; height: 8px; display: inline-block; box-sizing: border-box;
  border: 1px dashed var(--c-teal); border-bottom: none; background: rgba(var(--teal-rgb),0.14); }
.fnd-xaxis { display: flex; gap: 3px; margin-top: 2px; }
.fnd-xt { flex: 1 1 0; min-width: 0; text-align: center; font-size: 0.54rem; color: var(--n-400);
  font-variant-numeric: tabular-nums; }
.fnd-xt-strong { color: var(--n-600); font-weight: 700; }

.fnd-caveat { font-size: 0.62rem; line-height: 1.45; color: var(--n-600); background: var(--n-050);
  border-left: 2px solid var(--n-250); padding: 6px 9px; border-radius: 0 var(--r-control) var(--r-control) 0; margin: 2px 0 9px; }
.fnd-caveat-soft { color: var(--n-500); }
.fnd-method { font-size: 0.58rem; color: var(--n-500); font-style: italic; line-height: 1.4;
  font-variant-numeric: tabular-nums; }

/* ── supporting ledger row (NOT a 4-card grid) ── */
.fnd-ledger-h { font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase;
  font-weight: 800; color: var(--n-600); margin: 16px 2px 6px; padding-top: 14px;
  border-top: 1px solid var(--n-100); }
.fnd-ledger { display: flex; flex-direction: column; }
.fnd-row { padding: 10px 0; border-bottom: 1px solid var(--n-100); }
.fnd-row:last-child { border-bottom: none; padding-bottom: 2px; }
.fnd-row-name { font-size: 0.74rem; font-weight: 700; color: var(--n-900); line-height: 1.3;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; margin-bottom: 4px; }
.fnd-row-stat { font-size: 0.72rem; color: var(--n-800); line-height: 1.45; font-variant-numeric: tabular-nums; }
.fnd-row-stat b { color: var(--c-teal-deep); font-weight: 800; }
.fnd-row-detail { color: var(--n-500); font-size: 0.66rem; }
.fnd-row-caveat { font-size: 0.6rem; color: var(--n-600); line-height: 1.4; margin-top: 5px;
  padding-left: 8px; border-left: 2px solid var(--n-150); }
.fnd-row-method { font-size: 0.58rem; color: var(--n-500); font-style: italic; margin-top: 5px;
  font-variant-numeric: tabular-nums; }

/* ── honest data-unavailable state ── */
.fnd-unavail { border: 1px dashed var(--n-250); background: var(--n-025); border-radius: var(--r-panel);
  padding: 12px; margin: 6px 0; }
.fnd-unavail-t { font-size: 0.72rem; font-weight: 700; color: var(--n-600); }
.fnd-unavail-w { font-size: 0.64rem; color: var(--n-500); line-height: 1.45; margin-top: 3px; }

/* ── methodology (About) — collapsed by default, low-key ── */
.method-about { margin-top: 16px; border-top: 1px solid var(--n-100); padding-top: 10px; }
.method-about > summary { font-size: 0.62rem; letter-spacing: 0.1em;
  font-weight: 800; color: var(--n-600); cursor: pointer; list-style: none; padding: 4px 2px;
  display: flex; align-items: center; gap: 6px; }
.method-about > summary::-webkit-details-marker { display: none; }
.method-about > summary::before { content: '\25B8'; font-size: 0.7rem; color: var(--n-500); }
.method-about[open] > summary::before { content: '\25BE'; }
.method-about > summary:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; border-radius: var(--r-control); }
.method-body { padding: 4px 2px 2px; }
.method-h { font-size: 0.56rem; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 800;
  color: var(--n-500); margin: 12px 0 6px; }
.method-legend, .method-sources { list-style: none; padding: 0; margin: 0; }
.method-legend li { display: flex; gap: 8px; align-items: baseline; font-size: 0.66rem;
  color: var(--n-800); line-height: 1.45; margin-bottom: 7px; }
.method-legend li .prov-chip { flex: 0 0 auto; }
.method-sources li { display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  font-size: 0.66rem; color: var(--n-800); padding: 4px 0; border-bottom: 1px solid var(--n-050); }
.method-sources li small { color: var(--n-500); font-size: 0.6rem; white-space: nowrap; }
.method-foot { font-size: 0.62rem; color: var(--n-600); line-height: 1.5; margin-top: 10px; }
.method-foot code { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.92em;
  background: var(--n-050); padding: 1px 5px; border-radius: var(--r-control); color: var(--n-800); }

/* The Projects header tab (#tab-projects, .t360-lock, .t360-lockword) and its
   locked dialog (.pj-lock-*) were removed 2026-09-12 with the tab itself. */

/* ── JOIN DIALOG (join.js) ────────────────────────────────────────────────────
   A native <dialog> opened with showModal(): focus stays inside it, Escape
   closes it without script, and the page behind is inert. The values are the
   Projects lock dialog's, carried over when that dialog was removed: the 12px
   panel radius, the hairline, the surface, --shadow-2, a 0.42 ink scrim, and
   z-index 400 (inert for a modal in the top layer; it only matters if a
   browser ever shows it non-modally). Every control is at least 44px and no
   text is under 14px. Inputs are 16px so iOS does not zoom the page into them.
   Nothing here is scoped to a view: the dialog is appended to <body>. */
.dlg360 { width: min(92vw, 26rem); max-width: calc(100vw - 32px); max-height: calc(100dvh - 32px);
  overflow-y: auto; overscroll-behavior: contain; margin: auto; padding: 0; z-index: 400;
  border: 1px solid var(--n-150); border-radius: var(--r-panel); background: var(--n-000);
  color: var(--n-900); box-shadow: var(--shadow-2); font-family: 'Manrope', sans-serif; }
/* The fallback inside var() is for browsers whose ::backdrop does not yet
   inherit custom properties from the dialog. */
.dlg360::backdrop { background: rgba(var(--ink-rgb, 30, 39, 50), 0.42); }
.dlg360 [hidden] { display: none !important; }
.dlg360-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: center;
  justify-content: space-between; gap: 12px; padding: 6px 8px 6px 18px;
  background: var(--n-000); border-bottom: 1px solid var(--n-100); }
.dlg360-title { margin: 0; font-size: 1.05rem; font-weight: 800; color: var(--n-900); }
.dlg360-close { flex: 0 0 auto; width: 44px; height: 44px; padding: 0; border: none; background: none;
  border-radius: var(--r-control); font-size: 1.5rem; line-height: 1; color: var(--n-600); cursor: pointer; }
.dlg360-close:hover { background: var(--n-050); color: var(--n-900); }
.dlg360-body { display: grid; gap: 14px; padding: 14px 18px 4px; }
.dlg360-lede, .dlg360-p { margin: 0; font-size: 0.9rem; line-height: 1.5; color: var(--n-800); }
.dlg360-p + .dlg360-p { margin-top: 10px; }
/* Risk tokens, not amber: amber is for map alerts (DESIGN.md r4-D7). */
.dlg360-alert { font-size: 0.875rem; line-height: 1.45; color: var(--n-900); padding: 10px 12px;
  border: 1px solid var(--c-risk-line); border-radius: var(--r-control); background: var(--c-risk-wash); }
.dlg360 a { color: var(--c-chrome); font-weight: 700; text-decoration: underline; text-underline-offset: 2px; }
.dlg360-field { display: grid; gap: 6px; }
.dlg360-field label { font-size: 0.875rem; font-weight: 700; color: var(--n-900); }
/* --n-500 borders: a field's edge needs 3:1 against the surface (WCAG 1.4.11). */
.dlg360-field input, .dlg360-field select { width: 100%; min-height: 44px; padding: 8px 12px;
  font-family: 'Manrope', sans-serif; font-size: 16px; color: var(--n-900); background: var(--n-000);
  border: 1px solid var(--n-500); border-radius: var(--r-control); }
.dlg360-field [aria-invalid="true"] { border-color: var(--c-risk); }
.dlg360-err { margin: 0; font-size: 0.875rem; font-weight: 600; color: var(--c-risk-deep); }
.dlg360-check { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 10px; align-items: start; }
.dlg360-check input { width: 20px; height: 20px; margin: 2px 0 0; accent-color: var(--c-chrome); cursor: pointer; }
/* The padding and the equal negative margin make the whole line a 44px hit
   area without moving anything. */
.dlg360-check label { font-size: 0.875rem; line-height: 1.45; color: var(--n-900); cursor: pointer;
  padding: 10px 0; margin: -10px 0; }
.dlg360-hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
/* Sticky, so Join stays on screen on a short phone while the form scrolls. */
.dlg360-foot { position: sticky; bottom: 0; display: flex; justify-content: flex-end; gap: 10px;
  margin-top: 14px; padding: 12px 18px 16px; background: var(--n-000); border-top: 1px solid var(--n-100); }
.dlg360-submit { min-height: 44px; padding: 0 24px; border: none; border-radius: var(--r-pill);
  background: var(--c-chrome); color: var(--n-000); font-family: 'Manrope', sans-serif;
  font-size: 0.95rem; font-weight: 700; cursor: pointer; }
.dlg360-submit:hover { opacity: 0.9; }
.dlg360-submit:disabled { opacity: 0.6; cursor: progress; }
.dlg360-done { padding: 16px 18px 0; }
.dlg360-done-title { margin: 0 0 8px; font-size: 1.05rem; font-weight: 800; color: var(--n-900); }
/* Focus lands on this heading so a screen reader starts at the confirmation;
   it is not a control, so it draws no ring. */
.dlg360-done-title:focus { outline: none; }
.dlg360-close:focus-visible, .dlg360-submit:focus-visible, .dlg360 a:focus-visible,
.dlg360-field input:focus-visible, .dlg360-field select:focus-visible,
.dlg360-check input:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; }

/* ── left-rail footer: coalition attribution above the independence disclaimer ── */
.rail-attrib { color: var(--n-600) !important; font-weight: 700; margin-bottom: 3px; }
.rail-disc { color: var(--n-500); }


/* ── review 2026-07-24: true-scale setback diagram street line ── */
.sb-street { stroke: var(--n-500); stroke-width: 2.5; stroke-linecap: round; stroke-dasharray: 6 4; }


/* ═══ City 3D / tree attribute popups — ArcGIS-style field table ═══ */
.ags-pop-title { font: 800 0.82rem/1.3 'Manrope', sans-serif; color: var(--n-900); }
.ags-pop-sub { font-size: 0.65rem; color: var(--n-500); margin: 2px 0 8px; }
.ags-tbl { width: 100%; border-collapse: collapse; }
.ags-tbl tr { border-top: 1px solid var(--n-100); }
.ags-tbl tr:first-child { border-top: 0; }
.ags-tbl th { text-align: left; font-weight: 600; color: var(--n-550); font-size: 0.67rem;
  padding: 4px 10px 4px 0; white-space: nowrap; vertical-align: top; }
.ags-tbl td { text-align: right; font-weight: 700; color: var(--n-900); font-size: 0.7rem;
  padding: 4px 0; font-variant-numeric: tabular-nums; }
.ags-pop-note { font-size: 0.6rem; color: var(--n-500); margin-top: 8px; font-style: italic;
  border-top: 1px solid var(--n-100); padding-top: 6px; }

/* ═══ Glossary ═══ */
.gloss { margin: 0 0 18px; }
.gl-head { font-family: 'Instrument Serif', Georgia, serif; font-size: 1.15rem; color: var(--n-900); }
.gl-sub { font-size: 0.66rem; color: var(--n-550); margin: 2px 0 10px; }
.gl-group { font-size: 0.55rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--n-400); margin: 12px 0 4px; }
.gl-t { border-bottom: 1px solid var(--n-100); }
.gl-t summary { cursor: pointer; padding: 6px 0; list-style: none; display: flex;
  flex-direction: column; gap: 1px; }
.gl-t summary::-webkit-details-marker { display: none; }
.gl-t summary b { font-size: 0.72rem; color: var(--n-900); }
.gl-t summary span { font-size: 0.62rem; color: var(--n-550); }
.gl-t[open] summary b { color: var(--c-blue-deep); }
.gl-body { font-size: 0.65rem; line-height: 1.55; color: var(--n-700); padding: 2px 0 9px; }
.gl-fig { margin-top: 6px; font-size: 0.64rem; color: var(--c-teal-deep); background: var(--c-teal-wash);
  border-radius: var(--r-control); padding: 5px 8px; }
.gl-fig b { font-variant-numeric: tabular-nums; }
.gl-src { margin-top: 5px; font-size: 0.55rem; color: var(--n-400); font-style: italic; }
.gl-foot { margin-top: 12px; padding-top: 7px; border-top: 1px solid var(--n-100);
  font-size: 0.55rem; color: var(--n-400); font-style: italic; line-height: 1.5; }

/* ═══ (H+L) rule diagram — redraws the CDD memo figure on this lot ═══ */
.hl-diagram, .hl-block { margin: 12px 0 4px; }
.hl-h { font-size: 0.58rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  color: var(--n-700); margin-bottom: 6px; }
.hl-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.hl-col { background: var(--n-025); border: 1px solid var(--n-150); border-radius: var(--r-control); padding: 6px; }
.hl-cap { font-size: 0.58rem; font-weight: 800; margin-bottom: 2px; }
.hl-cap-old { color: var(--n-550); }
.hl-cap-new { color: var(--c-blue-deep); }
.hl-svg { width: 100%; height: auto; display: block; }
.hl-ground { stroke: var(--n-300); stroke-width: 1; }
.hl-lotline { stroke: var(--c-risk); stroke-width: 1.2; stroke-dasharray: 3 2; }
.hl-dim { stroke: var(--n-550); stroke-width: 0.8; }
.hl-set { stroke: var(--c-risk); stroke-width: 1.2; }
.hl-bldg.hl-old { fill: rgba(var(--n-500-rgb),.35); stroke: var(--n-550); stroke-width: 1.2; }
.hl-bldg.hl-new { fill: rgba(var(--blue-rgb),.28); stroke: var(--c-blue-bright); stroke-width: 1.2; }
.hl-t { font-size: 6.5px; fill: var(--n-600); font-family: inherit; font-weight: 700; }
.hl-anchor-m { text-anchor: middle; }
.hl-anchor-s { text-anchor: start; }
.hl-setlbl { fill: var(--c-risk); }
.hl-math { font-size: 0.58rem; color: var(--n-700); margin-top: 4px; line-height: 1.4; }
.hl-math b { color: var(--n-900); }
.hl-bind { display: block; font-size: 0.54rem; color: var(--n-400); }
.hl-foot { font-size: 0.55rem; line-height: 1.45; color: var(--n-400); margin-top: 6px; font-style: italic; }
.hl-bk { background: var(--n-025); border: 1px solid var(--n-150); border-radius: var(--r-control); padding: 8px 10px; }
.hl-bkrow { display: flex; align-items: baseline; gap: 7px; margin-bottom: 2px; font-size: 0.62rem; color: var(--n-600); }
.hl-bkrow b { font-size: 0.82rem; font-weight: 800; color: var(--c-blue-ink); font-variant-numeric: tabular-nums; }

/* ═══ Three-state envelope toggle (old / new no-affordable / new affordable) ═══ */
.env-toggle { display: flex; flex-wrap: wrap; gap: 5px; margin: 0 0 7px; }
.env-chip { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--n-200);
  background: var(--n-025); color: var(--n-400); border-radius: var(--r-panel); padding: 4px 9px;
  font: inherit; font-size: 0.6rem; font-weight: 700; cursor: pointer; }
.env-chip.on { background: var(--n-000); color: var(--n-850); border-color: var(--n-400); }
.env-chip i { width: 9px; height: 9px; border-radius: var(--r-control); border: 1.5px solid; opacity: .45; }
.env-chip.on i { opacity: 1; }
.env-chip i.es-pre { border-color: var(--n-550); background: rgba(var(--n-500-rgb),.25); }
.env-chip i.es-br  { border-color: var(--c-risk-deep); background: rgba(var(--risk-rgb),.22); }
.env-chip i.es-in  { border-color: var(--c-violet-light); background: rgba(var(--violet-rgb),.25); }
.env-note { margin: 0 0 9px; font-size: 0.62rem; }
.en-row { display: flex; gap: 8px; align-items: baseline; margin-bottom: 1px; }
.en-k { flex: 0 0 62px; font-weight: 800; color: var(--n-700); font-size: 0.58rem;
  text-transform: uppercase; letter-spacing: .05em; }
.en-v { color: var(--n-850); }
.en-v i { color: var(--n-400); font-style: normal; }
.en-zone { font-weight: 800; color: var(--n-550); }
.en-sub { margin: 2px 0 7px 70px; font-size: 0.58rem; line-height: 1.45; color: var(--n-550); }
.en-punch { color: var(--c-amber-deep); background: var(--c-amber-wash); border-left: 2px solid var(--c-amber);
  border-radius: 0 var(--r-control) var(--r-control) 0; padding: 5px 7px; margin-left: 70px; }
/* Old rule / new rule: the heights stay open, the derivation folds (Rocker
   2026-08-13, "compress it and unfold it… like the setback envelope"). The two
   .en-sub blocks were aligned to the 70px .en-k gutter of the rows above them;
   inside the fold there is no gutter to align to, so the indent comes off. */
.en-expand { margin-top: 6px; }
.en-expand .en-sub, .en-expand .en-punch { margin-left: 0; }
.en-expand .en-sub:last-child { margin-bottom: 0; }
/* --n-550 measured 3.48:1 on the fold's body in light and 3.54:1 in dark, both
   under the 4.5 the PDR commits to. --n-600 is the file's existing
   secondary-text token and takes it to 5.22 / 4.73. Scoped to the fold — the
   same token is read by unrelated decorative text elsewhere. */
.en-expand .en-sub { color: var(--n-600); }

/* ═══ Multi-site build-out + facade shadow control ═══ */
.buildout-ctl { margin: 0 0 10px; padding: 9px 11px; border-radius: var(--r-control);
  background: var(--n-100); border: 1px solid var(--n-200); }
.bo-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.bo-title { font-size: 0.55rem; font-weight: 800; letter-spacing: 0.09em; color: var(--n-700); }
.bo-toggle { display: inline-flex; background: var(--n-150); border-radius: var(--r-control); padding: 2px; }
.bo-btn { border: 0; background: transparent; cursor: pointer; font: inherit;
  font-size: 0.62rem; font-weight: 700; color: var(--n-600); padding: 4px 10px; border-radius: var(--r-control); }
.bo-btn.active { background: var(--n-000); color: var(--c-blue-deep); box-shadow: var(--shadow-2); }
/* Scenario row: by-right vs inclusionary. Subordinate to Today / Max allowed —
   it only refines what "max" means, so it reads one step quieter. */
.bo-sub { display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-top: 7px; }
.bo-sublabel { font-size: 0.53rem; font-weight: 800; letter-spacing: 0.09em;
  color: var(--n-550); text-transform: uppercase; }
.bo-btn-sm { font-size: 0.58rem; padding: 3px 8px; }
.bo-readout { margin-top: 7px; }
.bo-row { display: flex; align-items: baseline; gap: 7px; margin-bottom: 3px; }
.bo-row b { font-size: 0.92rem; font-weight: 800; color: var(--c-blue-ink);
  font-variant-numeric: tabular-nums; min-width: 44px; }
.bo-row span { font-size: 0.62rem; color: var(--n-600); }
.bo-note { font-size: 0.56rem; line-height: 1.5; color: var(--n-550); margin-top: 5px;
  border-top: 1px solid var(--n-150); padding-top: 5px; }
.bo-warn { font-size: 0.62rem; color: var(--c-amber-deep); background: var(--c-amber-wash); border-radius: var(--r-control);
  padding: 6px 8px; }

/* ═══ Model prediction block in the parcel card ═══
   Violet is the model's colour everywhere in this app (the map flag rings use
   var(--c-violet)). It must never borrow the blue/red of an observed filing. */
.pc-model { margin: 0 22px 14px; padding: 11px 12px; border-radius: var(--r-control);
  background: var(--c-violet-wash-2); border: 1px solid var(--c-violet-wash); border-left: 3px solid var(--c-violet); }
.pm-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.pm-tag { font-size: 0.53rem; font-weight: 800; letter-spacing: 0.1em;
  color: var(--n-000); background: var(--c-violet); padding: 3px 6px; border-radius: var(--r-control); }
.pm-score { font-size: 1.05rem; font-weight: 800; color: var(--c-violet-deep);
  font-variant-numeric: tabular-nums; }
.pm-title { font-size: 0.72rem; font-weight: 700; color: var(--c-violet-deep); margin: 6px 0 8px; }
.pm-held { font-size: 0.62rem; line-height: 1.45; color: var(--c-violet-muted); margin-bottom: 8px;
  background: var(--c-violet-wash); border-radius: var(--r-control); padding: 6px 7px; }
.pm-held small { color: var(--c-violet-soft); }
.pm-filed { font-size: 0.62rem; line-height: 1.45; color: var(--c-amber-deep); background: var(--c-amber-wash);
  border-left: 3px solid var(--c-amber); border-radius: 0 var(--r-control) var(--r-control) 0; padding: 6px 8px;
  margin-bottom: 9px; }
.pm-filed b { color: var(--c-amber-deep); }
.pm-bar { display: grid; grid-template-columns: 1fr 62px 58px; align-items: center;
  gap: 7px; margin-bottom: 4px; }
.pm-bl { font-size: 0.6rem; color: var(--c-violet-muted); }
.pm-btrack { height: 6px; background: var(--c-violet-wash); border-radius: var(--r-control); overflow: hidden; }
.pm-btrack i { display: block; height: 100%; background: var(--c-violet); border-radius: var(--r-control); }
.pm-bv { font-size: 0.6rem; font-weight: 700; color: var(--c-violet-deep); text-align: right;
  font-variant-numeric: tabular-nums; }
.pm-bv small { color: var(--c-violet-soft); font-weight: 600; }
.pm-conf { font-size: 0.62rem; color: var(--c-violet-muted); margin-top: 8px; }
.pm-conf ul { margin: 3px 0 0; padding-left: 15px; }
.pm-conf li { margin-bottom: 1px; }
.pm-caveat { font-size: 0.58rem; line-height: 1.5; color: var(--c-violet-muted); margin-top: 9px;
  border-top: 1px solid var(--c-violet-wash); padding-top: 7px; }

/* ═══ Official zoning legend (reproduces the CDD printed legend) ═══ */
/* Zoning legend runs HORIZONTALLY (2026-07-31). ~70 districts stacked in a
   268px column ran off the bottom of the window and buried the provenance
   caveats at the end — which are the part that must not be missed, since some
   of those colours are ours and not the city's. Now: each family is a block,
   its swatches sit two rows deep, and the blocks scroll sideways. */
.theme-legend.tl-zoning { max-width: min(880px, 68vw); max-height: none; }
.tl-zscroll { display: flex; gap: 16px; overflow-x: auto; overflow-y: hidden;
  padding: 2px 2px 8px; scrollbar-width: thin; }
.tl-zscroll::-webkit-scrollbar { height: 8px; }
.tl-zscroll::-webkit-scrollbar-thumb { background: var(--n-200); border-radius: var(--r-pill); }
.tl-zgroup { flex: 0 0 auto; }
.tl-zgrid { display: grid; grid-auto-flow: column; grid-template-rows: repeat(2, auto);
  gap: 2px 14px; }
.tl-zgroup .tl-src-warn { margin-top: 6px; max-width: 300px; white-space: normal; }
.tl-zgrid .tl-zrow { white-space: nowrap; }
.tl-fam { font-size: 0.62rem; letter-spacing: 0.09em;
  font-weight: 800; color: var(--n-400); margin: 7px 0 2px; }
.tl-fam:first-of-type { margin-top: 3px; }
.tl-zrow { font-size: 0.68rem; line-height: 1.35; }
/* min-width tracks the code's own type — it is a column, and a district code
   that outgrows its gutter collides with the name it is labelling. */
.tl-code { display: inline-block; min-width: 50px; font-weight: 800; color: var(--n-900);
  font-size: 0.66rem; letter-spacing: 0.02em; }
.tl-src { font-size: 0.6rem; color: var(--n-400); font-style: italic; margin-top: 8px;
  border-top: 1px solid var(--n-100); padding-top: 5px; }
/* Districts we had to colour ourselves — never allowed to read as the city's */
.tl-fam-ours { color: var(--c-amber-deep); }
.tl-src-warn { color: var(--c-amber-deep); font-style: normal; background: var(--c-amber-wash);
  border-top: 0; border-left: 3px solid var(--c-amber); border-radius: 0 var(--r-control) var(--r-control) 0;
  padding: 5px 7px; margin-top: 5px; }


/* ═══ Basemap block inside the layers dock — collapsible so it can never
       squeeze the scrollable layer list out of view ═══ */
#bm3d-ctl { flex: 0 0 auto; }
#bm3d-ctl > summary { cursor: pointer; list-style: none; font-weight: 800;
  letter-spacing: 0.06em; font-size: 0.6rem;
  color: var(--n-600); padding: 2px 0 4px; display: flex; align-items: center; gap: 6px; }
#bm3d-ctl > summary::-webkit-details-marker { display: none; }
#bm3d-ctl > summary::after { content: '▸'; margin-left: auto; font-size: 0.75rem; opacity: .6; }
#bm3d-ctl[open] > summary::after { content: '▾'; }
#bm3d-ctl label { display: block; margin: 2px 0; font-size: 0.68rem; }

/* Collapsed legends for the layers that are on but not the subject. Clicking a
   chip promotes that layer to the full legend. Count on the chip so you know
   how much is hidden before you click — a silent collapse is the same failure
   as a silent truncation. */
.tl-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 7px;
  padding-top: 6px; border-top: 1px solid var(--n-100); }
.tl-chip { font: inherit; font-size: 0.66rem; font-weight: 700; cursor: pointer;
  border: 1px solid var(--n-150); background: var(--n-000); color: var(--n-600);
  padding: 3px 8px; border-radius: var(--r-pill); font-family: 'Manrope', sans-serif; }
.tl-chip:hover { background: var(--n-050); color: var(--n-900); border-color: var(--n-200); }
.tl-chip b { font-weight: 800; color: var(--n-400); margin-left: 2px; }

/* ── DASHBOARD TAB ─────────────────────────────────────────────────────────
   Three columns per the approved mockup: selected neighborhood left, the live
   map and the cross-neighborhood views centre, the index and reading notes
   right. Falls to one column under 1180px, where three columns would put the
   map in a ~300px box and stop it being a map. */
.dash { padding: 16px 20px 40px; overflow-y: auto; }
.dash-head { display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; margin-bottom: 14px; }
.dash-title { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400;
  font-size: 1.7rem; margin: 0; color: var(--n-900); }
.dash-sub { font-size: 0.76rem; color: var(--n-600); margin: 2px 0 0; }
/* The Analytics tab's one instruction comes in two versions (renderDashboard in
   cambridge.js) and exactly one paints. A screen that cannot hover is told to
   tap; the hover-and-click sentence would describe gestures it does not have. */
.dash-sub-touch { display: none; }
@media (hover: none) {
  .dash-sub-hover { display: none; }
  .dash-sub-touch { display: inline; }
}
.dash-pick { display: flex; align-items: center; gap: 8px; }
.dash-pick label { font-size: 0.6rem; letter-spacing: 0.08em;
  font-weight: 800; color: var(--n-400); }
.dash-pick select { font-family: 'Manrope', sans-serif; font-size: 0.78rem; font-weight: 700;
  color: var(--n-900); border: 1px solid var(--n-150); border-radius: var(--r-control); padding: 6px 10px;
  background: var(--n-000); }
.dash-console { display: grid; grid-template-columns: 320px minmax(340px, 0.95fr) 320px;
  gap: 14px; align-items: start; }
.dash-col { display: grid; gap: 14px; }
.dash-col > .card, .dash-col .card { background: var(--n-000); border: 1px solid var(--n-150);
  border-radius: var(--r-panel); padding: 14px 16px; }
.dash-col .card h3, .dash .card h3 { font-size: 0.7rem; letter-spacing: 0.07em; color: var(--n-600); margin: 0 0 10px; }
.dash-mapcard { padding: 0 !important; overflow: hidden; }
.dash-maphead { display: flex; align-items: center; justify-content: space-between;
  padding: 12px 14px 8px; }
.dash-maphead h3 { margin: 0; }
/* Rocker 2026-08-03, said four times: the map is "squished" — "make the map
   double as high as it is". She is reading a PROPORTION, not a multiplier, and
   the arithmetic says a literal 920px is the wrong answer. The centre column is
   732px wide here (1400px of .dash content — 1440 viewport, rail closed by
   setView for a full view, 20px padding each side — split 320 / minmax(340,
   0.95fr) / 320 with two 14px gaps). So 460px reads as 1.59:1, and her
   complaint is exact. 560px puts it at 1.31:1, which is the most this can take
   and still leave the legend band above the fold: at 1440x900 the canvas is
   842px under the 58px header, and header + dash head + card head + 560 + band
   comes to ~830. At 920px the map alone would outrun the viewport and push the
   grey band she asked for off-screen, which defeats the point. Do not raise the
   cap without re-doing this sum. The clamp gives short viewports the old 460px
   rather than a map that is the entire scroll. */
.dash-mapslot { position: relative; height: clamp(460px, 64vh, 560px); }
.dash-mapslot #map { position: absolute; inset: 0; width: 100%; height: 100%; }
.dash-chip { display: inline-block; font-size: 0.6rem; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; padding: 2px 7px; border-radius: var(--r-pill);
  background: rgba(var(--teal-rgb),0.12); color: var(--c-teal-deep); }
.dash-chip.mod { background: rgba(var(--blue-rgb),0.12); color: var(--c-blue-deep); }
.dash-hr { border: 0; border-top: 1px solid var(--n-100); margin: 10px 0; }
.dash-flag { font-size: 0.72rem; color: var(--c-amber-deep); background: var(--c-amber-wash); border-left: 3px solid var(--c-amber);
  border-radius: 0 var(--r-control) var(--r-control) 0; padding: 8px 10px; margin: 0; }
.dash-note { font-size: 0.7rem; color: var(--n-600); margin: 8px 0 0; line-height: 1.5; }
.dash-empty { font-size: 0.8rem; color: var(--n-600); }
/* index ring */
.dash-ring { display: flex; align-items: center; gap: 14px; }
.dash-dial { width: 92px; height: 92px; border-radius: 50%; flex: 0 0 auto; display: grid;
  place-items: center; background: conic-gradient(var(--c-teal) 0 var(--p), var(--n-100) var(--p) 100%); }
.dash-dial b { width: 68px; height: 68px; border-radius: 50%; background: var(--n-000); display: grid;
  place-items: center; font-family: 'Instrument Serif', Georgia, serif; font-size: 1.45rem;
  font-weight: 400; color: var(--n-900); }
.dash-parts { flex: 1; min-width: 0; }
.dash-mini { display: grid; grid-template-columns: 82px 1fr 30px; gap: 7px; align-items: center;
  font-size: 0.68rem; margin-bottom: 4px; color: var(--n-800); }
.dash-mini .t { height: 7px; background: var(--n-050); border-radius: var(--r-pill); overflow: hidden; }
.dash-mini .t i { display: block; height: 100%; background: var(--c-blue-mid); border-radius: var(--r-pill); }
.dash-mini .n { text-align: right; color: var(--n-600); font-variant-numeric: tabular-nums; }
/* donuts, vertical in the narrow left column */
.dash-pies { display: grid; gap: 16px; }
.dash-pie { display: grid; grid-template-columns: 96px 1fr; gap: 12px; align-items: center; }
.dash-pie h4 { grid-column: 1 / -1; font-size: 0.64rem; letter-spacing: 0.05em; color: var(--n-400); margin: 0 0 2px; }
.dash-donut { width: 96px; height: 96px; border-radius: 50%; position: relative; }
.dash-donut::after { content: ''; position: absolute; inset: 24px; background: var(--n-000); border-radius: 50%; }
.dash-key { font-size: 0.68rem; display: grid; gap: 2px; }
.dash-key span { display: flex; align-items: center; gap: 5px; color: var(--n-800); }
.dash-key i { width: 9px; height: 9px; border-radius: var(--r-control); flex: 0 0 auto; }
.dash-key b { margin-left: auto; color: var(--n-600); font-weight: 600; font-variant-numeric: tabular-nums; }
/* matrix */
.dash-matrix { overflow-x: auto; }
.dash-matrix table { border-collapse: collapse; font-size: 0.7rem; width: 100%; min-width: 560px; }
.dash-matrix th { text-align: left; font-weight: 700; color: var(--n-600); padding: 5px 6px;
  font-size: 0.62rem; letter-spacing: 0.04em; white-space: nowrap; }
.dash-matrix td { padding: 0; border: 2px solid var(--n-000); }
.dash-matrix td span { display: block; padding: 6px 7px; border-radius: var(--r-control);
  font-variant-numeric: tabular-nums; }
.dash-matrix tbody th { font-weight: 600; color: var(--n-900); text-transform: none; font-size: 0.7rem; }
.dash-matrix tr.sel th { font-weight: 800; }
/* SELECTION over data. Chrome, not encoding — and moving it off teal improves
   the encoding: a selection highlight must never share a hue with a series in
   the chart it sits on, and .dbar-track/.act-bar are still teal. */
.dash-matrix tr.sel td span { outline: 2px solid var(--c-chrome); outline-offset: -2px; }
.dash-spider { height: 120px; border: 1px dashed var(--n-150); border-radius: var(--r-control); display: flex;
  align-items: center; justify-content: center; text-align: center; padding: 10px;
  font-size: 0.7rem; color: var(--n-400); }
.dash-sources { margin-top: 16px; background: var(--n-000); border: 1px solid var(--n-150);
  border-radius: var(--r-panel); padding: 14px 16px; }
.dash-srcs { list-style: none; padding: 0; margin: 0 0 8px; font-size: 0.7rem; color: var(--n-600);
  line-height: 1.6; }
.dash-srcs a { color: var(--c-blue); font-weight: 700; }
@media (max-width: 1180px) { .dash-console { grid-template-columns: 1fr; } }

/* ── ABOUT TAB ─────────────────────────────────────────────────────────────
   Built from the same .card the dashboard uses, so About inherits the surface,
   border, radius and heading treatment rather than inventing any. Nothing here
   declares a colour or a type step that is not already in this file: the
   provenance legend, the dated source rows and the footnote are the existing
   .method-* classes, MOVED here out of the Findings pane's <details> rather
   than copied, so the two cannot drift apart on the next edit. */
/* THREE columns, not two. Two put the body copy on a 692px measure at 1440 —
   about 120 characters a line, which is where a reader loses the return sweep.
   Three lands it near 78.
   Multi-column rather than grid: these eight cards range from 314px to 1264px
   tall, and hand-assigning them to three grid columns left one column 2.3x the
   height of another whichever way they were dealt. Multicol balances the
   columns itself, and `break-inside: avoid` is what keeps a card whole. */
/* ── ABOUT · ONE COLUMN, IN A STATED ORDER ──────────────────────────────────
   Was `columns: 3`. A reviewer read column one and needed the correction route,
   which sat in column three at the same scroll depth. Collapsing to one column
   without reordering would have pushed it thirty-odd lines further down — the
   diagnosed problem, made worse by its own fix.

   So: one measured column, the order changed with the layout, a contents rail
   on wide screens, and the two routes a reader might want at any moment pinned
   to the bottom of the viewport rather than the bottom of the document. */
.about-doc { display: grid; grid-template-columns: 200px minmax(0, 68ch); gap: 40px;
  justify-content: center; align-items: start; padding-bottom: 72px; }
@media (max-width: 900px) { .about-doc { grid-template-columns: minmax(0, 1fr); gap: 20px; } }

.about-toc { position: sticky; top: 16px; font-size: 0.74rem; }
@media (max-width: 900px) { .about-toc { position: static; } }
.about-toc-h { margin: 0 0 8px; font-size: 0.6rem; letter-spacing: .1em;
  text-transform: uppercase; font-weight: 800; color: var(--n-600); }
.about-toc ol { margin: 0; padding: 0 0 0 1.4em; display: flex; flex-direction: column; gap: 5px; }
.about-toc a { color: var(--n-600); text-decoration: none; }
.about-toc a:hover, .about-toc a:focus-visible { color: var(--c-chrome); text-decoration: underline; }

.about-body { display: flex; flex-direction: column; gap: 30px; min-width: 0; }
/* scroll-margin so an anchor jump does not hide the heading under the header */
.about-sec { scroll-margin-top: 84px; }
.about-sec > h3 { margin: 0 0 10px; font-size: 1.05rem; }
/* 0.9rem = 14.4px, above the 14px body floor DESIGN.md commits to. This is the
   one long-form surface in the product and it is set to be read, not scanned. */
.about-sec .about-p { font-size: 0.9rem; line-height: 1.65; max-width: 68ch; }

.about-pin {
  position: sticky; bottom: 0; grid-column: 1 / -1;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 8px; padding: 10px 14px;
  background: var(--surface, #fff); border: 1px solid var(--line);
  border-radius: var(--r-control); box-shadow: 0 -2px 12px rgba(var(--ink-rgb), .07);
}
.about-pin-t { font-size: 0.76rem; font-weight: 700; color: var(--n-900); margin-right: auto; }

.about-grid { columns: 3; column-gap: 16px; }
.about-grid > .card { break-inside: avoid; margin: 0 0 16px; display: block; }
/* 0.9rem = 14.4px. This is the ONE surface in the product that is long-form
   prose rather than a dense instrument panel, and DESIGN.md commits to a 14px
   body floor — writing an accessibility statement in 11.5px type would have
   disproved itself in its own first paragraph. 0.9rem is an existing step in
   this file, not a new one. The label/value rows below stay smaller because
   they are metadata, not prose. */
.about-p { font-size: 0.9rem; color: var(--n-600); line-height: 1.6; margin: 0 0 11px; }
.about-view .method-legend li,
.about-view .method-sources li { font-size: 0.8rem; }
/* --n-500 measures 2.90:1 on the About field. That is the class of failure this
   page is meant to disclose, not commit — and every one of these values IS the
   evidence (a source's date, the tracker's snapshot). --n-600 takes it to
   5.09:1. Scoped to .about-view so the Findings pane is untouched. */
.about-view .method-sources li small { font-size: 0.72rem; color: var(--n-600); }
.about-view .method-foot { font-size: 0.8rem; }
.about-view .card h3 { font-size: 0.74rem; }
.about-p:last-child { margin-bottom: 0; }
.about-p strong { color: var(--n-900); font-weight: 700; }
.about-p code, .about-list code { font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.92em; background: var(--n-050); padding: 1px 5px;
  border-radius: var(--r-control); color: var(--n-800); }
.about-p a, .about-list a, .method-sources a { color: var(--c-chrome); font-weight: 700; }
.about-list { list-style: none; padding: 0; margin: 0; }
.about-list li { font-size: 0.9rem; color: var(--n-600); line-height: 1.6;
  padding: 0 0 10px 15px; position: relative; }
.about-list li::before { content: ''; position: absolute; left: 2px; top: 9px;
  width: 4px; height: 4px; border-radius: 50%; background: var(--n-300); }
.about-list li strong { color: var(--n-900); font-weight: 700; }
.about-list li:last-child { padding-bottom: 0; }
/* The provenance chips read inline in the Findings pointer, where they sit in a
   sentence rather than at the head of a list row. */
.method-foot .prov-chip { vertical-align: baseline; }
.about-view .method-legend { margin-top: 4px; }
.about-view .cp-weekly:focus-visible,
.method-body .cp-weekly:focus-visible { outline: 2px solid var(--c-chrome);
  outline-offset: 2px; }
/* [data-tracker="unlocated"] is written by syncTrackerCounts() and left EMPTY
   when every tracked address geocoded. Hidden either way, so the card never
   shows a stranded gap where a disclosure would have been. */
.about-p:empty { display: none; }
.about-unloc { border-left: 2px solid var(--n-200); padding-left: 9px; }
@media (max-width: 1180px) { .about-grid { columns: 2; } }
@media (max-width: 820px)  { .about-grid { columns: 1; } }

/* Bars and KPI blocks used by the dashboard renderer. These class names came
   over from the mockup with the markup but their styles did not, so the cards
   rendered as run-together text ("Population7,361") until this landed. */
.kpi { display: flex; flex-direction: column; gap: 2px; }
.kpi-k { font-size: 0.66rem; letter-spacing: 0.05em; color: var(--n-600); }
.kpi-v { font-family: 'Instrument Serif', Georgia, serif; font-size: 1.5rem; color: var(--n-900);
  line-height: 1.15; }
.kpi-vs { display: block; font-size: 0.68rem; color: var(--n-600); font-style: normal; }
.dbar-row { display: grid; grid-template-columns: 96px 1fr 62px; gap: 9px; align-items: center;
  margin-bottom: 5px; font-size: 0.72rem; color: var(--n-800); }
.dbar-lab { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dbar-track { height: 9px; background: var(--n-050); border-radius: var(--r-pill); overflow: hidden; }
.dbar-track i { display: block; height: 100%; background: var(--c-blue-mid); border-radius: var(--r-pill); }
.dbar-val { text-align: right; font-variant-numeric: tabular-nums; color: var(--n-600); }
.dbar-row.sel .dbar-val, .dbar-row.sel .dbar-lab { color: var(--n-900); font-weight: 800; }
.dbar-row.sel .dbar-track i { background: var(--c-chrome); }
.dbars { margin-top: 2px; }

/* ── EXPLORE STRIP ─────────────────────────────────────────────────────────
   Rocker asked for the analytics at the bottom of the map AND a separate tab
   (2026-07-30, 20:30 "yeah, both"). This is the map-linked half: it changes as
   you click, and it stays brief so the map remains the subject. Built as a
   sibling row under the canvas — the floating panels are not touched. */
.map-col { display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; }
.content-row .map-col > .map-canvas { margin: 14px 14px 10px 20px; flex: 1; min-height: 0; }
.explore-strip { margin: 0 14px 16px 20px; background: var(--n-000); border: 1px solid var(--n-150);
  border-radius: var(--r-panel); flex: 0 0 auto;
  /* r4-D1: the strip stays brief. It scrolls rather than shoving the map up. */
  max-height: 208px; overflow-y: auto; }
.es-main { display: grid; grid-template-columns: 160px 1fr auto; gap: 18px; align-items: center;
  padding: 10px 14px; }
.es-sel { display: flex; flex-direction: column; min-width: 0; }
.es-sel b { font-size: 0.86rem; color: var(--n-900); white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; }
.es-hood { font-size: 0.72rem; color: var(--n-600); }
.es-k { font-size: 0.58rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--n-400);
  font-weight: 800; }
.es-charts { display: flex; gap: 22px; flex-wrap: wrap; align-items: flex-start; }
.es-hint { font-size: 0.74rem; color: var(--n-400); }
.es-c { min-width: 120px; }
.es-c h5 { margin: 0 0 5px; font-size: 0.58rem; letter-spacing: 0.05em;
  color: var(--n-400); font-weight: 800; }
.es-cap { font-size: 0.66rem; color: var(--n-600); margin-top: 4px; }
.es-cap b { color: var(--n-900); font-size: 0.78rem; }
.es-vs { position: relative; height: 12px; background: var(--n-100); border-radius: var(--r-pill); }
.es-vs i { display: block; height: 100%; background: var(--c-blue-mid); border-radius: var(--r-pill); }
.es-vs u { position: absolute; top: -3px; width: 2px; height: 18px; background: var(--n-900); }
.es-stack { display: flex; height: 12px; border-radius: var(--r-pill); overflow: hidden; background: var(--n-100); }
.es-stack i { display: block; height: 100%; }
/* Every stacked bar carries its own key — a colour without a label is not a
   reading (DESIGN.md: colour chips always paired with text). */
.es-key { display: flex; flex-wrap: wrap; gap: 2px var(--s-2); margin-top: var(--s-1);
  font-size: 0.6rem; color: var(--n-600); max-width: 260px; }
.es-key span { display: inline-flex; align-items: center; gap: var(--s-1); }
.es-key i { width: 8px; height: 8px; border-radius: var(--r-control); flex: 0 0 auto; }
.es-key b { color: var(--n-900); font-variant-numeric: tabular-nums lining-nums; }
/* The seven-bracket detail a donut click routes here. The strip is a strip
   (DESIGN.md r4-D1: it stays brief), so the seven rows run two-up and the whole
   card is height-capped — nothing is dropped, it just does not push the map
   off the screen. The standing read lives in the Neighborhood analytics tab. */
.es-c-wide { flex: 1 1 360px; min-width: 300px; max-width: 460px; }
.es-c-wide .isp { width: auto; }
.es-c-wide .isp-h, .es-c-wide .isp-med, .es-c-wide .isp-bar, .es-c-wide .isp-key { display: none; }
.es-c-wide .isp-sub { margin-top: 0; }
.es-c-wide .isp-rows { display: grid; grid-template-columns: 1fr 1fr; gap: 2px var(--s-4); }
.es-c-wide .isp-note { font-size: 0.6rem; margin-top: var(--s-2);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.es-donut { width: 42px; height: 42px; border-radius: 50%; position: relative; display: grid;
  place-items: center; }
.es-donut::after { content: ''; position: absolute; inset: 10px; background: var(--n-000); border-radius: 50%; }
.es-donut b { position: relative; z-index: 1; font-size: 0.6rem; font-weight: 800; color: var(--n-900); }
.es-dims { display: flex; gap: 5px; flex-wrap: wrap; }
.es-dims button { font-family: 'Manrope', sans-serif; font-size: 0.66rem; font-weight: 700;
  border: 1px solid var(--n-150); background: var(--n-000); color: var(--n-600); padding: 4px 10px;
  border-radius: var(--r-pill); cursor: pointer; }
.es-dims button.on { background: rgba(var(--chrome-rgb),0.1); border-color: var(--c-chrome); color: var(--c-chrome); }
.es-foot { display: flex; align-items: center; gap: 14px; padding: 8px 14px; border-top: 1px solid var(--n-100);
  background: var(--n-025); border-radius: 0 0 var(--r-panel) var(--r-panel); }
.es-legend { display: flex; gap: 12px; flex-wrap: wrap; flex: 1; font-size: 0.66rem; color: var(--n-800); }
.es-more { margin-left: auto; font-family: 'Manrope', sans-serif; font-size: 0.68rem; font-weight: 700;
  color: var(--c-blue); background: none; border: 0; cursor: pointer; }
@media (max-width: 1200px) { .es-main { grid-template-columns: 1fr; } }
.es-rank { font-family: 'Instrument Serif', Georgia, serif; font-size: 1.5rem; color: var(--n-900);
  line-height: 1.1; }
.es-rank small { font-family: 'Manrope', sans-serif; font-size: 0.62rem; font-weight: 700;
  color: var(--n-400); margin-left: 5px; }
.es-lgt { font-size: 0.62rem; font-weight: 800; color: var(--n-600); margin-right: 4px; }

/* B6 — units lost vs proposed. Two bars per neighborhood, never one: charting
   only the losses would answer the question in a way that misleads. */
.ul-tot { display: flex; gap: 16px; flex-wrap: wrap; font-size: 0.72rem; color: var(--n-600);
  padding-bottom: 8px; margin-bottom: 8px; border-bottom: 1px solid var(--n-100); }
.ul-tot b { color: var(--n-900); font-size: 0.86rem; }
.ul-net b { color: var(--c-teal-deep); }
.ul-row { display: grid; grid-template-columns: 118px 1fr 78px; gap: 9px; align-items: center;
  margin-bottom: 5px; font-size: 0.7rem; }
.ul-lab { color: var(--n-800); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ul-bars { display: flex; flex-direction: column; gap: 2px; }
.ul-bars i { display: block; height: 7px; border-radius: var(--r-pill); min-width: 2px; }
i.ul-lost { background: var(--c-risk); }
i.ul-prop { background: var(--c-blue-mid); }
.ul-val { text-align: right; font-variant-numeric: tabular-nums; color: var(--n-600); }
.ul-key { display: flex; gap: 14px; margin-top: 8px; font-size: 0.66rem; color: var(--n-600); }
.ul-key span { display: inline-flex; align-items: center; gap: 5px; }
.ul-key i { width: 11px; height: 7px; border-radius: var(--r-pill); display: inline-block; }


/* ═══════════════════════════════════════════════════════════════════════════
   P0 · NOTHING FLOATS ON THE MAP  (Rocker 2026-07-31, 5:20 and again at 8:24)
   "I don't want anything in the map. I want to see the map 100% unobscured…
    on the bottom, under the map, is where the legend goes. On the left side
    you have all the layers… on the right side is the information."

   Three rules, enforced here rather than by discipline:
     · .map-canvas carries the GL canvas and the Mapbox north arrow + scale bar.
       Nothing else. (.table-view is exempt — it REPLACES the map, it is the
       keyboard/screen-reader path, and it is never up at the same time.)
     · every control docks into the left rail, compressed, one scroll;
     · the legend is a full-width strip UNDER the map, spanning the map column.
   The elements below were `position:absolute` inside .map-canvas. They are the
   SAME elements with the SAME ids — renderLayersPanel / renderThemeLegend /
   setupSolarSlider all still find them — only re-parented and un-positioned.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The rail now carries the layer dock (252px of content), so it needs the width. */
/* --rail-w: 252px, and 232px at ≤1250 (the :root tier block). */
.sidebar.rail { width: var(--rail-w); }
.main { margin-left: var(--rail-w); }

/* L2 — compressed. Her words: "make that compress a little bit so we need to
   scroll more". Tighter rows, tighter group headers, one scroll for the lot. */
.rail-scroll { padding: 8px 10px 14px; }
.rail-item { padding: 5px 9px; gap: 8px; font-size: 0.74rem; line-height: 1.25; }
.rail-h { margin: 11px 0 4px; }
.rail-src-row { padding: 3px 4px; }

/* Map view: the 3D/top-down and map/table toggles, docked. */
.rail-mapview { display: flex; flex-direction: column; gap: 5px; padding: 0 2px 2px; }
.rail-scroll .view-toggle,
.rail-scroll .mode-toggle {
  position: static; inset: auto; transform: none; z-index: auto;
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 3px;
  background: var(--n-050); border: 1px solid var(--n-150); border-radius: var(--r-control); padding: 3px;
  box-shadow: none;
}
.rail-scroll .vt-btn { padding: 6px 8px; font-size: 0.72rem; text-align: center; }
.rail-scroll .vt-btn.active { background: var(--c-chrome); color: var(--n-000); }

/* Layer dock, docked. `overflow: clip` is KEPT deliberately — see the note by
   the .city-layers rule above; `hidden` would re-create the focus-scroll bug.
   With max-height gone the rail is the single scroll container, so .cl-list no
   longer needs its own (nested scrollers inside a scrolling rail are a trap). */
.rail-scroll .city-layers {
  position: static; inset: auto; z-index: auto;
  width: auto; max-height: none; margin: 0;
  background: var(--n-000); border: 1px solid var(--n-150); border-radius: var(--r-panel);
  box-shadow: none; min-width: 0;
}
.rail-scroll .cl-list { overflow-y: visible; max-height: none; flex: 0 0 auto; }
.rail-scroll .cl-head { position: sticky; top: 0; background: var(--n-000); z-index: 2; border-radius: var(--r-panel) var(--r-panel) 0 0; }
.rail-scroll #bm3d-ctl { font-size: 0.66rem; }

/* The `.rail-scroll .solar-slider` docked variant that lived here was deleted
   2026-08-07. The rail dock was tried 2026-08-03 and reverted 2026-08-05 (the
   rail is 20px collapsed by default and opening it steals map width); the CSS
   was left behind styling nothing. The solar bar is now a 56px transport strip
   on the map's bottom edge — see .solar-slider at the top of this file.
   The build-out block DID move into the rail on 2026-08-07, so it keeps its
   narrow-column wrap. */
.rail-scroll .bo-head, .rail-scroll .bo-sub { flex-wrap: wrap; gap: 4px; }
.rail-scroll .buildout-ctl { margin: 2px 0 0; padding: 8px 9px; }
.rail-scroll .bo-toggle { flex-wrap: wrap; }

/* L3 — THE LEGEND STRIP. Full width of the map column, directly under the map,
   above the analytics strip. Reason it is not a rail panel: a collapsed side
   legend cannot be referenced while you are reading the map, which is the whole
   point of a legend. The rail keeps per-row swatches; this is canonical.

   2026-08-03 — IT IS NOW A GREY BAND, AND THE SAME BAND ON BOTH MAP SURFACES.
   Her words: "where the legend starts needs to be sitting lower… if you move
   the legend all the way to the grey beneath it, that's where I want it. I want
   more space for the map." So it stops being a white card floating on a white
   column: full-bleed, no radius, --n-100 (the token already reserved for wash /
   inset surfaces) with a hairline on top. The hairline is what makes the map
   above it read as ENDING — a band with no edge just looks like the map ran out
   of pixels. --n-100 rather than --n-050 because on the analytics tab the host
   is a white card, and the page grey is too close to white to register there.

   .dash-mapcard is the second host. The analytics tab shows the SAME map, so it
   gets the same legend rather than a second design; the card's overflow:hidden
   clips the band's square corners back to the card radius.
   :is() keeps the specificity identical to the old `.map-col >` selector, so
   nothing else in the cascade moves. */
:is(.map-col, .dash-mapcard) > .theme-legend {
  position: static; inset: auto; z-index: auto;
  max-width: none; width: auto; margin: 0;
  flex: 0 0 auto;
  flex-direction: row; flex-wrap: wrap; align-items: center;
  gap: 4px 14px;
  max-height: 86px; overflow-y: auto; overflow-x: hidden;
  border: 0; border-top: 1px solid var(--n-150); border-radius: 0;
  padding: 10px 14px 12px;
  box-shadow: none; background: var(--n-100);
}
/* Explore insets its canvas 20px from the rail. The band still runs edge to
   edge, but its first swatch lines up with the map above it rather than with
   the band's own corner — the alignment is the thing you actually read. */
.map-col > .theme-legend { padding-left: 20px; }
/* Every hairline INSIDE the strip was drawn in --n-100 against white. --n-100
   is now the band itself, so each one steps down to --n-200 or it disappears,
   and the chips take back a white surface — a control that dissolves into its
   own background stops reading as a control. */
:is(.map-col, .dash-mapcard) > .theme-legend .tl-title {
  margin: 0; flex: 0 0 auto; align-self: center;
  border-right: 1px solid var(--n-200); padding-right: 12px;
}
:is(.map-col, .dash-mapcard) > .theme-legend .tl-chips { margin-top: 0; flex: 0 0 auto;
  border-top-color: var(--n-200); }
:is(.map-col, .dash-mapcard) > .theme-legend .tl-src { margin-top: 0; flex: 1 1 100%;
  border-top-color: var(--n-200); }
:is(.map-col, .dash-mapcard) > .theme-legend .tl-chip { background: var(--n-000); }
/* The groups take the width the strip has; the title stays a fixed left rail. */
:is(.map-col, .dash-mapcard) > .theme-legend .tl-groups { flex: 1 1 auto; min-width: 0; }
/* 150 → 164 and 132 → 144: the old caps plus the ~9% the tl- type grew (see the
   type block up by .tl-title). These are scroll/clip limits, and a cap left
   behind by its own content is a silent truncation — the failure the chip
   counts exist to prevent. */
:is(.map-col, .dash-mapcard) > .theme-legend { max-height: 164px; }
/* The ~70-district zoning legend already scrolls sideways two rows deep, which
   is exactly the shape a bottom strip wants. Give it the full width. */
:is(.map-col, .dash-mapcard) > .theme-legend.tl-zoning { max-width: none; max-height: 144px; overflow-y: hidden; }
:is(.map-col, .dash-mapcard) > .theme-legend.tl-zoning .tl-zscroll { flex: 1 1 100%; }

/* The panel-reopen chip, docked into the analytics strip's footer. */
.es-foot .cp-open { position: static; inset: auto; z-index: auto; }
.es-foot .cp-open[hidden] { display: none; }

/* The stacked S2 transport block (.ss-anim / .ss-seasons / .ss-speed-wrap and
   a full-bleed .ss-play) was deleted 2026-08-07 — 131px of the old column.
   Play is a 64px transport button on the bar; speed and record moved into the
   "⋯" popover; the December/June pair went entirely, being a second visual
   language for the Dec 21 / Jun 21 chips one row above it. All of it now lives
   in the .solar-slider block at the top of this file. */

/* ── P1 · MASSING & SHADOW rail block (S1, S4) ────────────────────────────── */
.rail-massing { display: flex; flex-direction: column; gap: 6px; padding: 0 2px 2px; }
.mv-toggle { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px;
  background: var(--n-050); border: 1px solid var(--n-150); border-radius: var(--r-control); padding: 3px; }
.mv-btn { font-family: 'Manrope', sans-serif; font-size: 0.66rem; font-weight: 700;
  border: none; background: none; color: var(--n-600); padding: 6px 4px; border-radius: var(--r-control);
  cursor: pointer; }
.mv-btn:hover { color: var(--n-900); }
/* Solid reads red because the state it turns on is red. */
.mv-btn.on { background: var(--c-chrome); color: var(--n-000); }
.mv-btn[data-massing="solid"].on { background: var(--c-risk); }
.mv-check { display: block; font-size: 0.68rem; color: var(--n-800); line-height: 1.3;
  cursor: pointer; padding: 2px 2px 0; }
.mv-check input { accent-color: var(--c-blue-deep); margin-right: 5px; }
.mv-check small { display: block; font-size: 0.58rem; color: var(--n-500); margin-left: 20px; }
/* Was risk-red. A reviewer read the red as a warning and clicked it expecting
   something to be wrong (2026-08-31). Red is the alert colour everywhere else
   in this product; a shortcut to a worked example is not an alert. Chrome. */
.mv-goto { font-family: 'Manrope', sans-serif; font-size: 0.68rem; font-weight: 700;
  border: 1px solid var(--c-chrome); background: var(--c-chrome-wash); color: var(--c-chrome); padding: 6px 8px;
  border-radius: var(--r-control); cursor: pointer; text-align: left; }
.mv-goto:hover { background: var(--c-chrome-wash); }

/* ── P4 · H3 · INCOME SPREAD popup (click a proportional circle) ──────────── */
/* Not a donut. Seven ordered brackets are a SEQUENCE — a donut throws the order
   away and asks the reader to compare arc lengths around a ring, which is the
   one thing a stacked bar does better. The grammar she asked for is "median AND
   the share who are poor", and that is what this prints. */
.isp { width: 300px; font-family: 'Manrope', sans-serif; }
.isp-h { font-weight: 800; font-size: 0.9rem; color: var(--n-900); }
.isp-med { font-size: 0.72rem; color: var(--n-600); margin: 2px 0 9px; }
.isp-med b { color: var(--n-900); font-size: 0.82rem; font-variant-numeric: tabular-nums; }
.isp-bar { display: flex; height: 15px; border-radius: var(--r-control); overflow: hidden;
  background: var(--n-100); margin-bottom: 8px; }
.isp-bar i { display: block; height: 100%; min-width: 1px; }
.isp-key { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 10px;
  font-size: 0.62rem; color: var(--n-600); }
.isp-key span { display: inline-flex; align-items: center; gap: 5px; }
.isp-key i { width: 9px; height: 9px; border-radius: var(--r-control); flex: 0 0 auto; }
.isp-key b { color: var(--n-900); font-variant-numeric: tabular-nums; }
.isp-note { margin-top: 9px; font-size: 0.64rem; line-height: 1.45; color: var(--n-800);
  background: var(--n-050); border-left: 3px solid var(--c-amber); padding: 6px 8px; border-radius: 0 var(--r-control) var(--r-control) 0; }
.isp-note b { color: var(--c-amber-deep); }
.isp-src { margin-top: 7px; font-size: 0.56rem; color: var(--n-400); font-style: italic; }
/* The seven published ACS brackets, under the four display classes. Same ramp,
   finer resolution — never a second palette (B5). */
.isp-sub { margin-top: 10px; font-size: 0.58rem; letter-spacing: 0.06em; font-weight: 800; color: var(--n-500);
  display: flex; align-items: center; gap: 6px; }
.isp-rows { margin-top: 5px; display: flex; flex-direction: column; gap: 2px; }
.isp-row { display: grid; grid-template-columns: 10px 1fr auto; gap: 6px;
  align-items: center; font-size: 0.63rem; color: var(--n-700); position: relative;
  padding-bottom: 3px; }
.isp-row i { width: 10px; height: 10px; border-radius: var(--r-control); }
.isp-row b { color: var(--n-900); font-variant-numeric: tabular-nums lining-nums; }
.isp-row u { position: absolute; left: 16px; bottom: 0; height: 2px;
  border-radius: var(--r-pill); opacity: 0.75; max-width: calc(100% - 16px); }

/* ── DONUT COMPOSITION MARKERS (U1) ────────────────────────────────────────
   DOM markers, not a Mapbox circle layer: a circle layer cannot draw arcs, and
   a marker gives the click target, the focus ring and the accessible name for
   free. NO text on the marker — the breakdown is one click away in the panel,
   which is the documented mitigation for pie-symbol comprehension. */
/* NEVER give this element `position` or `transform`. Both belong to Mapbox.
   `.mapboxgl-marker` is `position:absolute` and Marker._update() writes
   `element.style.transform` inline on every map move. This rule set is
   specificity (0,1,0), identical to Mapbox's, and cambridge.css loads AFTER
   mapbox-gl.css — so anything declared here silently wins.

   That is not hypothetical. `.hood-donut { position: relative }` sat at the
   bottom of this block and turned all 13 markers into in-flow boxes inside
   `.mapboxgl-canvas-container` (which Mapbox leaves static). Measured on the
   live site 2026-09-02, viewport 1440x900: computed position `relative`,
   container `static`, offsetTop accumulating 0 / 60 / 120 / 179 / 235 / 291 as
   each donut pushed the next one down. Mapbox's transform was correct and was
   applied on top of ~500px of accumulated block flow, so the donuts marched
   down and off the map while the canvas painted Cambridge correctly. Reported
   by Joah Massey 2026-08-31 as "clearly inaccurate tracking on the map"; he saw
   it on Zoning too, because it is the same marker class and the same cascade,
   not a per-tab bug.

   Hover and selection scaling therefore go on the inner <svg>, not here — an
   inline transform beats a stylesheet, so `.hood-donut{transform:scale()}`
   never applied either. Same reason the transition lives on the svg: a
   transition on the marker would animate Mapbox's own repositioning and smear
   every donut across the map on pan. */
.hood-donut {
  display: grid; place-items: center; cursor: pointer;
  filter: drop-shadow(0 1px 2px rgba(var(--ink-rgb), 0.28));
}
.hood-donut svg { display: block; transition: transform 0.12s ease; }
.hood-donut:hover svg { transform: scale(1.06); }
.hood-donut:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 3px; border-radius: 50%; }
/* THE ONE PLACE CHROME AND INCOME SHARE SPACE, disclosed rather than hidden.
   These donuts are Mapbox markers drawn in the income macro colours, sitting
   on the income-fill layer, so a selection ring here is a saturated ring on a
   saturated multi-hue field. Measured against the income arcs:
     teal   #2b8577 — 1.89:1 on m3 #1c4f84, but 1.01:1 on m2 #477cb0 (gone)
     chrome #1c5c84 — 1.16:1 on m3 #1c4f84, 1.64:1 on m2 #477cb0
   Neither hue is reliable; chrome is the less bad of the two, and it keeps
   selection consistent with the rest of the shell.

   CORRECTION 2026-09-02: this comment used to argue the 1.16:1 ring was "a weak
   signal rather than a broken one" because ".sel also scales 1.08". It did not.
   `.hood-donut{transform:scale(1.08)}` was a dead rule — Mapbox writes transform
   inline on the marker every move, and inline beats a stylesheet — so the ring
   WAS the only cue, at 1.16:1, which fails AA outright. The scale now lives on
   the inner <svg> where nothing overwrites it, and the claim above is true again.

   The real fix is still a change of FORM, not hue — a white outer halo under the
   ring so it reads against any arc. Queued with the chart-series decision. */
.hood-donut.sel svg { transform: scale(1.08); }
.hood-donut.sel::after {
  content: ''; position: absolute; inset: -6px;
  border: 2px solid var(--c-chrome); border-radius: 50%;
}

/* ── THE ONE POPUP, IN TOOLTIP DRESS (research rule 6) ─────────────────────
   Two lines: name + one headline value. No close button, no pointer events —
   a tooltip you can click is a popup, and popups are what B1 was about. */
.mapboxgl-popup.pop-tip .mapboxgl-popup-content {
  padding: 7px 10px; border-radius: var(--r-control);
  box-shadow: var(--shadow-2); font-family: 'Manrope', sans-serif;
  background: var(--n-000); border: 1px solid var(--n-150);
}
.mapboxgl-popup.pop-tip .mapboxgl-popup-close-button { display: none; }
.mapboxgl-popup.pop-tip { pointer-events: none; max-width: 220px; }
.pop-tip .tip-n { display: block; font-size: 0.74rem; font-weight: 800; color: var(--n-900);
  line-height: 1.25; }
.pop-tip .tip-v { display: block; font-size: 0.68rem; color: var(--n-600);
  font-variant-numeric: tabular-nums lining-nums; line-height: 1.3; }
.pop-tip .tip-h { display: block; font-size: 0.56rem; color: var(--n-400);
  margin-top: 3px; font-style: italic; }

/* ── CONTROLS DRAWER (2026-08-03) ──────────────────────────────────────────
   Quan: "for frame 1 it should show parcel card on the right with layers
   drawer (open)?" — yes, and collapsing the drawer is what turns FRAME 2
   (rail + map + parcel card) into FRAME 1 (map + parcel card). One shell,
   two states, so there is no second layout to keep in sync.

   The rail is position:fixed and .main clears it with margin-left, so
   collapsing RECLAIMS the space for the map instead of sliding a panel over
   it. Rocker's "I want to see the map 100% unobscured" holds in both states,
   which an overlay drawer would not. */
.sidebar.rail { transition: width .18s ease; }
.main         { transition: margin-left .18s ease; }
body.rail-off .sidebar.rail { width: 0; border-right: none; overflow: hidden; }
body.rail-off .main         { margin-left: 0; }

.rail-toggle { display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto;
  margin-right: 12px; padding: 7px 11px; border: 1px solid var(--n-150); border-radius: var(--r-control);
  background: var(--n-000); color: var(--n-900); cursor: pointer;
  font-family: 'Manrope', sans-serif; font-size: 0.74rem; font-weight: 700; }
.rail-toggle:hover  { background: var(--n-025); border-color: var(--n-200); }
/* Was var(--accent, var(--c-blue-deep)) — an alert token with a chrome
   fallback that could never fire, so this focus ring rendered AMBER while the
   other seventeen rendered blue. The dead fallback records what was meant. */
.rail-toggle:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; }
.rail-toggle svg { width: 15px; height: 15px; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; }
.rt-count { font-variant-numeric: tabular-nums lining-nums; color: var(--n-500); font-weight: 600; }

/* ── THEME TOGGLE · sits immediately after Controls, borrows its chrome ──────
   Icon-only by default. There is room for it now only because the nav labels
   were shortened; the header measures 0px of overflow at 1280 both before and
   after this button, which is the number that decides whether it may exist.
   The text label is a >1500px luxury and nothing depends on it. */
.theme-toggle { padding: 7px 9px; margin-right: 8px; }
.theme-toggle svg { width: 15px; height: 15px; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* One <svg> per state, and CSS picks — no icon rebuilt in JS, so the button
   cannot end up showing a state the attribute disagrees with. */
.theme-toggle .tt-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .tt-moon { display: none; }
:root[data-theme="dark"] .theme-toggle .tt-sun  { display: block; }
.tt-label { display: none; }
@media (min-width: 1501px) { .tt-label { display: inline; } .theme-toggle { padding: 7px 11px; } }
/* Neighborhood analytics is full-page: the drawer is not just closed, it is
   gone, because none of its map controls apply there (directive U3). */
body.view-full .rail-toggle { display: none; }

/* S1 — a snapshot past its refresh window says so rather than looking current. */
/* --c-amber-ink is not defined anywhere, so this was rendering its raw #a9781f
   fallback — a retired 3.60:1 literal hiding behind a dangling token, and the
   last raw amber hex below the token block. Pointed at the alert token it
   always meant. */
.is-stale { color: var(--c-amber); font-weight: 700; }

/* G1 — a Drive file that is not link-shared says so, rather than vanishing. */
.pj-nomedia { padding: var(--s-3, 12px); font-size: 0.76rem; line-height: 1.45;
  color: var(--c-slate-600, #5b6b78); background: var(--c-slate-050, #f4f6f8);
  border: 1px dashed var(--c-line, #dde3e8); border-radius: var(--r-control, 8px); }

/* Projects the tracker holds but we could not geocode — stated, never dropped. */
.cp-unloc { display: block; margin-top: 4px; color: var(--c-slate-600, #5b6b78); font-size: 0.72rem; }

/* Tabs that navigate to a real page rather than switching a view. They are
   links, not spans, so they open in a new tab, get focus rings and can be
   middle-clicked — a disabled <span> could do none of that. */
.t360-link { text-decoration: none; }
.t360-link:hover { color: var(--n-900); }

/* ── F1 · NEIGHBORHOOD IN FOCUS ─────────────────────────────────────────── */
.focus-card { border-left: 3px solid var(--c-chrome); }
.focus-name { font-family: 'Instrument Serif', Georgia, serif; font-size: 1.55rem;
  line-height: 1.15; margin: var(--s-2) 0 var(--s-1); }
.focus-line { font-size: 0.8rem; color: var(--n-800); }
.focus-tie { font-size: 0.7rem; color: var(--n-600); margin-top: 3px; font-style: italic; }
.focus-tbl { width: 100%; border-collapse: collapse; margin-top: var(--s-3); font-size: 0.72rem; }
.focus-tbl td { padding: 3px 0; border-bottom: 1px solid var(--n-100); color: var(--n-700); }
.focus-tbl td.n { text-align: right; font-variant-numeric: tabular-nums lining-nums; white-space: nowrap;
  padding-left: var(--s-3); }
.focus-tbl tr:last-child td { border-bottom: 0; }
/* The method note is the point of the card, not fine print — it explains why
   the cadence is not the weekly one that was asked for. */
.focus-why { margin-top: var(--s-3); padding: var(--s-3); border-radius: var(--r-control);
  background: var(--n-050); font-size: 0.7rem; line-height: 1.5; color: var(--n-700); }
.focus-voice { margin-top: var(--s-2); font-size: 0.68rem; color: var(--n-500); font-style: italic;
  border: 1px dashed var(--n-200); border-radius: var(--r-control); padding: var(--s-2) var(--s-3); }

/* ================================================================
   MAP UNDERLAY — one block, two columns
   ================================================================
   Rocker 2026-08-03, clarified 08-05: "nothing on the bottom" was
   never a request to DELETE the bottom strip — the 07-30 record
   ("yeah, both") stands. What she meant is that the two stacked
   blocks under the map eat too much height: "the income tab takes
   too much space… can we instead make into one table but in two
   columns."

   Stacked, the map key (max 164px) and the selected-parcel strip
   (max 208px) cost up to ~372px plus margins. Side by side they
   cost max(164, 208) = 208px — about 164px of map height returned,
   which is the same fight as the map-height clamp above.

   Done by gridding .map-col itself rather than wrapping the two in
   a container: nine selectors above depend on
   `:is(.map-col, .dash-mapcard) > .theme-legend` being a DIRECT
   child, and a wrapper would silently break every one of them.

   2026-08-13 — THE RIGHT CELL IS NOW .map-footer, AND IT CARRIES THE
   SHADOW TOOL. Rocker: "what is currently floating on the map, we put
   down there. So their shadow tool is always with us… And then the
   other one on the left side is the legend map." So the row still has
   exactly two cells — LEGEND left, SHADOW right — and the selection
   strip rides inside the right one under the bar rather than claiming
   a third cell, because a third cell is a third band row and the
   standing note is that the map is too small.

   .theme-legend stays a DIRECT child for the reason above. Only
   .explore-strip moved, into .map-footer; its six `> .explore-strip`
   selectors moved with it and are the `.map-footer > .explore-strip`
   rules below.
   ---------------------------------------------------------------- */
.content-row .map-col {
  display: grid;
  /* NOT A RATIO — A WIDTH THE SHADOW BAR ACTUALLY NEEDS, AND THE REST TO THE
     LEGEND. The right cell used to be 1.15fr because it held five analytic
     cards; it now holds the transport bar, whose height is a step function of
     its width. Measured 2026-08-17 by driving .map-footer's width directly:

        cell width   bar rows / height   footer (bar + 8 + strip)
        >= 420px         2 / 108px             153px   ← fits the band
        380-400          3 / 160px             219px
        <= 360           4 / 212px             271px

     420 is not arbitrary: the bar's first row is dates 222 + all-hours 74 +
     play 64 + two 10px gaps = 380, and 380 + 12+12 padding + 2 border = 406.
     420 is that with headroom. Below it the bar takes a third row, the footer
     overtakes the LEGEND as the band's tallest cell, and the band grows — i.e.
     the map pays for the bar's wrapping, which is the whole thing this move was
     supposed to avoid.

     So: the right track is `minmax(0, 420px)` — it takes 420 when there is 420
     to take and shrinks rather than overflowing when there is not — and the
     legend takes everything left over, with a 200px floor so it never
     collapses. One rule for every topic; the columns no longer jump width when
     you change topic.

     The floor is 200 and not 260 for a measured reason: at 1280 with the left
     rail OPEN the band has 624px to divide, and a 260px floor leaves the footer
     404px — fourteen pixels under the cliff, which costs the map 43px of height
     to buy the legend 56px of width. The legend is height-capped and scrolls;
     the map is not. Between roughly 1100 and 1230 with the rail open there is
     not enough width for both and the band does grow by ~43px; below 1100 the
     pair stacks (see the breakpoint at the end of this block). */
  grid-template-columns: minmax(200px, 1fr) minmax(0, 420px);
  grid-template-rows: minmax(0, 1fr) auto;
  align-content: stretch;
  column-gap: 14px;
}
.content-row .map-col > .map-canvas { grid-column: 1 / -1; grid-row: 1; }

/* Two cells, each its own hairline card. The single-divider "one block"
   treatment retired with the move: it was two halves of one strip, and the
   right half is now a stack of two controls, which cannot butt a shared
   border without drawing that border twice. */
.content-row .map-col > .theme-legend {
  grid-column: 1; grid-row: 2;
  margin: 0 0 16px 20px;
  border: 1px solid var(--n-150);
  border-radius: var(--r-panel);
  background: var(--n-050);
  max-height: 208px; overflow-y: auto;
  padding: 10px 14px;
}

/* ── .map-footer — the band's right cell ────────────────────────────────────
   A bare flex column: no border, no padding, no background of its own. Its
   two children are already cards, and a box around two boxes is a third box.
   That also makes it FREE when the solar bar is display:none — an empty flex
   column with no padding is 0px tall, so on every non-shadow topic this cell
   is exactly the selection strip and nothing more, which is what it was
   before the move.

   Measured at 1440 (2026-08-17): legend 176px vs footer 100px (56 bar + 8 gap
   + 36 strip) on the Environment topic, so the ROW is 176px — set by the
   legend, unchanged from before the move, and the canvas keeps its 587px. */
.content-row .map-col > .map-footer {
  grid-column: 2; grid-row: 2;
  margin: 0 14px 16px 0;
  display: flex; flex-direction: column; gap: 8px;
  min-width: 0; align-self: start;
}
.content-row .map-col > .map-footer > .explore-strip {
  margin: 0;
  border-radius: var(--r-panel);
  max-height: 208px;
}
/* The map key carries the quieter surface, so its chips need to step back
   up to the card colour to still read as controls against it. */
.content-row .map-col > .theme-legend .tl-chip { background: var(--n-000); }

/* The shadow topic used to get its OWN column split (1.25fr/0.75fr) to buy the
   legend width for the era toggle. That override is gone: the track sizing
   above is already "the bar's 420px, and the rest to the legend", which gives
   the legend MORE than the old override did at every width it was measured at
   (616px at 1440 against 649px before, with the band 29px shorter).

   max-height still gets headroom on the shadow topic: it only ever clips, so it
   costs nothing while the content fits, and it stops a silent truncation if it
   stops fitting. A cap left behind by its own content is the failure the
   "+N more" chips exist to prevent. */
.content-row .map-col:has(> .theme-legend .sera-cell) > .theme-legend,
.content-row .map-col:has(> .theme-legend .sera-cell) > .map-footer > .explore-strip {
  max-height: 256px;
}

/* With no subject layer the legend is [hidden]; the footer then takes the full
   width rather than leaving a dead column. */
.content-row .map-col:has(> .theme-legend[hidden]) > .map-footer {
  grid-column: 1 / -1; margin-left: 20px;
}

/* Below the two-column threshold the pair STACKS — legend over shadow. Two
   ~400px columns would put the legend's swatch groups and the transport bar's
   scrubber under the same squeeze at the same time, and the bar's scrubber has
   a 300px flex basis it cannot go under without wrapping to a third row. One
   full-width legend over one full-width footer is the honest shape. */
@media (max-width: 1100px) {
  .content-row .map-col { grid-template-columns: minmax(0, 1fr); }
  .content-row .map-col > .theme-legend {
    grid-column: 1; grid-row: 2; margin: 0 14px 8px 20px;
  }
  .content-row .map-col > .map-footer {
    grid-column: 1; grid-row: 3; margin: 0 14px 16px 20px;
  }
  /* No extra legend cap is needed here, and one was measured and removed rather
     than left in place doing nothing: at 1000 the stacked band is legend 176 +
     8 + footer 153 = 337, against 352 for the two stacked strips it replaces,
     so the map is 15px TALLER stacked than it was before the move. The bar gets
     the full column width and still renders two rows (its first row needs 380px
     and its scrubber another 347). */
}

/* ================================================================
   SOLAR TIME — no longer anchored to, or over, the MAP
   ================================================================
   Quan 2026-08-05: "solar time should be in the map bottom." It got there in
   two steps: out of the rail and onto the canvas's bottom edge (2026-08-07),
   then off the canvas entirely and into the bottom band (2026-08-13, Rocker:
   "what is currently floating on the map, we put down there").

   THREE RULES DIED WITH THAT SECOND MOVE, and none of them is coming back:
     · `.solar-slider { position:absolute; left/right/bottom:12px }` — the bar
       is a flex child of .map-footer now, see the top of this file;
     · `.map-canvas.has-solarbar .mapboxgl-ctrl-bottom-* { bottom: calc(16px +
       var(--solarbar-h)) }` — the Mapbox scale and attribution had to step up
       over the bar while it covered the canvas. Nothing covers the canvas, so
       they sit where Mapbox puts them;
     · `--solarbar-h`, published by a ResizeObserver in cambridge.js, which had
       exactly one reader — the rule above. Both are deleted.
   The canvas now carries the north arrow and the scale bar and NOTHING else,
   which is the standing rule ("I don't want anything in the map").
   ---------------------------------------------------------------- */

/* ── WHOSE SHADOW — moved out of the solar bar and into the LEGEND BAND
   2026-08-07. It is a "what am I looking at" control, not a "when" control:
   same class as the swatches it now sits beside, it costs the canvas zero
   pixels, and the band already carries the provenance line the two envelopes
   need. It stays a first-class control — it is the newest and most
   argumentative thing on this topic — and the label is always the full
   "Ord 2025-1", never "New": that string IS the finding.
   Each era is paired with the colour it draws on the map, so the toggle and
   the legend read as one object. ── */
/* `Shadow [Old rule|Both|Ord 2025-1]` on one line, the sun caption beneath,
   as the FIRST cell of the band's group flow. It takes a full row of that flow
   — measured: at the band's ~440px there is nothing it can share a line with —
   which is why the band's cap is raised alongside it. Two other shapes were
   built and measured first: a stacked .tl-group (71px, worse) and a narrow
   vertical stack beside .tl-title (that one stole 136px of width from the
   groups and reflowed them 40px TALLER — the band's binding constraint is
   width, not height). */
.sera-cell { flex: 1 1 100%; flex-direction: row; flex-wrap: wrap;
  align-items: center; gap: 3px 8px; }
.sera-cell .tl-gname { flex: 0 0 auto; }
.sera-cell .sera-toggle { display: inline-flex; border: 1px solid var(--n-200);
  border-radius: var(--r-control); overflow: hidden; background: var(--n-000); }
.sera-cell .tl-gsub { flex: 1 1 100%; margin: 0; }
.sera-btn { display: inline-flex; align-items: center; gap: 5px;
  border: 0; background: transparent; cursor: pointer;
  font-family: 'Manrope', sans-serif; font-size: 0.7rem; font-weight: 700; color: var(--n-700);
  padding: 5px 8px; white-space: nowrap; }
.sera-btn + .sera-btn { border-left: 1px solid var(--n-200); }
.sera-btn:hover { background: var(--n-050); color: var(--n-900); }
.sera-btn[aria-pressed="true"] { background: var(--c-chrome); color: var(--n-000); }
.sera-btn:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: -2px; }
/* The swatches ARE the map's own casts: the pre-2025 envelope is drawn as a
   dashed outline at low fill, Ord 2025-1's as a solid 150/255 cast. */
.sera-sw { width: 11px; height: 11px; border-radius: 3px; flex: 0 0 auto; }
.sera-sw-old  { background: rgba(var(--ink-rgb),0.16); border: 1px dashed rgba(var(--ink-rgb),0.75); }
.sera-sw-new  { background: rgba(var(--ink-rgb),0.59); border: 1px solid rgba(var(--ink-rgb),0.47); }
.sera-sw-both { background: linear-gradient(135deg,
                  rgba(var(--ink-rgb),0.16) 0 50%, rgba(var(--ink-rgb),0.59) 50% 100%);
                border: 1px solid rgba(var(--ink-rgb),0.47); }
.sera-btn[aria-pressed="true"] .sera-sw { border-color: rgba(var(--white-rgb),0.85); }
/* 11px / --n-600, not the band's shared 0.6rem-on---n-400: this caption is
   load-bearing (it is the sun altitude the shadow is cast from) and the shared
   style is under both DESIGN.md's 11px caption floor and 4.5:1. */
.sera-cell .tl-gsub { font-variant-numeric: tabular-nums; font-size: 11px;
  color: var(--n-600, #586b7d); max-width: none; }

/* ── LAYER ON/OFF CHIPS, in the same cell ───────────────────────────────────
   Rocker 2026-08-13: "we can keep on the bottom the navigation between legend
   on the left side and on the right side. For example, we can show tree canopy
   on, off, or we can show some shade on and off. You know. Flooding condition
   on and off." And earlier, on the shadow: "I wonder whether we could make that
   more direct in the legend … shadow on off kind of option."

   They render in the shadow cell because that is where "what am I looking at"
   already lives, and they reuse .sera-btn's geometry outright — one control
   language for the whole cell, and no second set of sizes to keep in step.
   The only difference is the swatch: a filled dot in the LAYER'S OWN colour
   when on, a hollow ring when off, so the chip says which layer as well as
   whether. Colours come from the registry row, not from this file.

   EVERY CHIP IS A REMOTE CONTROL, not a state store: it clicks the dock's own
   checkbox and then re-reads it. renderLayersPanel stays the single source of
   layer state and the two can never drift. See sysChipHTML() in cambridge.js. */
.sera-cell .sera-sys { display: inline-flex; flex-wrap: wrap; gap: 4px;
  align-items: center; }
.sys-chip { display: inline-flex; align-items: center; gap: 5px;
  border: 1px solid var(--n-200); border-radius: var(--r-pill);
  background: var(--n-000); cursor: pointer;
  font-family: 'Manrope', sans-serif; font-size: 0.7rem; font-weight: 700;
  color: var(--n-700); padding: 4px 9px; white-space: nowrap; }
.sys-chip:hover { background: var(--n-050); color: var(--n-900); }
.sys-chip[aria-pressed="true"] { background: var(--c-chrome); border-color: var(--c-chrome);
  color: var(--n-000); }
.sys-chip:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; }
/* The dot carries the layer's colour via an inline custom property, so no
   layer palette is duplicated here. Off = the ring only, which reads as
   "this is the thing, and it is not on". */
.sys-sw { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto;
  border: 1.5px solid var(--sys-c, currentColor); background: transparent; }
.sys-chip[aria-pressed="true"] .sys-sw { background: var(--sys-c, currentColor);
  border-color: rgba(var(--white-rgb),0.85); }

/* ── COMPACT INCOME STRIP ───────────────────────────────────────────────────
   The seven-bracket table and its method note used to run ~300px inside a
   strip already competing with the map, against a standing note that the map
   is too small. They now fold into a disclosure. Nothing is deleted: the
   detail is one click away and the provenance chip rides on the summary, so
   the source is visible whether the block is open or shut. */
.isp-lead {
  font-size: 0.72rem; line-height: 1.35; color: var(--n-700, #3f4b52);
  margin: 7px 0 2px;
}
.isp-lead b { color: var(--n-900, #23282c); font-variant-numeric: tabular-nums; }

.isp-more { margin-top: 6px; }
.isp-more > summary {
  cursor: pointer; list-style: none;
  font-size: 0.6rem; letter-spacing: .04em; text-transform: uppercase;
  font-weight: 700; color: var(--n-600, #5b7282);
  padding: 4px 0; display: flex; align-items: center; gap: 6px;
}
.isp-more > summary::-webkit-details-marker { display: none; }
/* the caret is drawn rather than inherited, so the row height is predictable */
.isp-more > summary::before {
  content: ''; width: 0; height: 0; flex: 0 0 auto;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent;
  transition: transform .12s ease;
}
.isp-more[open] > summary::before { transform: rotate(90deg); }
/* Was var(--c-teal-deep, #1c5c84): a teal token carrying a Spinellblau
   fallback that could never fire. The file had been drifting to the house
   value on its own; this just makes it honest. */
.isp-more > summary:hover { color: var(--c-chrome); }
.isp-more > summary:focus-visible {
  outline: 2px solid var(--c-chrome); outline-offset: 2px; border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .isp-more > summary::before { transition: none; }
}

/* ── BOTTOM BAND COMPACTION (V3) ────────────────────────────────────────────
   Measured 2026-08-08: the band's height was never set by its content. It was
   two literals — max-height 208px, and 256px on shadow topics — and the strip's
   content was 456-471px, so ~260px sat behind a scrollbar. At 1440 the band was
   37.5% of the map and 50.5% on the climate topic, against a standing client
   note that the map is too small.
   Nothing here deletes data. The analytic cards move to the Neighborhood
   analytics tab (#es-more now opens it), the seven brackets stay in the
   disclosure, and every provenance string is relocated rather than dropped. */
/* `> .explore-strip` became `> .map-footer > .explore-strip` on 2026-08-13 —
   the strip moved one level down, into the band's right cell, under the shadow
   bar. A cap that no longer selects anything is worse than no cap: it reads as
   protection that is not there. */
.content-row .map-col > .theme-legend,
.content-row .map-col > .map-footer > .explore-strip { max-height: 112px; }
.content-row .map-col:has(> .theme-legend .sera-cell) > .theme-legend,
.content-row .map-col:has(> .theme-legend .sera-cell) > .map-footer > .explore-strip { max-height: 176px; }

/* STACKED, the legend gives some of that cap back. Below 1100px the legend and
   the footer are two full-width rows, so the band is their SUM rather than the
   larger of the two, and the shadow topic's 176px legend would put the band at
   176 + 8 + 153 = 337. 132 puts it at 293. The legend is the cell that scrolls
   and it has the full column width here, so 132px shows roughly the same number
   of swatch rows a 176px cap showed at 470px wide.
   (Placed here, after the caps above, because it has to out-order them: same
   specificity, and the 176 rule would otherwise win on source order.) */
@media (max-width: 1100px) {
  .content-row .map-col:has(> .theme-legend .sera-cell) > .theme-legend { max-height: 132px; }
}

/* Header line: selection, dimension control, actions. Then the lead beneath.
   The old 160px | 1fr | auto grid squeezed the charts to 300px, which stacked
   five cards vertically with 88px of gap. */
.es-main {
  grid-template-columns: minmax(0, 1fr) auto auto;
  grid-template-areas: "sel dims acts" "lead lead lead";
  align-items: center; gap: 5px 12px; padding: 6px 14px 7px;
}
.es-sel { grid-area: sel; flex-direction: row; align-items: baseline; gap: 7px;
  min-width: 0; overflow: hidden; }
.es-sel b { font-size: 0.78rem; }
.es-dims { grid-area: dims; display: inline-flex; align-items: center; gap: 6px; }
.es-acts { grid-area: acts; display: inline-flex; align-items: center; gap: 10px; }
.es-lead { grid-area: lead; }
/* The cards live in the analytics tab now; the node stays so the renderer and
   any future inline view keep working. */
.es-main > .es-charts { display: none; }

.es-dimlab { font-size: 0.69rem; color: var(--n-600, #586b7d); }
.es-dimsel {
  font: 500 0.72rem/1.2 'Manrope', sans-serif; color: var(--n-800, #2f3b45);
  background: var(--n-000, #fff); border: 1px solid var(--n-200, #d7dee3);
  border-radius: var(--r-control, 4px); padding: 3px 6px;
}
.es-dimsel:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; }

/* One line of figures. Each item carries its own label and city comparison, so
   the row stays readable when it wraps. */
.es-lead { display: flex; flex-wrap: wrap; gap: 4px 18px; align-items: baseline; }
.esl { display: inline-flex; align-items: baseline; gap: 5px; font-size: 0.72rem; }
.esl i { font-style: normal; color: var(--n-600, #586b7d); }
.esl b { font-size: 0.84rem; color: var(--n-900, #1f272d); font-variant-numeric: tabular-nums; }
.esl u { text-decoration: none; font-size: 0.69rem; color: var(--n-600, #586b7d); }

/* Legend: the +N chip rides the group-name line instead of taking its own.
   :not(.sera-cell) matters — this block is appended after css:2408, where the
   shadow cell sets its own flex layout but inherits `display` from .tl-group.
   Without the exclusion, `display:grid` here silently rebuilds that cell and
   the era toggle plus the sun-altitude readout lose their row. */
.tl-group:not(.sera-cell) {
  display: grid; grid-template-columns: auto auto;
  grid-template-areas: "name chip" "rows rows";
  justify-content: start; align-items: center; gap: 1px 8px;
}
.tl-group:not(.sera-cell) > .tl-gname { grid-area: name; }
.tl-group:not(.sera-cell) > .tl-chip { grid-area: chip; margin-top: 0; padding: 1px 7px; }
.tl-group:not(.sera-cell) > .tl-rows { grid-area: rows; grid-template-rows: repeat(2, auto); gap: 1px 14px; }
.tl-groups { gap: 4px 14px; }

/* The relocated provenance, now above the 11px floor and at 4.89:1 rather
   than the 2.26:1 it had per-group. */
.tl-prov {
  flex: 1 1 100%; margin: 3px 0 0;
  font-size: 0.69rem; line-height: 1.35; font-style: italic;
  color: var(--n-600, #586b7d);
}
.tl-prov b { font-style: normal; font-weight: 700; color: var(--n-700, #46596b); }

/* Type floors. These were already failing both the 11px caption floor and the
   4.5:1 contrast floor at 9.28-9.92px on --n-400 (2.54:1). Fixed, not traded. */
.es-c h5, .isp-src, .es-rank small, .es-key {
  font-size: 0.69rem; color: var(--n-600, #586b7d);
}

/* cp-open was neutralised by `.es-foot .cp-open` (css:2107). That row no longer
   exists, so the same reset rides its new home — without this the chip returns
   to floating over the canvas, which is what docking it fixed. */
.es-acts .cp-open { position: static; inset: auto; z-index: auto; }
.es-acts .cp-open[hidden] { display: none; }
.es-acts .es-more, .es-acts .cp-open { white-space: nowrap; }

/* ── NEIGHBORHOOD TAB (#cp-neighborhood) ────────────────────────────────────
   Rocker 2026-08-13: "what about if we could click parcel information and next
   to is another tab and we would do neighborhood information", with the
   constraint stated in the same breath — "the map stays exactly how it is. You
   toggle on or off. It's like an Excel spreadsheet." The panel is a fixed
   336px, so a tab costs the map nothing; this block only re-flows the strip's
   own cards into one column, it authors no new type, colour or radius.

   The cards' markup is unchanged — .es-c, .es-vs, .es-stack, .es-key,
   .es-donut, .isp-* — because it is literally esCharts() and
   incomeDetailHTML() output. Same four income display classes, same seven ACS
   brackets, same VARIABLE_COLORS.income the choropleth reads. */
.nb-sel { display: flex; flex-direction: column; gap: 1px; margin-bottom: 8px; }
.nb-sel b { font-size: 0.86rem; color: var(--n-900); }
.nb-dims { margin-bottom: 10px; }
.nb-lead:not(:empty) { margin-bottom: 10px; }
/* One column at 336px: the strip's row of five cards would otherwise stack as
   five 300px-wide items with 22px gaps and read as a gap-first list. */
.nb-charts { flex-direction: column; gap: 14px; }
.nb-charts .es-c { min-width: 0; width: 100%; }
.nb-charts .es-c-wide { flex: 1 1 auto; min-width: 0; max-width: none; }
/* The seven brackets go two-up in the wide strip; a 336px panel is a one-up
   column, so the disclosure stops being a squeeze of two clipped labels. */
.nb-charts .es-c-wide .isp-rows { grid-template-columns: 1fr; }
.nb-charts .es-key { max-width: none; }
/* The line clamp existed to stop a 3-line method note owning the bottom band.
   There is no band pressure here — this pane scrolls — so the note runs full. */
.nb-charts .es-c-wide .isp-note { -webkit-line-clamp: none; overflow: visible; }

/* ============================================================================
   CANVAS CONTROLS — north arrow + graphic scale
   ============================================================================
   The only two things permitted on the map (Rocker's standing exception:
   "a classic map has a title, and has a scale and where N is"). Everything
   is drawn from existing tokens, so both invert correctly under
   :root[data-theme="dark"] without a single dark-mode override below.
   ========================================================================== */

/* NO HARD PLATE. A solid white disc on a light basemap reads as a chip
   someone dropped on the map; the reference sketches are line drawings sitting
   directly on the sheet. So the ground is a translucent wash of the surface
   token — enough to hold the glyph over dark parcels, water or a satellite
   tile, not enough to look like a control. color-mix keeps it token-derived,
   so it inverts with the theme and adds no raw hex. */
.cam-north {
  background: color-mix(in srgb, var(--n-000) 72%, transparent);
  backdrop-filter: blur(2px);
  border-radius: 999px;
  box-shadow: none;
  padding: 0;
  width: 44px;
  height: 44px;
}
.cam-north-svg { width: 44px; height: 44px; display: block; }
/* Rotation happens about the RING's centre, not the SVG box — the N sits
   above the circle, so a box-centre origin would swing the letter in an arc. */
.cam-north-rot { transform-origin: 22px 24px; }
.cam-north-ring   { fill: none; stroke: var(--n-700); stroke-width: 1.1; }
.cam-north-needle   { fill: var(--n-900); }   /* solid half = north */
.cam-north-needle-s { fill: var(--n-900); }
.cam-north-tail     { fill: none; stroke: var(--n-700); stroke-width: 1; }
.cam-north-n {
  fill: var(--n-900);
  font: 700 8.5px/1 'Manrope', sans-serif;
  letter-spacing: 0.06em;
}

.cam-scale {
  background: color-mix(in srgb, var(--n-000) 72%, transparent);
  backdrop-filter: blur(2px);
  border-radius: var(--r-control, 6px);
  box-shadow: none;
  padding: 4px 8px 3px;
  margin-bottom: 6px;
}
.cam-scale-svg { display: block; overflow: visible; }
/* The checkerboard. `is-fill` alternates; the rest are hollow with the same
   stroke, which is what makes the segments countable rather than a gradient. */
.cam-scale-seg {
  fill: var(--n-000);
  stroke: var(--n-900);
  stroke-width: 0.9;
}
.cam-scale-seg.is-fill { fill: var(--n-900); }
.cam-scale-lab {
  fill: var(--n-900);
  font: 600 8.5px/1 'Manrope', sans-serif;
}
.cam-scale-unit {
  fill: var(--n-700);
  font: 600 8.5px/1 'Manrope', sans-serif;
}

/* On a translucent ground the glyph carries its own legibility. A paint-order
   halo is what a cartographer would use — it survives a dark parcel, water and
   a satellite tile without a box, and costs nothing when the ground is pale. */
.cam-scale-lab,
.cam-scale-unit,
.cam-north-n {
  paint-order: stroke fill;
  stroke: var(--n-000);
  stroke-width: 2.2px;
  stroke-linejoin: round;
}

/* ── ANALYTICS · THE FOLDED CONSOLE ─────────────────────────────────────────
   The three-column console was this tab until 2026-09-02, when the Explorer
   orbit took the surface. It is folded rather than deleted because it holds the
   Neighborhood Index, which the reviewer who asked for the merge also asked to
   keep: "the neighborhood index is a really nice tab and that should stay."
   Closed by default — the point of the merge was fewer things at once. */
.dash-more { margin: 18px 0 0; border-top: 1px solid var(--line); padding-top: 14px; }
.dash-more > summary {
  cursor: pointer; list-style: none; display: flex; flex-direction: column; gap: 2px;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-control);
  background: var(--c-chrome-wash);
}
.dash-more > summary::-webkit-details-marker { display: none; }
.dash-more > summary:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; }
.dash-more-t { font-weight: 700; font-size: 0.82rem; color: var(--c-chrome); }
.dash-more-t::after { content: ' ▾'; }
.dash-more[open] .dash-more-t::after { content: ' ▴'; }
.dash-more-s { font-size: 0.68rem; color: var(--n-600); }
.dash-more .dash-console { margin-top: 14px; }

/* ── WHAT'S NEXT ────────────────────────────────────────────────────────────
   Public meetings, this week's updates, and the tested findings. Built
   2026-09-02 for the review task that was abandoned rather than failed.

   The place question is the wide column and the calendar sits under it, in that
   order and not the other way round: the reader this site names wants to know
   what is happening on their street, and "the Planning Board meets Tuesday" is
   the right answer to a question they have not asked yet. */
.nv-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
  gap: 16px; align-items: start; }
.nv-main, .nv-side { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
@media (max-width: 1040px) { .nv-grid { grid-template-columns: 1fr; } }

.nv-sr { position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }

.nv-place h3 { margin: 0 0 4px; }
.nv-place-sub { margin: 0 0 12px; font-size: 0.78rem; color: var(--n-600); max-width: 62ch; }
.nv-search { display: flex; gap: 8px; flex-wrap: wrap; }
.nv-search input {
  flex: 1 1 260px; min-width: 0; height: 40px; padding: 0 12px;
  font: inherit; font-size: 0.86rem; color: var(--n-900);
  border: 1px solid var(--line); border-radius: var(--r-control); background: var(--n-000);
}
.nv-search input:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 1px; }
.nv-search button {
  height: 40px; padding: 0 16px; font: inherit; font-size: 0.8rem; font-weight: 700;
  color: var(--n-000); background: var(--c-chrome); border: 0;
  border-radius: var(--r-control); cursor: pointer;
}
.nv-search button:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; }
.nv-result { margin-top: 12px; }
.nv-hits { list-style: none; margin: 0 0 8px; padding: 0; display: flex;
  flex-direction: column; gap: 6px; }
.nv-hit { display: flex; flex-direction: column; gap: 2px; width: 100%; text-align: left;
  padding: 9px 11px; border: 1px solid var(--line); border-radius: var(--r-control);
  background: var(--n-000); cursor: pointer; font: inherit; }
.nv-hit:hover { background: var(--c-chrome-wash); border-color: var(--c-chrome); }
.nv-hit:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 1px; }
.nv-hit-a { font-weight: 700; font-size: 0.84rem; color: var(--n-900); }
.nv-hit-m { font-size: 0.7rem; color: var(--n-600); }
.nv-empty { font-size: 0.8rem; color: var(--n-600); line-height: 1.6; margin: 0; }

.nv-meet-hd { display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.nv-meet-hd h3 { margin: 0; }
/* The as-of line is ALWAYS rendered, not only when the list is empty. This is
   the most time-sensitive content on the site — being wrong means a reader
   misses a hearing — and it is the one figure that must never be undated. */
.nv-asof { font-size: 0.66rem; letter-spacing: .04em; text-transform: uppercase;
  font-weight: 700; color: var(--n-600); }
.nv-asof.is-stale { color: var(--c-amber); }

.nv-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.nv-table th, .nv-table td { text-align: left; padding: 8px 10px 8px 0;
  border-bottom: 1px solid var(--line); vertical-align: top; }
.nv-table thead th { font-size: 0.64rem; letter-spacing: .07em; text-transform: uppercase;
  color: var(--n-600); border-bottom-width: 2px; }
.nv-table tbody th { font-weight: 700; color: var(--n-900); }
.nv-table tr.is-off tbody th, .nv-table tr.is-off td { opacity: .55; }
.nv-table tr.is-off .nv-day, .nv-table tr.is-off .nv-time { text-decoration: line-through; }
.nv-day { display: block; font-weight: 700; color: var(--n-900); }
.nv-time { display: block; font-size: 0.72rem; color: var(--n-600); }
.nv-none { color: var(--n-600); font-style: italic; }
/* Colour is never the only carrier: the chip has a text label, per DESIGN.md. */
.nv-dev { display: inline-block; margin-left: 7px; padding: 1px 6px; border-radius: 999px;
  font-size: 0.58rem; letter-spacing: .06em; text-transform: uppercase; font-weight: 800;
  color: var(--c-chrome); background: var(--c-chrome-wash); border: 1px solid var(--line); }
.nv-cancel { display: inline-block; margin-left: 7px; padding: 1px 6px; border-radius: 999px;
  font-size: 0.58rem; letter-spacing: .06em; text-transform: uppercase; font-weight: 800;
  color: var(--c-amber); background: var(--c-amber-wash); }

.dash-xlink .cp-weekly { margin-top: 8px; }

/* .cp-weekly is a full-width block button in the side panel it was written for.
   In the pinned bar it must sit inline beside its sibling, so the width and
   display are overridden here rather than by forking the class. */
.about-pin .cp-weekly { width: auto; display: inline-flex; align-items: center;
  margin: 0; flex: 0 0 auto; }
/* The bar is a child of the grid, so it spans both columns and stops at the
   document's own width instead of the viewport's. */
.about-pin { max-width: 100%; }

/* ── HOW MUCH DETAIL ────────────────────────────────────────────────────────
   One bit, in the rail. Matches .view-toggle so it reads as the same kind of
   control as the two above it rather than a new species. */
.rail-detail { padding: 0 12px 4px; }
.detail-toggle { display: flex; gap: 0; border: 1px solid var(--line);
  border-radius: var(--r-control); overflow: hidden; }
.dt-btn { flex: 1 1 0; padding: 6px 8px; border: 0; background: var(--n-000);
  font-family: 'Manrope', sans-serif; font-size: 0.68rem; font-weight: 700;
  color: var(--n-600); cursor: pointer; }
.dt-btn + .dt-btn { border-left: 1px solid var(--line); }
.dt-btn.is-on { background: var(--c-chrome); color: var(--n-000); }
.dt-btn:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: -2px; }
.rail-note { margin: 6px 0 0; font-size: 0.62rem; line-height: 1.45; color: var(--n-600); }

/* Plain folds the workings. It never REMOVES them — the summary line below each
   fold says what is inside, because a control that hides content silently
   contradicts this product's own rule that every figure declares itself. */
body:not(.detail-full) .en-expand > .pc-expand-body { display: none; }
body.detail-full .en-expand { }
/* Says what the fold is hiding, so Plain never removes anything silently. */
.en-folded { font-size: 0.6rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--c-chrome); margin-left: 6px; }

/* ═══════════════════════════════════════════════════════════════════════════
   SMALL SCREENS — the tier DESIGN.md claimed and the stylesheet did not have
   ═══════════════════════════════════════════════════════════════════════════
   DESIGN.md:207 promised "responsive tiers per design-review r4 §D3 (1024
   icon-rail, 768 bottom-sheet, <768 read-only summary)". None of it existed.
   The smallest layout rule in this file governed .about-grid at 820px.

   MEASURED FIRST, 2026-09-02, Zoning tab, before writing any of this:

     width   header scrollWidth   rail x   .main x   map canvas width
     390px   1375                 -232     232       0
     768px   1375                 -232     232       182
     1024px  1375                 -232     232       438

   Three separate faults, all invisible at 1440:

   1. THE HEADER IS 1375px WIDE AT EVERY VIEWPORT. Not clipped gracefully —
      the nav starts at x=674 and Donate ends at 1375, so below about 1150px the
      tabs are simply unreachable and below 700px only the wordmark is on
      screen. There is no way to change tab on a phone.

   2. .main RESERVES 232px FOR A RAIL THAT IS NOT THERE. The 1024px rule at
      line 531 translates the rail off-canvas and zeroes the margin, and then
      line 2661 sets `margin-left: 252px` unconditionally, several thousand
      lines later, and wins. So a quarter of a phone screen is empty space
      beside a 0px-wide map.

   3. THE PARCEL PANEL IS A FIXED 336px BESIDE THE MAP. At 390 that leaves the
      map nothing, and the panel itself runs to x=532 — off the screen.

   The reader this site names arrives from a neighbour's text message, on a
   phone, standing outside the lot. Every one of the above is on the path
   between them and the answer.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── ≤1250px · the rail is off-canvas, so stop reserving space for it ────────
   The rail still opens over the content via #rail-toggle, which is what the
   translateX(-100%) at line 532 was always for. */
@media (max-width: 1024px) {
  .main { margin-left: 0; }
  .sidebar.rail { z-index: 240; box-shadow: var(--shadow-md); }
  /* Opened on a small screen it is an overlay, not a column. */
  body:not(.rail-off) .sidebar.rail { transform: translateX(0); }
}

/* ── ≤900px · the panel goes under the map, not beside it ───────────────────
   Stacked, the map keeps a real height and the parcel card gets the full
   width it needs to be read rather than 300px of it being off-screen. */
@media (max-width: 900px) {
  .content-row { flex-direction: column; overflow-y: auto; }
  .content-row .map-canvas,
  .content-row .map-col > .map-canvas { margin: 10px 12px; flex: 0 0 auto; height: 52vh; min-height: 300px; }
  .cpanel { width: auto; margin: 0 12px 16px; flex: 1 1 auto; max-height: none; }
  .dash { padding: 12px 12px 32px; }
}

/* ── HEADER TIERS (rewritten 2026-09-12) ────────────────────────────────────
   What the stylesheet does now. scripts/checks/header-fit.mjs measures it at
   1900, 1536, 1280, 1150, 1024, 900, 430 and 390px wide.

     ≥1150px    one 58px row: brand · Controls · theme · search · tabs ·
                [Donate] · Join
     1150–901   two rows: brand · Controls · theme · search · [Donate] · Join,
                then the tabs on a row of their own that scrolls sideways
     ≤900px     two rows: brand · Join · Menu, then search · Controls · theme.
                The tabs (and Donate) are in a sheet under the header that
                Menu opens. The sheet IS .toolbar-right; there is no second nav.

   The header is FIXED at every tier. From 2026-09-02 it went sticky below
   1150 so that nothing had to know its height; now the ResizeObserver right
   after it in cambridge.html publishes that height as --hdr-h, and .main's
   padding-top and the rail's top read the variable at every width.

   Why these widths. The old one-row header was 1375px of content, so it had
   to break before 1150; without Projects, Log in and the strapline it needs
   about 1000px, so 1150 now holds one row with room for Donate. A 1900px
   laptop at 150% OS scaling is ~1267 CSS px: one row. At 175% it is ~1086
   CSS px: two rows, by design.

   The two-row tier ends at 1149.98px, not 1150, so that a window exactly
   1150 wide (the checks' own viewport) is one row: a max-width and a
   min-width query at the same integer would both match it. */
@media (max-width: 1149.98px) {
  .hdr360 { height: auto; min-height: 58px; flex-wrap: wrap; row-gap: 0; overflow: hidden; }
  /* 57px + the 1px hairline: the first row keeps the one-row header's height. */
  .hdr-brand { order: 1; min-height: 57px; }
  .rail-toggle, .theme-toggle { order: 2; }
  .hdr360 .map-search { order: 3; margin-right: auto; }
  /* The cluster dissolves, so the tabs can take a row of their own while
     Donate stays up beside Join. .toolbar-right is a <div> with no role, so
     display:contents costs no semantics. */
  .hdr360 .toolbar-right { display: contents; }
  .hdr360 #btn-login, .hdr360 .btn-donate { order: 4; }
  .hdr360 .btn-join { order: 5; }
  .hdr360 .tabs360 { order: 6; display: flex; flex: 1 1 100%; min-width: 0; margin: 0 -4px;
    /* nowrap so it SCROLLS rather than wrapping: five tabs fit in two swipes.
       The padding keeps each focus ring inside the scroll box, which clips. */
    flex-wrap: nowrap; gap: 4px; padding: 2px 4px 6px;
    overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
  .hdr360 .tabs360::-webkit-scrollbar { display: none; }
  .tabs360 > * { flex: 0 0 auto; }
  .t360 { display: inline-flex; align-items: center; }
  /* Touch targets: 44px is the floor (DESIGN.md), and below 1150 the reader is
     as likely to be on a tablet or a phone as on a mouse. */
  .t360, .btn-news, .btn-demo, .rail-toggle, .dt-btn, .vt-btn { min-height: 44px; }
}
@media (max-width: 1150px) {
  .content-row .map-canvas,
  .content-row .map-col > .map-canvas { height: 46vh; min-height: 260px; }
}

/* ── ≤900px · brand, Join and Menu; then search; the tabs in a sheet ─────── */
@media (max-width: 900px) {
  /* visible, not hidden: the sheet hangs below the header's box. 250 lifts it
     over the rail, which is a 240 overlay at this width. */
  .hdr360 { overflow: visible; z-index: 250; }
  /* Basis 0, not auto. Flexbox breaks lines on each item's content width
     BEFORE anything shrinks, so a brand that could have shrunk still pushed
     Menu onto a third row at 390. At basis 0, Join and Menu always fit beside
     it and the brand takes what is left, truncating before it would wrap. */
  .hdr-brand { order: 1; flex: 1 1 0; min-width: 0; min-height: 56px; }
  .hdr-brand > div { min-width: 0; }
  .hdr-brand .rail-wm { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .hdr360 .btn-join { order: 2; padding: 0 18px; }
  .hdr-menu-btn { order: 3; display: inline-flex; }
  /* A zero-height, full-width item: whatever is ordered after it starts row two. */
  .hdr360::after { content: ''; order: 4; flex: 0 0 100%; height: 0; }
  .hdr360 .map-search { order: 5; flex: 1 1 0; min-width: 0; min-height: 44px; margin: 0 0 8px;
    padding: 0 0 0 11px; overflow: hidden; }
  /* 16px: iOS zooms the whole page into a text field set smaller than that. */
  .hdr360 .map-search input { align-self: stretch; font-size: 16px; }
  .hdr360 .map-search button { align-self: stretch; width: 44px; height: auto; border-radius: 0; }
  .rail-toggle, .theme-toggle { order: 6; margin: 0 0 8px; min-width: 44px; justify-content: center; padding: 0 12px; }
  /* Icon-only; the button's aria-label still says "Map controls". */
  .rail-toggle .rt-label { display: none; }
  /* The sheet: closed, it is not rendered, so it is out of the tab order. */
  .hdr360 .toolbar-right { display: none; }
  .hdr360.is-menu-open .toolbar-right { display: flex; flex-direction: column; align-items: stretch; gap: 8px;
    position: absolute; top: 100%; left: 0; right: 0; margin: 0; padding: 8px var(--edge) 14px;
    background: var(--n-000); border-bottom: 1px solid var(--n-150); box-shadow: var(--shadow-2);
    max-height: calc(100dvh - var(--hdr-h)); overflow-y: auto; overscroll-behavior: contain; }
  .hdr360.is-menu-open .tabs360 { display: flex; flex: 0 0 auto; flex-direction: column; gap: 2px;
    margin: 0; padding: 0; overflow: visible; }
  .hdr360.is-menu-open .t360 { min-height: 44px; padding: 0 12px; border-radius: var(--r-control); font-size: 0.95rem; }
  /* The rail's active-item treatment: a wash and chrome ink, no underline. */
  .hdr360.is-menu-open .t360.on { background: var(--c-chrome-wash); color: var(--c-chrome); }
  .hdr360.is-menu-open .t360.on::after { display: none; }
  .hdr360.is-menu-open .btn-donate { align-self: flex-start; min-height: 44px; padding: 0 20px; }
}

/* ── ≤560px · the last things that assume a desktop ─────────────────────────── */
@media (max-width: 560px) {
  /* The strip is `display:flex` with the dismiss button pushed right by
     margin-left:auto. Wrapped onto a phone the <strong> label and the sentence
     sat on top of each other; giving the row wrap and letting the text take a
     full line of its own is the whole fix. */
  .intro-bar { flex-wrap: wrap; row-gap: 2px; padding: 7px 12px;
    font-size: 0.72rem; line-height: 1.45; align-items: flex-start; }
  .intro-bar strong { flex: 0 0 auto; }
  .intro-bar span { flex: 1 1 100%; order: 3; }
  .intro-bar button { order: 2; margin-left: auto; }
  .nv-search input { flex: 1 1 100%; }
  .nv-search button { flex: 1 1 100%; }
  .about-pin { flex-direction: column; align-items: stretch; }
  .about-pin .cp-weekly { width: 100%; }
  .dash-title { font-size: 1.35rem; }
}

/* ── ≤560px · the header ────────────────────────────────────────────────────
   Nothing of its own any more. It used to hide the strapline here to buy back
   one of three rows (173px at 390); the strapline now goes at ≤1400, the
   header is two rows at every phone width, and its edge comes from --edge. */

/* ── ≤420px · the wordmark keeps its whole name ─────────────────────────────
   Measured 2026-09-12: "360CITI · CAMBRIDGE" needs 175px, and beside Join and
   Menu it got 166px at 390 and 136px at 360, so it truncated. Tighter padding
   on the two buttons and a 6px gutter buy 17px, which is enough at 390; below
   380 Menu also drops its icon and keeps the word, which buys 22px more. Both
   buttons stay over 44px wide. */
@media (max-width: 420px) {
  :root { --gutter: 6px; }
  .hdr360 .btn-join { padding: 0 14px; }
  .hdr-menu-btn { padding: 0 10px; gap: 6px; }
}
@media (max-width: 380px) {
  .hdr-menu-btn svg { display: none; }
}

/* The two sentences that open the Envelope tab. They sit above the numeric
   rows because a reviewer opened this card on nine of them and said he was
   overwhelmed. Prose reads as a finding where a row reads as a field, which is
   exactly why the guards in renderEnvNote had to land first — see its comment. */
.en-plain { margin: 0 0 8px; font-size: 0.8rem; line-height: 1.55; color: var(--n-900); }
.en-plain b { font-weight: 800; }
.en-plain-2 { color: var(--n-600); }

/* The Analytics head's neighbourhood select is mirrored from the orbit's rail
   (explorer.js updateAll) and no longer shown — one choice, one control. */
.dash-pick.is-mirror { display: none; }

/* ── THE MEETING CALENDAR ────────────────────────────────────────────────────
   A month grid drawn the way the civic forum draws one: a real table, Monday
   first, six-week window, cells 104px so two chips and a "+more" fit. The list
   under it is the detail; this is "what is happening, and when". */
.nv-cal-hd { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  flex-wrap: wrap; margin: 2px 0 10px; }
.nv-cal-month { margin: 0; font-family: 'Instrument Serif', Georgia, serif; font-weight: 400;
  font-size: 1.15rem; color: var(--n-900); }
.nv-cal-nav { display: flex; gap: 6px; }
.nv-cal-btn { min-height: 34px; padding: 0 12px; font: inherit; font-size: 0.74rem; font-weight: 700;
  color: var(--c-chrome); background: var(--n-000); border: 1px solid var(--line);
  border-radius: var(--r-control); cursor: pointer; }
.nv-cal-btn:hover { background: var(--c-chrome-wash); border-color: var(--c-chrome); }
.nv-cal-btn:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; }

/* The region scrolls sideways below 640px rather than the page. */
.nv-cal { position: relative; overflow-x: auto; margin-bottom: 18px; }
.nv-cal:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; }
.nv-cal table { width: 100%; min-width: 640px; border-collapse: collapse; table-layout: fixed; }
.nv-cal th { padding: 6px 8px; font-size: 0.64rem; letter-spacing: .06em; text-transform: uppercase;
  font-weight: 800; color: var(--n-600); text-align: left;
  background: var(--n-025, #f4f6f9); border: 1px solid var(--line); }
.nv-cell { position: relative; vertical-align: top; height: 104px; padding: 6px;
  border: 1px solid var(--line); background: var(--n-000); font-size: 0.72rem; }
.nv-cell.is-out { background: var(--n-025, #f4f6f9); }
.nv-cell.is-out .nv-day-n { color: var(--n-500); }
.nv-cell-hd { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.nv-day-n { display: inline-grid; place-items: center; min-width: 22px; height: 22px;
  border-radius: 999px; font-weight: 700; color: var(--n-900); }
.nv-cell.is-today .nv-day-n { background: var(--c-chrome); color: var(--n-000); }
.nv-ev { display: flex; gap: 5px; align-items: baseline; margin: 0 0 3px; padding: 2px 5px;
  border-radius: 4px; background: var(--c-chrome-wash); color: var(--n-900);
  line-height: 1.3; overflow: hidden; white-space: nowrap; }
.nv-ev-t { flex: 0 0 auto; font-size: 0.62rem; font-weight: 800; color: var(--c-chrome); letter-spacing: .02em; }
.nv-ev-b { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 0.68rem; }
/* Colour is never the only carrier: the dev chip also carries a left rule,
   and the cancelled chip strikes its text. */
.nv-ev.is-dev { border-left: 3px solid var(--c-chrome); padding-left: 4px; }
.nv-ev.is-off { background: var(--n-025, #f4f6f9); color: var(--n-600); }
.nv-ev.is-off .nv-ev-b, .nv-ev.is-off .nv-ev-t { text-decoration: line-through; }
.nv-ev-more { font-size: 0.62rem; font-weight: 700; color: var(--n-600); padding-left: 5px; }
.nv-list-h { margin: 0 0 8px; font-size: 0.7rem; letter-spacing: .08em; text-transform: uppercase;
  font-weight: 800; color: var(--n-600); }


/* ═══ What's next — two columns that end together (2026-09-05) ═══
   The Updates pane used to move whole into the 380px side column (4,411px
   against a 1,998px main column). The standing summary now sits under the
   calendar and the findings run full width; see renderNext(). */
#next-updates-slot > #cp-updates > .cp-h:first-child { display: none; }  /* the card's own h3 heads it */
.nv-card-hd { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.nv-card-hd h3 { margin: 0; }
.nv-summary .cp-summary { display: grid; grid-template-columns: 1fr; gap: 16px; margin-top: 10px; }
.nv-summary .cps-col { min-width: 0; }
.nv-summary .cp-summary .cp-h-gap { margin-top: 0; padding-top: 0; border-top: none; }
.nv-summary .act-bars { height: 96px; }
.nv-summary .act-empty { margin: 0; }
@media (min-width: 900px) { .nv-summary .cp-summary { grid-template-columns: 1fr 1fr; gap: 16px 28px; } }
.nv-findings { margin-top: 16px; }
@media (min-width: 900px) {
  .nv-findings .findings-root { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 8px 28px; align-items: start; }
  .nv-findings .fnd-hero { grid-column: 1; grid-row: 1 / 3; }
  .nv-findings .fnd-ledger-h { grid-column: 2; grid-row: 1; }
  .nv-findings .fnd-ledger { grid-column: 2; grid-row: 2; }
}

/* ═══ The zoning key, reachable (2026-09-05) ═══════════════════════════════
   Measured before this block, on the landing tab at 1440×900: the sideways
   strip showed a 348px window onto 5,450px of content — four districts of
   fifty-nine, with the caveat blocks 5,102px to the right. The strip stays
   (it is the right shape for a bottom band); this makes the rest reachable.

   1. The head stops overflowing. `.tl-title` is `flex: 0 0 auto` in the strip,
      so at 430px inside a 376px panel it was clipped by the panel's own
      overflow: the source line read "Zoning districts · CDD, Fe". Given the
      panel's width it wraps instead. */
.content-row .map-col > .theme-legend.tl-zoning .tl-title {
  flex: 0 0 auto; width: 100%; min-width: 0;
  border-right: none; padding-right: 0;
}
.content-row .map-col > .theme-legend.tl-zoning .tl-title .tl-count { white-space: normal; }
/* Wrapping the head costs it a line, and at the shared 112px cap that line came
   out of the swatches: the second district row was half cut. The zoning key
   gets its own cap so the resting state shows the two rows it is drawn as.

   ONLY above 1200. Below it the band is already taller than the space it has —
   measured at 1150x900, .content-row is 851px in a 754px window and does not
   scroll, so the band's last ~97px is cut whatever this cap says. Raising the
   cap there would buy nothing and cost map height. That overflow is a separate,
   pre-existing defect of the 1150 tier; it is written up in TODOS.md rather
   than half-fixed here. */
@media (min-width: 1201px) {
  .content-row .map-col > .theme-legend.tl-zoning { max-height: 142px; }
}

/* 2. The control. Sits on the head line, reads as a control against the band
      (which is --n-100, so a transparent chip would dissolve into it). */
.tl-all {
  margin-left: var(--s-3); border: 1px solid var(--n-200); background: var(--n-000);
  color: var(--c-chrome); font-family: 'Manrope', sans-serif; font-size: 0.62rem;
  font-weight: 800; letter-spacing: 0.04em; padding: 2px 8px;
  border-radius: var(--r-pill); cursor: pointer; white-space: nowrap;
  vertical-align: baseline;
}
.tl-all:hover { background: rgba(var(--chrome-rgb),0.08); border-color: var(--c-chrome); }
.tl-all:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 1px; }

/* 3. Open: the same content, wrapped into rows, scrolling DOWN if at all.
      Five classes, so it outranks the 112px cap and the 144px zoning cap
      without !important. Sideways scrolling is switched off entirely — the
      whole point is that nothing is off to the right any more. */
.content-row .map-col > .theme-legend.tl-zoning.is-open {
  max-height: min(58vh, 430px); overflow-y: auto;
}
.content-row .map-col > .theme-legend.tl-zoning.is-open .tl-zscroll {
  display: block; overflow: visible; min-width: 0; padding-bottom: 2px;
}
/* The families stack DOWN the panel, and each one reflows into as many rows as
   it needs. Left as `grid-auto-flow: column` with two fixed rows, "Special
   District" alone is fourteen codes — seven columns, 1,644px — and the open
   panel just overflowed sideways again instead of wrapping. Columns are sized,
   rows are not, so a family grows downward like a list. */
.content-row .map-col > .theme-legend.tl-zoning.is-open .tl-zgroup {
  max-width: 100%; margin-bottom: 4px;
}
.content-row .map-col > .theme-legend.tl-zoning.is-open .tl-zgrid {
  grid-auto-flow: row; grid-template-rows: none;
  /* min(100%, …) so a track can never be wider than the panel it is in. At
     1150px the legend cell is 180px, and a hard 160px minimum plus the gap put
     12px back outside the box — the same defect one breakpoint down. */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 160px), 1fr));
  gap: 1px 12px;
}
/* The strip's rows are `white-space: nowrap` so a district never breaks mid
   name while it scrolls sideways. In a fixed-width column that is what put
   144px back outside the panel: a label longer than its track simply ran past
   it. Wrapping is the right trade once the panel stops scrolling sideways. */
.content-row .map-col > .theme-legend.tl-zoning.is-open .tl-zgrid .tl-zrow {
  white-space: normal; align-items: flex-start; min-width: 0;
}
.content-row .map-col > .theme-legend.tl-zoning.is-open .tl-zgrid .tl-zrow i { margin-top: 2px; }
/* The source line closes the panel rather than being the 30px the old cap cut off. */
.content-row .map-col > .theme-legend.tl-zoning.is-open .tl-src { margin-top: 6px; }
/* Below ~1200 the header takes a second row and the band has less height to
   spend, so a 430px panel opened past the bottom of the window. Measured at
   1150×900 before this: the open panel's own bottom edge sat off screen. */
@media (max-width: 1200px) {
  .content-row .map-col > .theme-legend.tl-zoning.is-open { max-height: min(46vh, 300px); }
}


/* ═══ Long caveats fold (2026-09-05) ═══════════════════════════════════════
   About items keep their lead and fold the rest (foldAboutProse); the sun-
   shade caveat in the layer dock keeps one sentence and folds the mechanics.
   Native <details>: prints closed, opens without script, keyboard-operable. */
.about-fold > summary { cursor: pointer; list-style: none; }
.about-fold > summary::-webkit-details-marker { display: none; }
.about-fold > summary::after { content: '\00a0more'; color: var(--c-chrome); font-weight: 700;
  font-size: 0.78rem; white-space: nowrap; }
.about-fold[open] > summary::after { content: '\00a0less'; }
.about-fold > summary:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px;
  border-radius: var(--r-control); }
.about-fold-body { margin-top: 6px; }
/* A paragraph's fold sits under it as its own line. */
p.about-p + details.about-fold { margin: -6px 0 11px; }
p.about-p + details.about-fold > summary { font-size: 0.78rem; color: var(--c-chrome); font-weight: 700; }
p.about-p + details.about-fold > summary::after { content: ''; }
p.about-p + details.about-fold > .about-fold-body { font-size: 0.9rem; color: var(--n-600); line-height: 1.6; max-width: 68ch; }
.cl-fold { margin-top: 4px; }
.cl-fold > summary { cursor: pointer; list-style: none; color: var(--c-chrome); font-weight: 700; }
.cl-fold > summary::-webkit-details-marker { display: none; }
.cl-fold > summary::before { content: '\25B8 '; }
.cl-fold[open] > summary::before { content: '\25BE '; }
.cl-fold > summary:focus-visible { outline: 2px solid var(--c-chrome); outline-offset: 2px; }
.cl-fold[open] { margin-bottom: 4px; }


/* ═══ ≤1150 · main takes the window (2026-09-05, revised 2026-09-12) ═════════
   Measured at 1150×900 on 2026-09-05: .content-row 851px tall in a 754px
   window, body overflow:hidden, nothing scrolling — the bottom ~97px of the
   map column (most of the key, part of the footer) was unreachable, because
   `.main { height: 100vh }` sat under an in-flow header. The header is fixed
   again (2026-09-12) and takes no room in this column: main's padding-top is
   var(--hdr-h). Body stays the column so main follows 100dvh, the height a
   phone actually shows once its toolbars are counted, which 100vh does not. */
@media (max-width: 1150px) {
  body { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
  .main { flex: 1 1 auto; min-height: 0; height: auto; }
}
