/* site.css — the site-wide stylesheet, linked from templates/_head.html.

   Until 2026-09-28 this was an inline <style> block in _head.html, which put
   ~84KB of CSS inside EVERY page's HTML: never cacheable, re-downloaded on
   every click. It is linked with a content-hash version (`static_v` in
   app.py), so browsers keep it for a year and any edit gets a new URL.
   It contains no template syntax and must stay that way: this file is served
   as-is, not rendered. Page-specific styles stay inline in their templates. */
  /* Height of the shared top bar (.sitenav): 10px padding + its tallest
     child + 10px + the 1px border. That child is the search field at ~35px,
     not the 34px brand mark, so this does not track --brand-size the way it
     did when the map pages had their own bar with a 40px logo in it.

     Two things measure from it: the map rails start below it, and the news
     centre's chip bar sticks under it. On a map the bar floats over the
     figure and reserves nothing, so this is only where the rails begin —
     and it stays put when the bar slides up, because the bar can come back
     down at any moment and must not land on a control. */
  /* ── The palette ──────────────────────────────────────────────────────
     Four colours were given and everything else is derived from them:

         var(--bg)  slate ground      var(--accent)  brass accent
         var(--ink)  warm off-white    var(--muted)  cool grey

     Declared here because _head.html is the one file every page includes,
     so a token changed here changes the whole site. New work should reach
     for these rather than a literal; the literals that remain are the ones
     the migration could not express as a token (chart series, which live in
     Python, and a handful of one-off tints).

     THE NEUTRALS ARE A RAMP, not two colours. var(--muted) at 3.3:1 against the
     ground is right for labels and captions and too quiet for a paragraph,
     so --ink-2 and --ink-3 sit between it and the cream: 10.2:1 and 5.3:1.
     Body text that would have been var(--muted) is --ink-3.

     SURFACES ARE LIFTS OF THE GROUND, not transparency over it: a card at
     rgba(white, 0.04) picks up whatever is behind it, which on the map
     pages is the figure. --surface is the ground lifted ~8%, --well is it
     dropped, for anything recessed (inputs, code, a chart's plot area).

     STATUS AND DATA COLOURS ARE NOT THE ACCENT. Brass means "you can click
     this". Good/warning/bad and the chart series are their own tokens, and
     the chart series are validated against the ground rather than picked by
     eye (see CATEGORY_COLORS in serving/mapview.py). */
  /* ══ THEME ══════════════════════════════════════════════════════════
     LIGHT IS THE DEFAULT and dark is the option, which is the reverse of
     what this site did until the UI rework. Everything below is expressed
     as tokens for exactly this reason: the palette is swapped in one place
     and 489 previously hard-coded literals across 42 templates follow,
     because they were converted to `var(--…)` first. A literal cannot
     change theme.

     Two blocks:
       :root                     the light palette — the default, always
       :root[data-theme="dark"]  night mode, applied only when the reader
                                 has asked for it and the no-flash script
                                 in <head> has stamped it on <html>

     See the note above the dark block for why there is no
     prefers-color-scheme rule, and what to add if you want one. */
  :root {
    /* The browser paints scrollbars, form controls, autofill and dropdown
       lists itself, against its own idea of the document. Told wrongly, the
       page is themed and its furniture is not. Swapped with the palette. */
    color-scheme: light;

    --topbar-h: 56px;
    /* The bar's CURRENT offset, which is --topbar-h while it is down and 0px
       while navchrome.js has slid it away. Anything sticky that parks itself
       under the bar measures from THIS, so it follows the bar up instead of
       leaving a 56px band with content sliding past above it. Declared here
       so a page without the script (or with JS off) gets the bar's real
       height and behaves exactly as it did before. */
    --topbar-now: var(--topbar-h);

    /* ── Type ─────────────────────────────────────────────────────────
       A news page should not read like a dashboard, and this one did:
       one system sans doing headlines, body and figures alike.

       Two families now, the old newspaper split. A SERIF carries the
       headlines and the story titles, because that is what makes a page of
       text read as journalism rather than as an interface; the SANS keeps
       the furniture — nav, chips, meta lines, buttons — where it is already
       doing its job well and where a serif at 11px would just be mush.

       BOTH ARE SYSTEM STACKS, and that is a deliberate constraint rather
       than a compromise. `font-src 'self' data:` in serving/headers.py
       blocks Google Fonts outright, so a web font would mean relaxing the
       site's CSP and adding a third-party request to every page load. The
       stacks below resolve to a real, well-drawn serif on every desktop
       platform — Iowan Old Style or Palatino on macOS, Georgia elsewhere,
       Charter/Utopia on Linux — and cost nothing.

       --font-display is capped at the weights these faces actually ship:
       synthesising a bold from Georgia looks like exactly that. */
    --font-display: "Iowan Old Style", "Palatino Linotype", Palatino,
                    "Charter", Georgia, "Times New Roman", serif;
    --font-body: -apple-system, BlinkMacSystemFont, system-ui,
                 "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
                 monospace;

    /* Ground and surfaces. Not pure white: #FBFAF8 is the same warm cast the
       dark theme's cream ink has, so the two themes read as one design
       rather than as a light mode bolted onto a dark one. Surfaces LIFT
       toward white, wells drop toward grey — the inverse of the dark theme,
       where surfaces lift toward the ink. */
    --bg: #FBFAF8;
    --surface: #FFFFFF;
    --surface-2: #F4F2EE;
    --well: #F0EEE9;
    --chrome: rgba(255,255,255,0.88);   /* controls floating over a map */

    /* Translucent bars: the sticky nav, the floating filter strip, a card
       over the map. These are the GROUND at high alpha rather than the ink,
       which is why they need their own tokens — a bar painted
       rgba(23,33,43,0.86) stays a dark slate bar however light the page
       behind it becomes, and that is exactly what it did on the first pass
       of this rework. */
    --scrim-bg: rgba(251,250,248,0.86);
    --scrim-well: rgba(240,238,233,0.90);

    /* Hairlines and lifted surfaces, as a ramp. In the dark theme these are
       WHITE at low alpha; here they are the ink at low alpha. Same names,
       inverted material — which is the whole reason they had to become
       tokens before the palette could be swapped. */
    --tint-04: rgba(23,33,43,0.04);
    --tint-05: rgba(23,33,43,0.05);
    --tint-06: rgba(23,33,43,0.06);
    --tint-07: rgba(23,33,43,0.07);
    --tint-08: rgba(23,33,43,0.08);
    --tint-10: rgba(23,33,43,0.11);
    --tint-12: rgba(23,33,43,0.14);
    --tint-18: rgba(23,33,43,0.20);
    --line: var(--tint-10);
    --line-strong: var(--tint-18);

    /* Ink, darkest to quietest — the dark theme's ramp turned over. The
       contrast targets are kept, not the values: --muted is the caption
       grey and still clears 4.5:1 on this ground, where on the dark one it
       sat at 3.3:1. Light grounds are less forgiving of pale greys than
       dark grounds are of dim ones. */
    --ink: #17212B;
    --ink-2: #33414F;
    --ink-3: #4E5C6A;
    --muted: #63707D;
    --faint: #8B96A1;

    /* Interaction. The brass darkens: #C5A15A is a light-on-dark accent and
       fails against white, so the light theme uses a deeper cut of the same
       hue and keeps the tint for fills. */
    --accent: #8A6A22;
    --accent-hi: #6F5419;
    --accent-soft: rgba(138,106,34,0.10);
    --accent-line: rgba(138,106,34,0.28);

    /* State. Darkened for the same reason as the accent. */
    --ok: #2F7D4F;
    --warn: #8A6A22;
    --bad: #B4353D;

    /* The leaning axis. Kept as a diverging scale with a neutral middle;
       both poles are darkened to hold up on a light ground. */
    --lean-left: #2A63A8;
    --lean-mid: var(--muted);
    --lean-right: #A8434C;

    /* Shadows are real on a light ground, where on a dark one they were
       nearly decorative. Softer and shorter than the dark theme's. */
    --shadow-1: 0 1px 2px rgba(23,33,43,0.06), 0 2px 6px rgba(23,33,43,0.05);
    --shadow-2: 0 2px 4px rgba(23,33,43,0.08), 0 8px 20px rgba(23,33,43,0.08);
    --shadow-3: 0 4px 10px rgba(23,33,43,0.10), 0 18px 40px rgba(23,33,43,0.12);
  }

  /* THE DARK PALETTE IS AN OPT-IN, NOT AN OS FOLLOW.

     There is deliberately no `@media (prefers-color-scheme: dark)` block
     here. Following the operating system is the usual advice and it is not
     what was asked for: the brief was "make it light mode and add the option
     for night mode", which means a reader on a dark-themed OS still opens
     this site light and chooses dark if they want it. Auto-following would
     mean most of those readers never saw the light design at all.

     Adding the follow back is one block — copy this rule into
     `@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) }`
     and the toggle keeps working in both directions, because an explicit
     choice is always stamped on <html> and always wins. */
  :root[data-theme="dark"] {
    color-scheme: dark;
    --scrim-bg: rgba(23,33,43,0.86);
    --scrim-well: rgba(18,26,34,0.90);
    --bg: #17212B;
    --surface: #1C2732;
    --surface-2: #22303D;
    --well: #121A22;
    --chrome: var(--scrim-well);
    --tint-04: rgba(245,243,238,0.04);
    --tint-05: rgba(245,243,238,0.05);
    --tint-06: rgba(245,243,238,0.06);
    --tint-07: rgba(245,243,238,0.07);
    --tint-08: rgba(245,243,238,0.08);
    --tint-10: rgba(245,243,238,0.10);
    --tint-12: rgba(245,243,238,0.12);
    --tint-18: rgba(245,243,238,0.18);
    --ink: #F5F3EE;
    --ink-2: #C7CDD2;
    --ink-3: #98A1A9;
    --muted: #66717C;
    --faint: #4E5862;
    --accent: #C5A15A;
    --accent-hi: #D9B878;
    --accent-soft: rgba(197,161,90,0.14);
    --accent-line: rgba(197,161,90,0.32);
    --ok: #7FB08A;
    --warn: #D9B878;
    --bad: #CE7A7A;
    --lean-left: #3E85CC;
    --lean-right: #C4626A;
    --shadow-1: 0 1px 2px rgba(0,0,0,0.28), 0 2px 8px rgba(0,0,0,0.18);
    --shadow-2: 0 2px 4px rgba(0,0,0,0.32), 0 10px 26px rgba(0,0,0,0.30);
    --shadow-3: 0 4px 10px rgba(0,0,0,0.38), 0 20px 48px rgba(0,0,0,0.44);
  }

  :root {
    /* ── Form ─────────────────────────────────────────────────────────
       The palette above decides what colour a thing is. These decide what
       SHAPE it is, and they exist for the same reason: the site had ten
       different corner radii, four shadows and no shared idea of how long
       a hover takes, because each was chosen once, locally, by whoever was
       writing that page.

       RADIUS IS A SCALE, and the step you want is the one matching the
       element's size — a 6px radius on a 400px card reads as a square with
       chipped corners, and a 16px radius on a 24px chip reads as a pill
       that failed. Chips and pills take --r-pill and stop being a
       judgment call.

       SHADOWS ARE TWO LAYERS, not one. A single large soft shadow is the
       blurry look of a 2013 card; a tight near-black contact shadow under
       a wider ambient one is what reads as "lifted" on a dark ground,
       where a shadow cannot rely on darkening the background because the
       background is already dark. --shadow-1 is resting furniture,
       --shadow-2 is a hover lift, --shadow-3 is anything that floats over
       the page (a menu, a type-ahead, a sheet).

       ONE DURATION AND ONE CURVE for ordinary state changes. The curve is
       an ease-out: state changes should leave immediately and arrive
       gently, because the thing the eye is waiting for is the arrival.
       Everything here is suppressed wholesale under
       prefers-reduced-motion, at the bottom of this file's finish
       block. */
    --r-xs: 6px;
    --r-sm: 8px;
    --r-md: 12px;
    --r-lg: 16px;
    --r-xl: 22px;
    --r-pill: 999px;

    --shadow-1: 0 1px 2px rgba(0,0,0,0.28), 0 2px 8px rgba(0,0,0,0.18);
    --shadow-2: 0 2px 4px rgba(0,0,0,0.32), 0 10px 26px rgba(0,0,0,0.30);
    --shadow-3: 0 4px 10px rgba(0,0,0,0.38), 0 20px 48px rgba(0,0,0,0.44);

    --dur: 160ms;
    --dur-slow: 260ms;
    --ease: cubic-bezier(0.22, 0.61, 0.36, 1);

    /* The measure a column of prose is capped at. One number, so the
       reading pages and the feed agree about how wide a paragraph is
       allowed to get on a 2560px monitor. */
    --measure: 68ch;
  }

  /* ── Site-wide type and rendering ───────────────────────────────────
     Applied on <html> rather than <body> because every page declares its
     own body rule and would otherwise each need this appended.

     antialiased: the default (subpixel) rendering of light text on a dark
     ground overshoots — stems pick up colour fringing and the whole page
     reads heavier than the weights it asked for. This is the one place the
     dark ground makes a real typographic difference and it applies to
     every page, so it belongs here and not in a per-page style block.

     text-size-adjust stops iOS Safari inflating text in a rotated
     landscape view, which on the map pages moved the chrome. */
  html {
    -webkit-text-size-adjust: 100%;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    scroll-behavior: smooth;
  }

  /* ── A dropdown's options ──
     Two of these left on the site, both in the admin console (the retention
     window and the ticket status). An <option> inherits nothing useful from
     its <select>, so it gets opaque colours here — --well rather than
     --surface because a menu reads as recessed, the same as an input.

     READ THIS BEFORE ADDING A THIRD <select>. This rule and `color-scheme`
     above were both written to fix /tickets' category menu, which rendered
     illegibly on this dark page: the options were there, arrow keys moved
     through them, and nothing could be seen. Both were applied, both
     verified as computed style, and the menu was still unreadable — because
     a native popup is a window the BROWSER draws, from the platform theme
     and the page's colours in a combination that varies by browser, OS and
     system theme, and it does not appear in a screenshot, so it cannot be
     checked the way everything else here is. /tickets is radio pills now
     (§6j). On a dark form, prefer radios or a segmented control: styled by
     this file, visible at once, and verifiable. */
  select option {
    background-color: var(--well);
    color: var(--ink);
  }

  /* ── Rendered LLM summaries (dashboards.render_summary_html) ──
     A story summary is a small document: a heading, some bullets, sometimes
     a second section. The renderer emits that structure as real elements, so
     it needs rules wherever a summary can appear — the story dashboard, the
     article page's story block, and the story map's sidebar, which are three
     different stylesheets. Here, because this file is the one all three
     include.

     The list is deliberately not the browser's default: a disc bullet
     indented 40px inside a card reads as a nested document. A small accent
     marker on the text's own left edge keeps the summary reading as part of
     the card it sits in. */
  .sum-h {
    font-size: 11px; font-weight: 700; letter-spacing: 0.01em;
    color: var(--accent);
    margin: 16px 0 8px;
  }
  .sum-h:first-child { margin-top: 0; }
  .sum-list { list-style: none; margin: 0 0 4px; padding: 0; }
  .sum-list li {
    position: relative; padding-left: 18px; margin-bottom: 7px;
    line-height: 1.62;
  }
  .sum-list li::before {
    content: ""; position: absolute; left: 3px; top: 0.62em;
    width: 5px; height: 5px; border-radius: 50%; background: var(--accent);
    opacity: 0.7;
  }
  .sum-list li:last-child { margin-bottom: 0; }
  /* Scoped to a summary's own siblings. A bare `p + p` rule here would reach
     into every page's prose. */
  .sum-h + .sum-list, .sum-list + p { margin-top: 0; }

  /* Tabular figures for numbers that sit in a column — ranks, counts,
     timestamps. Deliberately opt-in per element rather than set on <html>:
     tabular digits are wider and evenly spaced, which is right in a column
     and wrong inside a sentence, and most of this site's numbers are in
     sentences. */
  .tnum { font-variant-numeric: tabular-nums; }

  .brand-mark {
    --brand-size: 40px;
    --brand-font: 16px;
    display: inline-flex; align-items: center; gap: 9px;
    /* Never wrap: this sits in a fixed-height bar on the map pages. */
    white-space: nowrap; line-height: 1;
  }
  .brand-logo {
    display: block; flex-shrink: 0;
    width: var(--brand-size); height: var(--brand-size);
    border-radius: 6px;
    /* The mark is a square drawing on its own cream ground (it is artwork,
       not a transparent glyph), so it sits on the dark chrome as a badge
       rather than being knocked out of it — the drawing's own navy is too
       close to the page background to read without that ground.
       object-fit keeps it square whatever --brand-size a page passes. */
    object-fit: cover;
    border: 1px solid var(--tint-12);
  }
  .brand-text {
    font-weight: 700; font-size: var(--brand-font); color: var(--ink);
    letter-spacing: -0.01em;
  }

  /* ── Site-wide finish ───────────────────────────────────────────────
     Small things that apply everywhere and were previously left to the
     browser's defaults, which are all blue and belong to no palette. */

  /* Text selection in the accent rather than the OS blue. */
  ::selection { background: rgba(197,161,90,0.30); color: var(--ink); }

  /* One visible focus ring for keyboard users, on every control on the
     site. The default outline is a thin blue line that disappears against
     a dark ground; this is the accent, offset so it traces the control
     rather than sitting on it. Only :focus-visible, so a mouse click does
     not draw it. */
  a:focus-visible, button:focus-visible, input:focus-visible,
  textarea:focus-visible, select:focus-visible, summary:focus-visible,
  [tabindex]:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 4px;
  }

  /* Scrollbars in the palette. Firefox takes the two-value shorthand,
     WebKit needs its own pseudo-elements, and both are ignored where they
     are not understood. */
  * { scrollbar-color: #3A444E var(--bg); scrollbar-width: thin; }
  ::-webkit-scrollbar { width: 10px; height: 10px; }
  ::-webkit-scrollbar-track { background: var(--bg); }
  ::-webkit-scrollbar-thumb {
    background: #3A444E; border-radius: 6px;
    border: 2px solid var(--bg);      /* inset, so the thumb reads as a bar */
  }
  ::-webkit-scrollbar-thumb:hover { background: var(--faint); }

  /* Motion off, wholesale, for anyone who has asked their OS for it. One
     rule at the top of the cascade rather than a per-component opt-out,
     because the failure mode of the per-component version is that the one
     animation somebody forgets is the one that makes a reader ill. Not
     `none`: a 0.01ms duration still fires transitionend/animationend, so
     any script waiting on one still runs. */
  @media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }

  /* ── The shared top bar (see _sitenav.html) ──
     One bar for every page that is not a map. It replaced two: the inline
     one in _dash_style.html and directory.html's own .topbar, which had
     drifted into different padding, different link colours, a different
     brand size and a different set of links.

     Sticky rather than scrolling away, because these pages can be long
     (the article dashboard, /methodology, a full directory tab) and the
     bar is the only way back out of them.

     The background is near-opaque rather than tinted: the maps' chrome
     floats over a figure and is translucent on purpose, but here the bar
     sits over ordinary content that would otherwise slide visibly
     underneath it. It keeps a blur so that content passing under it goes
     soft rather than sliding through at full contrast, which is what makes
     a sticky bar read as a layer instead of a hole.

     THE LINKS ARE TARGETS, NOT WORDS. They used to be bare text that
     changed colour on hover, which gives a pointing device nothing to aim
     at and gives the current page no weight beyond a slightly brighter
     grey. Each is now a padded pill: the hit area is the pill, hover fills
     it, and the active one is a filled brass-tinted pill rather than a
     shade of text. That is the single biggest reason the old bar read as a
     debug toolbar and this one reads as navigation. */
  .sitenav {
    position: sticky; top: 0; z-index: 60;
    display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
    padding: 10px 28px;
    /* saturate() alongside blur: a plain blur over a near-monochrome dark
       page averages toward flat grey, and the saturation boost is what
       keeps the bar reading as the same slate as the page under it. */
    background: var(--scrim-bg);
    backdrop-filter: blur(14px) saturate(140%);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    border-bottom: 1px solid var(--line);
    /* ── It gets out of the way while you read (navchrome.js) ──────────
       The bar is 56px of chrome over every scrolling page, showing a link
       row you used once on the way in. It slides up on the way down and
       comes back on a scroll up, near the top of the document, with the
       pointer near the top of the window, or on focus.

       `transform` and not `top`: the bar stays sticky and keeps its place
       in flow either way, so nothing below it moves and there is no reflow
       per frame. The transition is on transform alone for the same reason.

       :focus-within is the keyboard path and it is CSS rather than JS on
       purpose, the same call mapchrome.js documents: tabbing into a bar
       that is off-screen must bring it back, and a rule that cannot be
       forgotten is better than a listener that can. */
    transition: transform var(--dur-slow) var(--ease);
    will-change: transform;
  }
  .sitenav:not(.mapnav).is-up { transform: translateY(-100%); }

  /* ── The hardware-fund strip (_fundstrip.html) ──
     In flow under the sticky bar, so it scrolls away with the page instead
     of adding to the chrome that stays. One line on a desktop; below 900px
     it is hidden, because the form does not fit and a phone is already
     excluded server-side (is_phone). */
  .fundstrip {
    display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px;
    padding: 8px 28px; font-size: 12.5px; color: var(--ink-2, var(--ink));
    background: rgba(197,161,90,0.07);
    border-bottom: 1px solid rgba(197,161,90,0.22);
  }
  .fundstrip .fs-lead { font-weight: 700; color: var(--accent); }
  .fundstrip .fs-fund { display: inline-flex; align-items: center; gap: 8px;
                        color: inherit; text-decoration: none; }
  .fundstrip .fs-fund:hover .fs-name { text-decoration: underline; }
  .fundstrip .fs-bar { width: 56px; height: 6px; border-radius: 3px; overflow: hidden;
                       background: var(--tint-10, rgba(128,128,128,.2)); }
  .fundstrip .fs-bar > span { display: block; height: 100%; background: var(--accent); }
  .fundstrip .fs-amt { color: var(--muted); font-variant-numeric: tabular-nums; }
  .fundstrip .fs-form { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; }
  .fundstrip select, .fundstrip input[type=text] {
    font: inherit; color: var(--ink); background: var(--tint-06, transparent);
    border: 1px solid var(--tint-12, rgba(128,128,128,.35)); border-radius: 6px;
    padding: 4px 8px;
  }
  .fundstrip input[type=text] { width: 108px; }
  .fundstrip select { max-width: 170px; }
  .fundstrip .fs-form { gap: 6px; }
  .fundstrip .fs-accept { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); }
  .fundstrip .fs-accept a { color: inherit; }
  .fundstrip .fs-accept input { accent-color: var(--accent); }
  .fundstrip button[type=submit] {
    font: inherit; font-weight: 700; color: var(--accent); cursor: pointer;
    background: rgba(197,161,90,0.16); border: 1px solid rgba(197,161,90,0.40);
    border-radius: 6px; padding: 4px 14px;
  }
  .fundstrip .fs-close { background: none; border: 0; color: var(--muted);
                         font-size: 18px; line-height: 1; cursor: pointer; padding: 0 2px; }
  /* The lead goes first on a laptop-width window: the fund names say the
     same thing, and without it the form stays on the same line. */
  @media (max-width: 1360px) { .fundstrip .fs-lead { display: none; } }
  @media (max-width: 900px) { .fundstrip { display: none; } }
  .sitenav:not(.mapnav).is-up:focus-within { transform: none; }
  @media (prefers-reduced-motion: reduce) {
    .sitenav { transition: none; }
  }

  /* ── The logo never leaves ─────────────────────────────────────────
     A small fixed mark in the top-left corner, fading in exactly when the
     bar is up. There is always a way home and always something saying
     whose site this is, which is the half of a top bar worth keeping when
     the rest of it is in the way.

     aria-hidden with tabindex="-1": it is the same link as the brand
     inside the bar, and a screen reader or a keyboard user reaches that
     one by focusing the bar (which brings it straight back). Two anchors
     to "/" announced one after the other is noise, not access.

     pointer-events flips with the opacity. Left visible-but-transparent
     it would sit over the real brand and swallow its clicks. */
  .nav-brandpuck {
    /* top: 8px keeps the mark on the same baseline the bar's own brand sits
       on (the bar is 10px of padding above a 34px mark), so the logo does
       not appear to jump when the bar slides out from behind it. */
    position: fixed; top: 8px; left: 16px; z-index: 61;
    display: flex; align-items: center;
    padding: 6px 12px 6px 10px; border-radius: 12px;
    background: var(--scrim-bg);
    backdrop-filter: blur(14px) saturate(140%);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    border: 1px solid var(--line);
    text-decoration: none;
    opacity: 0; pointer-events: none;
    transition: opacity var(--dur-slow) var(--ease);
  }
  .nav-brandpuck:hover { text-decoration: none; }
  .sitenav:not(.mapnav).is-up ~ .nav-brandpuck {
    opacity: 1; pointer-events: auto;
  }
  /* The map pages have their own bar and their own rules; the puck would be
     a second floating mark over the figure. mapchrome.js never sets is-up,
     so this is belt and braces rather than a fix. */
  .sitenav.mapnav ~ .nav-brandpuck { display: none; }

  /* ── The tab row is centred on the WINDOW, not on the gap ──────────
     Three columns, the outer two equal, so the middle one is centred in
     the bar whatever the brand and the search side happen to measure.

     WHY THIS IS NOT JUST `justify-content: center` ON .nav-links. It
     already was, and the row was still 114px left of centre — measured,
     at 1600px, 1280px and 1100px alike. The link row spans the space
     BETWEEN the brand and the right-hand group, so centring inside it
     centres on the middle of that gap; the gap's middle is the window's
     middle only when the two sides are the same width. They are not:
     brand 128px against right 363px, so the run sat (363-128)/2 = 118px
     left. The note on `.nav-links` put this at "about 13px and not
     something a reader can see", which was true when the right side was
     the search box and a bare emoji button — the worded Light/Dark
     switch that replaced it is ~200px wider, and the estimate was never
     re-measured. It is a grid rather than an arithmetic correction so
     that it cannot go stale the same way: nothing here knows or cares
     what either side measures.

     TWO EARLIER VERSIONS OF THIS, AND WHY NEITHER GRID WORKED.

     `minmax(0, 1fr) auto minmax(0, 1fr)` centred perfectly and let a side
     column shrink below its own content — and `justify-self: end` then
     holds .nav-right's RIGHT edge against a column narrower than the group
     is, so the search field hung out of its column to the left and painted
     straight over "Support us". Measured at 1280px: tab run ending at
     x=913 against the right group starting at x=889, a 24px overlap. Grid
     columns cannot overlap; grid ITEMS overflowing their own column can.

     `1fr minmax(0, auto) 1fr` fixed the overlap and lost the centring: an
     `fr` distributes FREE space equally, but each column starts from its
     own content-sized base, and those bases are unequal (128 against 363).
     Equal shares of the remainder therefore do not produce equal columns —
     measured 42px off at 1280px, 93px at 1100px.

     WHAT ACTUALLY GIVES EQUAL SIDES: `flex: 1 1 0` on both. A zero basis
     means the two of them start equal and grow equally, so they finish
     equal, and the middle is centred between them for free. `min-width:
     max-content` is the floor that the grid version lacked — neither side
     can be squeezed under its own content, so nothing clips and nothing
     overlaps, because flex items do not overlap the way an overflowing
     grid item does. When the bar is too narrow for all three the middle
     is what gives, shrinking and scrolling inside itself exactly as
     before; the sides are then at their content widths, unequal, and the
     row is honestly off-centre rather than centred and broken.

     `flex-wrap: nowrap` is the other half. The `flex: 1 1 0` on .nav-links
     was there to stop an overfull line wrapping the search field onto a
     second row and doubling the bar's height — flexbox breaks lines from
     hypothetical sizes BEFORE it shrinks anything, so a content-sized
     basis on the link row was enough to trigger it. Forbidding the wrap
     outright removes the hazard rather than working around it, which is
     what lets the link row take an `auto` basis here and be centred at
     all. --topbar-h (56px) is documented as the bar's EXACT height and
     /news's chip bar and the map rails both measure from it, so a second
     row is not a cosmetic problem.

     Desktop only. At 720px and below the bar is a deliberately wrapped
     column (`.nav-links`/`.nav-right` both take `flex: 1 0 100%`), which
     needs the wrapping this turns off. */
  @media (min-width: 721px) {
    .sitenav { flex-wrap: nowrap; }
    .sitenav .nav-brand,
    .sitenav .nav-right { flex: 1 1 0; min-width: max-content; }
    /* The link row keeps its own `justify-content: safe center`; what
       changes is that the box it centres in is now the middle third of
       the bar rather than the gap between two unequal ends. */
    .sitenav .nav-links { flex: 0 1 auto; }
    /* margin-left: auto was what pushed this to the far end of the row.
       With a real flex basis doing that job it would only fight it. */
    .sitenav .nav-right { margin-left: 0; justify-content: flex-end; }
  }
  /* ── The map variant ────────────────────────────────────────────────
     The same bar, floating over a full-bleed figure instead of sitting
     above content, and out of the way unless it is wanted.

     WHY IT HIDES AT ALL. Everywhere else the bar sits above the page and
     costs nothing. On a map it is over the data: 56px across the full width
     of a figure whose whole point is that you can see all of it. The old
     answer was to put the links in a rail down the right-hand edge instead,
     which cost the same pixels in a different place and meant maintaining a
     third copy of the link list.

     WHEN IT COMES DOWN. Two conditions, ORed, and the second is the one
     that makes this usable rather than a hidden menu:

       - the pointer is near the top of the window (static/mapchrome.js), or
       - the map is at its default framing, which is where every visitor
         starts and where the reset button puts them back.

     So arriving at the page, or pressing reset, shows the navigation
     without asking; the moment you pan or zoom into the data it gets out
     of the way. The page reports the second condition through
     window.mapChrome.setAtDefaultView(), from inside its existing
     showResetButton() — the same choke point, the opposite sense.

     :focus-within is the keyboard path, and it is CSS rather than JS on
     purpose: tabbing into a bar that is off-screen must bring it back, and
     a rule that cannot be forgotten is better than a listener that can. */
  .sitenav.mapnav {
    position: fixed; top: 0; left: 0; right: 0; z-index: 999;
    /* 16px rather than .sitenav's 28px, so the brand sits directly over
       .leftrail's own 16px inset and the whole left edge of the page reads
       as one axis. */
    padding-left: 16px; padding-right: 16px;
    background: var(--chrome);
    /* Not the 4px sliver of a translucent bar. Fully off-screen the bar
       leaves no trace at all, and a visitor who has panned once would have
       no reason to think there is anything up there; parking the bottom
       border on the top edge of the window is the affordance. */
    transform: translateY(calc(-100% + 4px));
    transition: transform var(--dur-slow) var(--ease);
    will-change: transform;
  }
  .sitenav.mapnav.is-down,
  .sitenav.mapnav:focus-within { transform: none; }
  /* Nothing shifts the bar for the detail panel any more. The panel used to
     be a 360px flex child that took its width out of the map, so a bar fixed
     to the viewport sat across the panel's own headline and had to be pulled
     in — the same fix .rightrail needed before it. The panel is an overlay
     at z-index 1200 now, above the bar's 999, so it simply covers it. */

  /* The brand is an <a>, so it matched `.sitenav a` above and picked up
     that rule's link padding and inline layout. Between the two, a 34px
     mark was making a 48px anchor and a 69px bar — which is why --topbar-h
     (the number the map rails and the news centre's chip bar both measure
     from) kept disagreeing with the bar's real height, and why the bar
     looked oddly tall for what is in it.

     display:flex drops the inline line box (`.brand-mark` inside is an
     inline-flex and was sitting on a baseline, with descender space under
     it), and the padding reset drops the rest. The mark IS the target here,
     so it needs no padding of its own to be clickable. The bar is 55px now:
     10 + its tallest child + 10 + the border. */
  .sitenav .nav-brand {
    display: flex; align-items: center; padding: 0; margin-right: 8px;
    text-decoration: none; flex-shrink: 0;
  }
  .sitenav .nav-brand:hover { text-decoration: none; }
  /* ── The link row: one line at every width, scrolling if it has to ──
     `flex: 1 1 0`, and the zero basis is the whole point. The row used to
     be `flex-wrap: wrap` at its content width, which meant flexbox laid it
     out at its full max-content size, found the line overfull and wrapped
     the SEARCH FIELD onto a second row rather than shrinking anything.
     That silently doubled the bar's height, and the bar's height is not
     free: --topbar-h (56px) is documented as its EXACT height because
     /news's chip bar sticks to it and the map rails measure from it, so a
     104px bar puts the chip bar through its own navigation.

     It went unnoticed because the old five-link row only overflowed
     between 720px and ~850px, a band a couple of dozen pixels above the
     breakpoint where the phone rules take over. Adding a sixth link with a
     tag on it moved the same wrap up to ~1075px, which is an ordinary
     laptop.

     A zero basis makes the row's hypothetical size zero, so it never
     causes a wrap; it grows into whatever the brand and the search field
     leave, and overflows INSIDE itself when the links need more than that.
     The overflow scrolls sideways — the gesture the topic chips and the
     /news lead rail already use, so horizontal scrolling means one thing
     on this site — and the scrollbar is hidden because the row is short
     enough that a visible one is mostly chrome. */
  .nav-links {
    display: flex; align-items: center; gap: 2px;
    flex: 1 1 0; min-width: 0;
    flex-wrap: nowrap; overflow-x: auto;
    scrollbar-width: none; -ms-overflow-style: none;
    -webkit-overflow-scrolling: touch;
    /* ── Centred, and `safe` is doing real work here ──
       Centring inside this row puts the links across the middle of the
       box the row occupies rather than pinned to its left.

       WHICH BOX THAT IS CHANGED ON 2026-09-09, and the sentence that used
       to be here is why it needed changing. It said the row spanned the
       gap between the brand and the search field, so centring in it was
       the middle of that gap rather than of the window — "about 13px
       apart, and not something a reader can see". That was measured when
       the right-hand side was the search box and a bare emoji button. The
       worded Light/Dark switch is ~200px wider, and the gap's middle had
       drifted to **114px** left of the window's before anyone measured it
       again. The bar above now sizes the two ends equally, so this centres
       on the window; the number in a comment is the thing that goes stale,
       which is why the fix is structural and lives up there.

       `safe` is the version that survives the narrow case. Plain `center`
       on a scroll container centres content that overflows too, which
       pushes the first item off the LEFT edge into the part of the scroll
       range a scrollbar cannot reach — "Map view" would become
       unreachable on a phone. `safe center` falls back to flex-start the
       moment the row overflows, which is exactly when centring stops being
       possible anyway. The unprefixed line before it is the fallback for
       browsers that do not know the keyword: they get plain centring,
       which is only wrong while overflowing. */
    justify-content: center;
    justify-content: safe center;
  }
  .nav-links::-webkit-scrollbar { display: none; }
  .sitenav a {
    font-size: 13px; color: var(--ink-3); text-decoration: none;
    white-space: nowrap;
    padding: 7px 12px; border-radius: var(--r-sm);
    transition: background var(--dur) var(--ease),
                color var(--dur) var(--ease);
  }
  .sitenav a:hover {
    color: var(--ink); background: var(--tint-07);
    text-decoration: none;
  }
  /* Where you are now: a filled pill, not a brighter grey. Ink text on an
     accent tint, so the accent still means "interactive" while the fill
     carries "here". */
  .sitenav a.active {
    color: var(--ink); background: var(--accent-soft);
    box-shadow: inset 0 0 0 1px var(--accent-line);
  }
  /* ── The "planned" tag (see _sitenav.html) ──
     One link in the bar points at a page whose feature is not built:
     /connections. The tag is what lets it sit up here at all, by making
     the bar say so before the click rather than leaving the page's own
     banner to say it after.

     THE WORD IS "PLANNED", and it was picked over two shorter and two
     longer candidates. "Soon" promises a date nobody has committed to, and
     this site does not make promises it cannot check. "In development" is
     what the page's own banner says and is the honest long form, but the
     bar is a width budget: the six pills, the brand and the search field
     already spend the whole of a 1024px window (see the middle tier
     below), and the longer word is what pushed "Support us" off the end of
     it. "Planned" says the same thing in the space the row can afford.

     NEUTRAL, NOT BRASS, and that is the palette rule rather than a
     preference: the accent means "you can click this", so a status marker
     wearing it would read as a second, brighter link inside the first.
     Quiet grey on a faint lift keeps it as a label attached to a link.

     SENTENCE CASE SINCE 2026-08-25, and a size up because of it. This used
     to be 9.5px uppercase, on the reasoning that a lowercase word that small
     just reads as small text rather than as a tag. That reasoning was right,
     so removing the caps site-wide meant the pill had to get the legibility
     back somewhere else: 10.5px, and the border and fill are now the whole
     of what makes it a tag. Delete this rule and the span together when the
     feature ships. */
  .sitenav a .nav-tag {
    display: inline-block;
    margin-left: 6px;
    padding: 2px 7px;
    border: 1px solid var(--line-strong);
    border-radius: var(--r-pill);
    background: var(--tint-05);
    color: var(--muted);
    font-size: 10.5px; font-weight: 700; letter-spacing: 0.01em;
    vertical-align: 1px;
  }
  /* On the filled pill the tag would otherwise sit on an accent tint at
     the same lightness as its own fill and lose its edge. */
  .sitenav a.active .nav-tag,
  .sitenav a:hover .nav-tag { color: var(--ink-3); }

  .sitenav a.nav-support { color: var(--accent); }
  .sitenav a.nav-support:hover {
    color: var(--accent-hi); background: var(--accent-soft);
  }
  /* ── The map switch (see _mapswitch.html) ──
     A segmented control: a recessed track with the chosen option filled,
     which is how a control says "one of these is always on" where a row of
     links says "here are some places".

     DOCKED UNDER THE LOGO, not in the bar. It went in the bar first, beside
     the Map view entry it belongs to, and that was the wrong layer: the bar
     is the site's navigation and slides away while you work the map, so the
     one control that says which map you are looking at went with it. Down
     here it sits with the map's own controls, on the same 16px axis as the
     rail and the corner stack, and it stays put. */
  .mapswitchdock {
    position: fixed; left: 16px; top: calc(var(--topbar-h) + 10px); z-index: 901;
  }
  .mapswitch {
    display: inline-flex; padding: 3px; gap: 2px;
    background: var(--chrome); backdrop-filter: blur(8px);
    border: 1px solid var(--line-strong); border-radius: var(--r-pill);
    box-shadow: var(--shadow-1);
  }
  .mapswitch a {
    padding: 6px 16px; border-radius: var(--r-pill);
    font-size: 12px; font-weight: 600; color: var(--ink-3);
    text-decoration: none; white-space: nowrap;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
  }
  .mapswitch a:hover { color: var(--ink); background: var(--tint-08); }
  .mapswitch a[aria-current="page"] {
    background: var(--accent); color: var(--bg); font-weight: 700;
  }

  /* Search sits at the far end of the bar on every page, which is where it
     already sat on the directory and the dashboards. */
  .nav-right { margin-left: auto; display: flex; align-items: center; gap: 14px; }

  /* ── The theme switch ──────────────────────────────────────────────
     A labelled segmented control rather than an icon button. "Theme" is
     the quietest part and the two options carry the weight, so the control
     announces its subject before its state — which is the thing an icon
     could not do at any size. */
  /* ── The map's region control ─────────────────────────────────────
     Docked under the map switch, in the same chrome-over-the-figure
     language as the panels around it. Links, not buttons: the figure is
     drawn server-side, so the choice has to be in the URL — which also
     means a filtered map can be shared and survives a reload. */
  .mapregion {
    position: absolute; left: 18px; top: calc(var(--topbar-h) + 60px);
    /* 900, with the other rails. The callout bubbles are at 850 and
       were painting straight over this control — see
       _callout_style.html, which says the bubbles sit under the rails
       deliberately. */
    z-index: 900; display: flex; align-items: center; gap: 2px;
    padding: 4px 6px; background: var(--chrome);
    border: 1px solid var(--line); border-radius: var(--r-pill);
    backdrop-filter: blur(10px) saturate(140%);
    box-shadow: var(--shadow-1); font-size: 11.5px;
  }
  .mapregion-label {
    padding: 0 6px 0 4px; color: var(--faint);
    text-transform: uppercase; letter-spacing: 0.08em;
    font-size: 10px; font-weight: 700;
  }
  .mapregion a {
    padding: 4px 9px; border-radius: var(--r-pill);
    color: var(--ink-3); text-decoration: none; white-space: nowrap;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
  }
  .mapregion a:hover { background: var(--tint-06); color: var(--ink); }
  .mapregion a[aria-current="page"] {
    background: var(--accent-soft); color: var(--accent); font-weight: 600;
    box-shadow: inset 0 0 0 1px var(--accent-line);
  }
  @media (max-width: 720px) { .mapregion { display: none; } }

  .theme-switch {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 3px 3px 9px;
    background: var(--surface); color: var(--ink-3);
    border: 1px solid var(--line); border-radius: var(--r-pill);
    font-family: inherit; font-size: 11px; line-height: 1;
    cursor: pointer;
    transition: border-color var(--dur) var(--ease);
  }
  .theme-switch:hover { border-color: var(--line-strong); }
  .theme-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .theme-switch-label {
    text-transform: uppercase; letter-spacing: 0.08em;
    font-weight: 700; color: var(--faint);
  }
  .theme-opt {
    padding: 4px 9px; border-radius: var(--r-pill);
    font-weight: 600; color: var(--ink-3);
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
  }
  /* The live option. Marked with a filled pill AND a weight change, so it
     does not rely on colour alone to say which one is on. */
  .theme-opt[data-live="true"] {
    background: var(--accent-soft); color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent-line);
  }
  @media (max-width: 900px) { .theme-switch-label { display: none; } }

  /* Visible to a screen reader, not to an eye. The button's only visible
     content is an emoji, which is announced as its unicode name if it is
     announced at all, so the real label lives here. */
  .sr-only {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }

  /* ── The middle tier: laptop and tablet-landscape widths ──
     Between the desktop bar and the phone rules there is a band where six
     links, a brand and a 190px search field want more room than the window
     has, and the row starts hiding its last pill ("Support us", the one
     link on the bar that is asking for something) behind a scroll. Nothing
     is dropped and nothing wraps: the pills tighten and the search field
     gives up the width it does not need while it is empty, which is enough
     to get the whole row back on screen. The field still grows to 240px on
     focus, so typing a real query is not done through the smaller slot. */
  @media (min-width: 721px) and (max-width: 1180px) {
    .sitenav { padding: 10px 18px; gap: 12px; }
    .sitenav .nav-links a { padding: 7px 8px; }
    .sitenav a .nav-tag { margin-left: 5px; padding: 2px 5px; letter-spacing: 0.01em; }
    .acct-search input { width: 118px; }
  }

  @media (max-width: 720px) {
    .sitenav { padding: 8px 12px; gap: 8px; }
    /* Its own row, and this is what `flex: 1 0 100%` is for: with the zero
       basis it inherits from the desktop rule the row would sit beside the
       brand and scroll inside the ~200px left over, which is a worse row
       than the full width of the screen. A 100% basis forces the wrap the
       desktop bar spends its effort avoiding. The sideways scroll and the
       hidden scrollbar are the row's own rules at every width now (see
       .nav-links above); the negative margin lets the first and last pill
       sit flush with the bar's padding while the row still has room to
       breathe. */
    .nav-links { flex: 1 0 100%; margin: 0 -4px; padding: 0 4px; }
    .sitenav a { font-size: 12px; padding: 6px 10px; }
    /* The tag stays on the phone rather than being hidden to save width:
       what it says is the reason its link is allowed in the bar, and a
       row that already scrolls sideways can afford the characters. */
    .sitenav a .nav-tag { font-size: 9px; padding: 1px 5px; margin-left: 5px; }
    /* Its own row under the links rather than squeezed onto the end of a
       row that already scrolls sideways: this is the one control on a map
       page a visitor is most likely to want, and burying it past the
       scroll would hide it. */
    .mapswitch a { padding: 6px 14px; }
    /* The field takes the full width of its own row rather than squeezing
       the links off the bar. */
    .nav-right { margin-left: 0; flex: 1 0 100%; }
    .nav-right .acct-search, .nav-right .acct-search input { width: 100%; }
  }

  /* ── Article search box (see _searchbox.html) ──
     Same split as the brand: markup in the partial, rules here, because the
     partial gets included inside three different nav bars. */
  /* position:relative anchors the type-ahead panel below the field. */
  .acct-search { display: inline-flex; margin: 0; position: relative; }
  .acct-search input {
    background: var(--well); color: var(--ink);
    border: 1px solid var(--line); border-radius: var(--r-pill);
    font-family: inherit; font-size: 12.5px; padding: 8px 14px; width: 190px;
    /* Search inputs get a UA-supplied inner shadow/appearance on WebKit that
       ignores the background above. */
    -webkit-appearance: none; appearance: none;
    transition: border-color var(--dur) var(--ease),
                box-shadow var(--dur) var(--ease), width var(--dur) var(--ease);
  }
  .acct-search input::placeholder { color: var(--muted); }
  .acct-search input:hover { border-color: var(--line-strong); }
  /* The focus state is a ring, not just a border colour: a 1px line
     changing hue is nearly invisible at this size against a dark ground,
     and the field is the one control on the bar somebody arrives at by
     keyboard. The width grows with it, so typing a real query is not done
     through a 190px slot. */
  .acct-search input:focus {
    outline: none; border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
    width: 240px;
  }

  /* ── Search type-ahead (built by static/searchsuggest.js) ──
     Anchored to the field's right edge by default, because on the
     directory and dashboard bars the field sits near the right of the page
     and a wider panel has to grow inwards or it runs off screen. That is
     now every page including the maps, which used to centre their field
     over the middle of the figure and had this re-anchored to match.

     Opaque, unlike the field itself: on a map this is a list of small text
     over the dots, which is the one place where the grid showing through
     would cost real legibility. */
  .acct-suggest {
    position: absolute; top: calc(100% + 6px); right: 0; z-index: 1000;
    width: min(420px, 86vw);
    /* Tall enough that eight results plus the "see all" row fit without
       scrolling on an ordinary window — the row that gets you to the full
       result page is the one that must not be hidden below a fold. */
    max-height: min(76vh, 560px); overflow-y: auto;
    background: var(--scrim-well); backdrop-filter: blur(10px);
    border: 1px solid var(--line-strong); border-radius: var(--r-md);
    padding: 6px; text-align: left;
    box-shadow: var(--shadow-3);
  }
  .acct-suggest[hidden] { display: none; }
  .sg-item {
    display: block; padding: 9px 11px; border-radius: var(--r-sm);
    text-decoration: none; color: var(--ink);
    transition: background var(--dur) var(--ease);
  }
  /* Hover and keyboard selection share one look — there is only ever one
     "the row you are about to open", however you got to it. */
  .sg-item:hover, .sg-item[aria-selected="true"] {
    background: rgba(197,161,90,0.16);
  }
  .sg-title { display: block; font-size: 12.5px; line-height: 1.35; }
  /* The matched run of the query inside the title. */
  .sg-title b { color: var(--ink); font-weight: 700; }
  .sg-meta { display: block; font-size: 10.5px; color: var(--muted); margin-top: 3px; }
  .sg-empty { padding: 10px; font-size: 12px; color: var(--muted); }
  .sg-all {
    margin-top: 4px; padding-top: 10px;
    border-top: 1px solid var(--tint-08); border-radius: 0 0 7px 7px;
    font-size: 12px; font-weight: 600; color: var(--accent);
  }

  /* ── Bottom-left control stack ──
     Reset-view above the account control, bottom-aligned. (The topic
     filters started here but moved up under the logo — see .leftrail —
     once the list had to show all twelve categories without scrolling.)
     Fixed to the viewport so it stays put on scrolling pages too, and
     align-items:flex-start so each child is only as wide as its content
     rather than all stretching to the widest one.

     pointer-events:none on the container, restored on the children: the
     stack spans a tall strip of the left edge, and without this it would
     swallow map drags started anywhere in that strip. Same trick
     .sitenav.mapnav already uses along the top. */
  .cornerstack {
    /* left matches .leftrail and the logo's own inset, so everything down
       the left edge lines up on one axis. */
    position: fixed; left: 16px; bottom: 16px; z-index: 900;
    display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
    pointer-events: none;
  }
  .cornerstack > * { pointer-events: auto; }

  /* ── Account control (see _accountcorner.html) ── */
  .acct-corner {
    display: inline-flex; align-items: center; gap: 8px;
    background: var(--chrome); backdrop-filter: blur(8px);
    border: 1px solid rgba(245,243,238,0.22); border-radius: var(--r-pill);
    padding: 7px 12px; box-shadow: var(--shadow-1);
  }
  .acct-corner-link {
    font-size: 12px; font-weight: 600; color: var(--ink);
    text-decoration: none; white-space: nowrap;
  }
  /* The word beside the emoji. It is a span so the phone can clip it
     (see the phone block further down) and this margin is the space
     that used to be a literal space in the markup. */
  .acct-lab { margin-left: 5px; }
  .acct-corner-user {
    font-size: 12px; font-weight: 700; color: var(--ink);
    white-space: nowrap; max-width: 110px;
    overflow: hidden; text-overflow: ellipsis;
    border-left: 1px solid var(--tint-12); padding-left: 8px;
  }
  /* ── Legal row (see _accountcorner.html) ──
     Its own pill under the account control rather than two more links inside
     it: the account pill is a control (who you are, sign in, sign out) and
     these are references. Set quieter than the row above on purpose, since
     this is the one thing in the corner nobody is looking for until they
     are. It carries no chrome background of its own on the maps because
     .cornerstack already floats; the blur and border match the account pill
     so the corner reads as one stack. */
  .legal-corner {
    display: inline-flex; align-items: center; gap: 7px;
    background: var(--chrome); backdrop-filter: blur(8px);
    border: 1px solid var(--tint-12); border-radius: var(--r-pill);
    padding: 4px 11px; box-shadow: var(--shadow-1);
  }
  .legal-corner a {
    font-size: 11px; font-weight: 600; color: var(--ink-3);
    text-decoration: none; white-space: nowrap;
  }
  .legal-corner a:hover { color: var(--accent); }
  /* The separators. .legal-release below overrides the size back up: this
     rule exists to make a "·" recede, not to shrink a word to 10px. */
  .legal-corner span { color: var(--faint); font-size: 10px; }
  /* The version marker. Deliberately the quietest thing in the corner —
     tabular figures so the digits do not shift when it is bumped, and the
     same muted grey as an unhovered link rather than the separator's grey,
     so it reads as a label and not as punctuation. Not a link: there is no
     changelog to point at yet. */
  .legal-corner .legal-release {
    font-size: 10.5px; font-weight: 600; color: var(--muted);
    letter-spacing: .01em; font-variant-numeric: tabular-nums;
    white-space: nowrap; cursor: default;
  }
  /* The data date, one step quieter again than the version above it — its
     own line, lighter weight, and the separator's grey rather than the
     label's. The hierarchy is the message: the software version is the
     claim about the build, and this is a footnote about the corpus that
     build is drawing. Sharing a line would have read as one compound
     version string, which is exactly the conflation it exists to prevent.
     Rendered only when there is a real date -- see get_publish_meta. */
  .legal-data {
    font-size: 10px; font-weight: 500; color: var(--faint);
    letter-spacing: .01em; font-variant-numeric: tabular-nums;
    white-space: nowrap; cursor: default; margin-top: 1px;
  }

  .acct-corner-out { display: inline-flex; margin: 0; }
  .acct-corner-out button {
    background: none; border: none; cursor: pointer; font-family: inherit;
    font-size: 13px; color: var(--muted); padding: 0 2px; line-height: 1;
  }
  .acct-corner-out button:hover { color: var(--bad); }
  .acct-corner-signin {
    display: inline-block;
    background: var(--tint-12); color: var(--ink);
    border: 1px solid rgba(245,243,238,0.28); border-radius: var(--r-pill);
    backdrop-filter: blur(8px); box-shadow: var(--shadow-1);
    font-size: 13px; font-weight: 700; padding: 10px 18px; min-height: 40px;
    text-decoration: none; white-space: nowrap;
    transition: background var(--dur) var(--ease),
                transform var(--dur) var(--ease);
  }
  .acct-corner-signin:hover {
    background: rgba(245,243,238,0.24); transform: translateY(-1px);
  }
  .acct-corner-signin:active { transform: none; }
  /* Inside the account row (History · Support · Sign in) rather than standing
     alone, so the corner keeps one shape signed in or out. The standalone
     .acct-corner-signin sizing above is the pill on pages that render it on
     its own; this trims it to sit in the row. */
  .acct-corner-signin-inline {
    font-size: 12px; padding: 4px 12px; min-height: 0;
    border-left: 1px solid var(--tint-12);
    border-radius: var(--r-pill); margin-left: 2px;
  }
  .acct-corner-signin-inline:hover { transform: none; }

  /* ── Give feedback (see _feedbackbox.html) ──
     A <details> in the corner stack, above the account control. Closed it is
     one button the same size as the account row beside it; open it is a small
     panel that grows UPWARD (bottom: 100%) so it never pushes the account
     control off the bottom of the screen — the stack is anchored to the
     bottom edge, so anything that grew downward would go off-page.

     Its own control rather than a link to /tickets because the point is that
     saying something costs one box and no navigation. /tickets is still there
     for anything that needs a category and room to explain. */
  .fb-box { position: relative; }
  .fb-box > summary {
    list-style: none; cursor: pointer;
    display: inline-flex; align-items: center; gap: 6px;
    background: var(--chrome); backdrop-filter: blur(8px);
    border: 1px solid rgba(127,176,138,0.28); border-radius: var(--r-pill);
    color: var(--ok); font-size: 12px; font-weight: 600;
    padding: 7px 12px; white-space: nowrap; box-shadow: var(--shadow-1);
    transition: background var(--dur) var(--ease);
  }
  .fb-box > summary::-webkit-details-marker { display: none; }
  .fb-box > summary:hover { background: rgba(127,176,138,0.16); }
  .fb-panel {
    position: absolute; bottom: 100%; left: 0; margin-bottom: 8px;
    width: 264px; display: flex; flex-direction: column; gap: 8px;
    background: var(--scrim-well); backdrop-filter: blur(10px);
    border: 1px solid rgba(127,176,138,0.28); border-radius: var(--r-md);
    box-shadow: var(--shadow-3); padding: 14px;
  }
  .fb-panel label { font-size: 11px; font-weight: 700; color: var(--muted);
                    letter-spacing: 0.01em; }
  .fb-panel textarea {
    width: 100%; min-height: 84px; resize: vertical;
    background: var(--well); color: var(--ink);
    border: 1px solid var(--line); border-radius: var(--r-sm);
    font-family: inherit; font-size: 13px; line-height: 1.5; padding: 9px 11px;
    transition: border-color var(--dur) var(--ease),
                box-shadow var(--dur) var(--ease);
  }
  .fb-panel textarea:focus {
    outline: none; border-color: var(--ok);
    box-shadow: 0 0 0 3px rgba(127,176,138,0.16);
  }
  .fb-actions { display: flex; align-items: center; gap: 10px; }
  .fb-actions button {
    background: var(--ok); color: var(--bg); border: none;
    border-radius: var(--r-pill);
    font-family: inherit; font-size: 12px; font-weight: 700;
    padding: 8px 16px; cursor: pointer;
    transition: background var(--dur) var(--ease),
                transform var(--dur) var(--ease);
  }
  .fb-actions button:active { transform: scale(0.97); }
  .fb-actions button:hover { background: #A8C7AE; }
  .fb-actions button[disabled] { opacity: .55; cursor: default; }
  .fb-note { font-size: 11px; color: var(--muted); min-height: 14px; }
  .fb-note.ok { color: var(--ok); }
  .fb-note.bad { color: var(--bad); }
  /* Signed out: the same button, but a link to the login page. Styled off
     the summary above so the corner does not visibly change shape when
     someone signs in — only what it does changes. */
  .fb-signedout {
    display: inline-flex; align-items: center; gap: 6px;
    background: var(--chrome); backdrop-filter: blur(8px);
    border: 1px solid rgba(127,176,138,0.28); border-radius: var(--r-pill);
    color: var(--ok); font-size: 12px; font-weight: 600;
    padding: 7px 12px; white-space: nowrap; text-decoration: none;
    box-shadow: var(--shadow-1);
    transition: background var(--dur) var(--ease);
  }
  .fb-signedout:hover { background: rgba(127,176,138,0.16); }
  /* The way into the admin console, only rendered for an admin account. */
  .acct-corner-admin { color: var(--accent); }

  /* ══ THE CORNER STACK ON A PHONE ══════════════════════════════════════
     It moves to the TOP-RIGHT, and the legal lines stay at the bottom.

     WHAT WAS WRONG. This stack is fixed to the bottom-left at every width,
     which is empty chrome space on a desktop and is the middle of the text
     on a phone: at 390px the three pills sat on top of the news centre's
     topic chips and across the body copy of /about and /connections, so the
     one piece of furniture that is on literally every page was also the one
     covering the words on every page. The top-right is the space the bar
     actually leaves free — the brand is ~128px of a 390px row and nothing
     else is on that line — so the controls go where there is room rather
     than where there is content.

     ONE INSTANCE, MOVED — not a second copy rendered into the bar. The
     feedback box is a <details> with an id and a <label for=…>, and the
     account row is a single control; rendering either twice would put two
     #fb-message fields on one page, which is the exact collision
     _feedbackbox.html's own header warns about.

     THE LEGAL LINES DO NOT COME ALONG. They are references, not controls —
     nobody reaches for Terms mid-article — so they stay at the bottom edge
     and sit LOWER than before (6px rather than 16px), which is both further
     out of the way and closer to where a reader expects the small print.
     They are `position: fixed` in their own right, so they leave the row
     above them entirely rather than being laid out with it. */
  @media (max-width: 720px) {
    .cornerstack {
      top: 10px; right: 10px; left: auto; bottom: auto;
      flex-direction: row; align-items: center; gap: 6px;
    }

    /* Icons only. The words are CLIPPED rather than removed, so the
       accessible name is still "History" and not "🕘" — see the note in
       _accountcorner.html. */
    .acct-lab {
      position: absolute; width: 1px; height: 1px; margin: 0;
      overflow: hidden; clip-path: inset(50%); white-space: nowrap;
    }
    .acct-corner { gap: 10px; padding: 6px 10px; }
    .acct-corner-link { font-size: 14px; }
    /* Sign in keeps its word: it is the one control here a signed-out
       reader has to be able to find without decoding a pictogram, and it
       is the button this corner exists to offer. */
    .acct-corner-signin-inline { font-size: 12px; padding: 4px 10px; }
    /* Signed in, the username goes. At 390px it is the widest thing in the
       row and the least load-bearing: the sign-out control beside it
       already says an account is signed in, and /account says which. */
    .acct-corner-user { display: none; }

    .fb-box > summary, .fb-signedout { padding: 7px 9px; font-size: 14px; }

    /* The panel opens DOWNWARD here. It grows upward everywhere else
       because the stack is anchored to the bottom of the screen; anchored
       to the top, upward is off-page. Right-aligned and width-capped so a
       264px panel cannot hang off a 390px screen. */
    .fb-panel {
      bottom: auto; top: 100%; margin-bottom: 0; margin-top: 8px;
      left: auto; right: 0; width: min(264px, calc(100vw - 20px));
    }

    /* Out of the row above and back to the bottom edge, as one block. */
    .legal-stack {
      position: fixed; left: 12px; bottom: 6px;
      display: flex; flex-direction: column; align-items: flex-start;
    }
  }

  /* ── The narrowest phones (iPhone SE and friends) ──
     The control cluster above is fixed, so it is NOT in the bar's flex flow
     and the brand cannot be asked to shrink out of its way: measured at
     320px the two overlapped by 15px, which puts the feedback button on top
     of the wordmark. The wordmark is what gives, because it is the only
     thing in that collision that is said elsewhere — the mark stays, the
     page title says the name, and the brand puck that fades in on scroll
     carries mark AND wordmark at full size. Losing a control or making the
     tap targets smaller would both cost more than losing two words. */
  @media (max-width: 360px) {
    .sitenav .nav-brand .brand-text { display: none; }
    .cornerstack { gap: 5px; }
    .acct-corner { gap: 9px; padding: 6px 9px; }
  }

  /* ── Topic filter panel (see _mapfilters.html) ── */
  .mapfilters {
    background: var(--chrome); backdrop-filter: blur(8px);
    border: 1px solid var(--line); border-radius: var(--r-md);
    box-shadow: var(--shadow-1);
    width: 210px; overflow: hidden;
    /* A flex column with min-height:0 so the list below can be the thing
       that shrinks when the rail runs out of room, instead of the panel
       overflowing its bounds. min-height:0 is required — flex items
       default to min-height:auto, which refuses to shrink below content. */
    display: flex; flex-direction: column; min-height: 0;
  }
  .mapfilters summary { flex-shrink: 0; }
  .mapfilters summary {
    display: flex; align-items: baseline; gap: 8px; cursor: pointer;
    padding: 9px 12px; list-style: none; user-select: none;
  }
  .mapfilters summary::-webkit-details-marker { display: none; }
  .mf-title { font-size: 12px; font-weight: 700; color: var(--ink); }
  .mf-hint  { font-size: 10px; color: var(--muted); }
  /* A caret that tracks open/closed without any JS. */
  .mapfilters summary::after {
    content: "▾"; margin-left: auto; color: var(--muted); font-size: 10px;
  }
  .mapfilters:not([open]) summary::after { content: "▸"; }
  /* Sized to its content, so every category shows at once — that is the
     point of docking the panel under the logo (.leftrail) instead of
     leaving it in the bottom-left stack: twelve rows plus the header is
     ~310px, and the left margin below the logo has room for it.

     No max-height of its own: it takes whatever height the rail has left
     (.leftrail is bounded, .mapfilters is a flex column that can shrink),
     so on any normal viewport the full list fits and no scrollbar appears
     at all. The old 46vh did NOT manage that — 46vh of a 731px viewport is
     336px against a ~310px list, so it clipped with room to spare. The
     overflow here only ever engages on a viewport too short to hold the
     list, where the alternative is categories running off the bottom of
     the screen and under the Sign in button. */
  .mf-list {
    display: flex; flex-direction: column;
    padding: 0 6px 6px;
    min-height: 0;
    overflow-y: auto;
  }
  .mf-item {
    display: flex; align-items: center; gap: 8px; width: 100%;
    background: none; border: 1px solid transparent;
    border-radius: var(--r-xs);
    cursor: pointer; font-family: inherit; font-size: 12px;
    color: var(--ink); padding: 6px 8px; text-align: left;
    transition: background var(--dur) var(--ease),
                border-color var(--dur) var(--ease), opacity var(--dur) var(--ease);
  }
  .mf-item:hover { background: var(--tint-06); }
  /* The isolated topic. Also the signal that clicking it again clears. */
  .mf-item[aria-pressed="true"] {
    background: rgba(197,161,90,0.16);
    border-color: rgba(197,161,90,0.35);
  }
  /* Everything else, while one topic is isolated. */
  .mf-list[data-isolated] .mf-item:not([aria-pressed="true"]) { opacity: 0.45; }
  .mf-dot {
    width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
    border: 1px solid rgba(245,243,238,0.25);
  }
  .mf-name {
    flex: 1; min-width: 0; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap;
  }
  .mf-count { font-size: 10px; color: var(--muted); flex-shrink: 0; }

  /* ── The map detail panel (.mappanel) and its scrim ──
     What opens when you click something on a map. Both maps use it: the
     story map fills it with a story's summary and article list, the article
     map with the article's own dashboard page in an <iframe> (embed mode,
     see serving/dashboards.py).

     TWO THIRDS OF THE VIEWPORT, over a dimmed map. It was a 360px column in
     the flex row, which is the shape that made the article map drop its
     panel entirely: a claim list, a leaning track and a publisher's
     correction record do not fit a 360px column, so the article map sent
     you to a new browser tab instead and left the view you had built behind
     you. At this width they fit, so a click can answer the question in
     place.

     An OVERLAY, not a flex child. As a flex child it took its width out of
     the map, which meant Plotly had to re-measure on every open and the
     bubbles had to be re-placed around it. Floating it over a scrim means
     the figure underneath never moves at all — you close the panel and the
     map is exactly as you left it.

     The scrim is what makes it modal, and that is the point of dimming
     rather than decoration: while this is open the map behind it is not
     the thing being read, and a click anywhere on it closes the panel
     rather than doing something to the figure you cannot see. */
  .mapscrim {
    position: fixed; inset: 0; z-index: 1100;
    background: rgba(9,13,18,0.62);
    backdrop-filter: blur(2px);
    opacity: 0; pointer-events: none;
    transition: opacity var(--dur-slow) var(--ease);
  }
  body.panel-open .mapscrim { opacity: 1; pointer-events: auto; }

  .mappanel {
    position: fixed; top: 0; right: 0; bottom: 0; z-index: 1200;
    /* Two thirds, with a floor so it stays usable on a small laptop and a
       ceiling so it does not become a 1700px column on an ultrawide. */
    width: clamp(420px, 66vw, 1180px);
    display: flex; flex-direction: column;
    background: var(--well);
    border-left: 1px solid var(--line-strong);
    box-shadow: var(--shadow-3);
    transform: translateX(100%);
    transition: transform var(--dur-slow) var(--ease);
    /* visibility, not display, so the transform has something to animate
       from; hidden while off-screen so nothing inside it is tabbable. */
    visibility: hidden;
  }
  /* Open state is per PANEL, not per page. The story map used to carry two
     of these — a story panel and an article panel, back when a dot opened a
     story — and one body-level class opened both at once. Every map has one
     panel now, and the split stays: it costs nothing and it is what makes a
     second panel safe to add. The body class only says "something is open",
     which is all the scrim needs to know. */
  .mappanel.is-open { transform: none; visibility: visible; }

  .mappanel-head {
    flex-shrink: 0; display: flex; align-items: center; gap: 12px;
    padding: 14px 20px;
    border-bottom: 1px solid var(--line);
    background: var(--surface);
  }
  .mappanel-title {
    font-size: 13px; font-weight: 700; color: var(--ink);
    letter-spacing: 0.02em;
  }
  .mappanel-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
  .mappanel-actions a, .mappanel-close {
    display: inline-flex; align-items: center; gap: 6px;
    background: none; border: 1px solid transparent; border-radius: var(--r-pill);
    font-family: inherit; font-size: 12px; font-weight: 600;
    color: var(--ink-3); text-decoration: none;
    padding: 6px 12px; cursor: pointer; line-height: 1;
    transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
  }
  .mappanel-actions a:hover {
    color: var(--accent); background: var(--accent-soft); text-decoration: none;
  }
  .mappanel-close { font-size: 16px; padding: 5px 11px; }
  .mappanel-close:hover { color: var(--bad); background: rgba(206,122,122,0.14); }

  /* The body scrolls, the head does not. min-height:0 is required — a flex
     child defaults to min-height:auto and refuses to shrink below content,
     which would push the scroll onto the whole panel and take the head with
     it. */
  .mappanel-body { flex: 1; min-height: 0; overflow-y: auto; }
  .mappanel-body.pad { padding: 20px; }
  /* The article map's variant: the real article page, embedded. */
  .mappanel-body iframe {
    display: block; width: 100%; height: 100%; border: 0;
  }
  .mappanel-loading {
    padding: 40px 20px; text-align: center;
    font-size: 13px; color: var(--muted);
  }

  /* ── Two charts, side by side ──
     They were stacked, which was the only thing a 360px column could do.
     At two thirds of a window they are a pair of small multiples and belong
     on one row: the question they answer together is "where does this
     article sit against its publisher, and where does that publisher sit
     against itself", and reading it needs both in one glance rather than a
     scroll between them. Falls back to stacked below 900px of PANEL width,
     which on a phone is always. */
  .panel-charts { display: grid; gap: 16px; grid-template-columns: 1fr 1fr; }
  /* Moved here from map_view.html's own <style> when the article page started
     drawing the same pair (SYSTEM.md §6b): two pages using one class is what
     this file is for, and a page-local copy is how the two would drift. */
  .chart-cap { font-size: 12px; color: var(--ink-3); margin-bottom: 4px; }
  @media (max-width: 900px) {
    .panel-charts { grid-template-columns: 1fr; }
  }

  /* "More stories to read" (.panel-more and the .pm-* cards) used to live
     here. Its one caller was the story map's story panel, which is gone: a
     story dot opens that story's lead article now, so the map has no
     story-level surface to hang a row of other stories off. The cards
     themselves are not lost, they are the news centre's own (_news_card.html
     and its styles), which is where the shape came from. */

  @media (max-width: 860px) {
    /* A bottom sheet, which is what every other panel on this site becomes
       at this width. Two thirds of a phone's WIDTH is not a readable
       column, and two thirds of its height is. */
    .mappanel {
      top: auto; left: 0; width: auto; height: 76vh;
      border-left: none; border-top: 1px solid var(--line-strong);
      border-radius: var(--r-lg) var(--r-lg) 0 0;
      transform: translateY(100%);
    }
  }

  /* ── Left rail ──
     Everything down the left edge under the logo: the section links, then
     the topic filter panel. Floats over the map, which now runs full-bleed
     beneath it (mapview.MAP_MARGIN is all zeros, so the grid reaches the
     page edges). left matches the bar's own 16px inset at this width (see
     .sitenav.mapnav), so the rail lines up under the brand; top is
     --topbar-h, the bar's height.

     pointer-events off on the rail, back on for its children, so the empty
     gaps between controls don't swallow map drags.

     There is no matching .rightrail any more. It carried the seven section
     links, which live in the shared bar now, and an empty rail fixed over
     the right-hand edge of a figure is chrome covering data. */
  .leftrail {
    /* --topbar-h plus a gap, not --topbar-h alone: that number is the bar's
       exact height (the news centre's chip bar sticks to it and has to be
       exact), and a control flush against the bar's bottom border reads as
       part of the bar rather than as the top of the rail.

       .midleft overrides this to centre on the edge (story map). The plain
       version drops further, past .mapswitchdock, so the article map's
       Compare Mode button sits under the map switch rather than on it —
       both are fixed, so neither stacks below the other on its own. */
    position: fixed; left: 16px; top: calc(var(--topbar-h) + 62px); z-index: 900;
    display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
    pointer-events: none;
    /* Stop the rail before the bottom-left stack rather than running under
       it: 56px top offset + ~56px for the account control + 16px gap. The
       filter list inside absorbs whatever is left (see .mf-list), so this
       one bound covers the whole column no matter how many section links
       sit above the panel — a per-element height budget would have to be
       retuned every time a link is added. */
    max-height: calc(100vh - 138px);
  }
  .leftrail > * { pointer-events: auto; }

  /* Story-map variant: the panel sits at the vertical middle of the left
     edge instead of docked under the logo. Opt-in per page rather than the
     default for .leftrail, because the article map's rail holds a single
     Compare Mode button, and one button floating alone in the middle of the
     left edge reads as misplaced rather than centred.

     The rail spans the whole usable left edge (top bar → the bottom-left
     stack) and centres its children inside that band, rather than using
     top:50%/translateY(-50%). Both put the panel in the same place on a
     tall viewport, but only this one is bounded: on a short viewport the
     translate version would grow past the bar and the account control in
     both directions, while this one keeps .mf-list as the thing that
     shrinks (see its comment). Height comes from top+bottom, so
     max-height is redundant here and would fight the bottom inset. */
  .leftrail.midleft {
    bottom: 72px;
    max-height: none;
    justify-content: center;
  }

  /* ── Map control buttons (.railctl) ──
     One button, the article map's Compare Mode, sitting in .leftrail. This
     was `.railnav`, a stacked column of the seven section links down the
     right-hand edge of both maps; those links are in the shared bar now
     (.sitenav.mapnav above), so what is left is the map's own control and
     the class says so. The width matches .mapfilters below it on the story
     map, so the left edge reads as one column rather than two ragged ones.

     Opaque, like the filter panel and unlike the grid-showing chrome this
     page used to have everywhere: over a control, category labels, dot
     clusters and callout titles ran straight through the text, and all of
     it moved on every pan and zoom. A control that cannot be read while the
     map moves is not showing off the map, it is in the way of it. */
  .railctl { display: flex; flex-direction: column; gap: 6px; width: 210px; }
  .railctl button, .railctl a {
    display: block; width: 100%; text-align: left;
    background: var(--chrome); backdrop-filter: blur(8px);
    color: var(--accent); border: 1px solid rgba(197,161,90,0.35);
    border-radius: var(--r-sm); font-family: inherit;
    font-size: 12px; font-weight: 600; padding: 9px 14px;
    text-decoration: none; white-space: nowrap; cursor: pointer;
    box-shadow: var(--shadow-1);
    transition: background var(--dur) var(--ease),
                border-color var(--dur) var(--ease),
                transform var(--dur) var(--ease);
  }
  .railctl button:hover, .railctl a:hover {
    background: rgba(38,52,66,0.92); border-color: rgba(197,161,90,0.55);
    /* Toward the map, not up: this is pinned to an edge, and a control that
       lifts vertically reads as detaching from the edge it is anchored to. */
    transform: translateX(-2px);
  }

  @media (max-width: 860px) {
    /* The bar is in normal flow at this width, so the rail follows it there
       instead of floating over the content beneath. Sitting right after the
       bar in the DOM, that keeps it directly under it here too. Laid out as
       a wrapping row: a vertical rail would push the map most of the way off
       a phone screen. The page script also starts the filter panel collapsed
       at this width. */
    .leftrail {
      position: static; flex-direction: row; flex-wrap: wrap;
      align-items: center; padding: 8px 16px 0;
      max-height: none;   /* in flow, so nothing below to stay clear of */
    }
    /* In flow with everything else, directly under the bar, which is where
       it already was visually. */
    .mapswitchdock { position: static; padding: 10px 16px 0; }
    /* The bar comes back into normal flow and stops hiding. Sliding chrome
       needs a pointer to bring it back and a phone has none: every gesture
       that would hide it is a touch gesture that could also reveal it by
       accident. In flow it displaces the map rather than covering it, which
       is what the map chrome has always done at this width. */
    .sitenav.mapnav {
      position: static; transform: none;
      background: var(--scrim-bg);
    }
    body.panel-open .sitenav.mapnav { right: auto; }
    /* The rail is a horizontal row here, so .midleft's centring would push
       the panel to the middle of the screen instead of the middle of the
       left edge. There is no vertical band to centre in at this width —
       everything is stacked in the flow. */
    .leftrail.midleft { justify-content: flex-start; }
    .railctl { flex-direction: row; flex-wrap: wrap; width: auto; }
    .railctl button, .railctl a { width: auto; }
    .mapfilters { width: 180px; }
  }

  /* ── Navigation progress ──────────────────────────────────────────────
     A bar across the very top while the next page is being fetched. See
     static/navprogress.js for when it runs; this only says what it is.

     Above the sticky bar's z-index on purpose: it is a statement about the
     whole window, not about the page under it. `transform` is not used for
     the growth because the bar is 2px tall and scaling a 2px box is the one
     case where the compositor's cheapness buys nothing and width animates
     just as smoothly. */
  .navprog {
    position: fixed; top: 0; left: 0; z-index: 200;
    height: 2px; width: 0;
    background: linear-gradient(90deg, var(--accent-line), var(--accent));
    opacity: 0;
    transition: width 220ms ease, opacity 200ms ease;
    pointer-events: none;
  }
  .navprog.is-on { opacity: 1; }

  /* Reduced motion keeps the BAR and drops the crawl: it is information
     about what the page is doing, not decoration, and the reader who turned
     motion off is the last one who should be left wondering whether their
     click registered. It appears at a fixed width instead of creeping. */
  @media (prefers-reduced-motion: reduce) {
    .navprog { transition: opacity 120ms ease; }
  }

