/* ============================================================
   tokens.css — SINGLE source of truth for design tokens.
   Any color, spacing, radius, or motion value used anywhere
   in the site is defined here. No other CSS file should
   declare a raw hex value — it must reference a token.
   ============================================================ */

:root {
  /* Tells the UA to paint its own furniture — scrollbars, form controls,
     the canvas behind an over-scroll — in the theme that is actually on.
     Set here rather than left to prefers-color-scheme, because the toggle
     can put a reader on the theme their OS did not ask for. */
  color-scheme: dark;

  /* ── Color: surfaces (cool dark navy) ─────────────────── */
  --bg:        #0b1322;
  --surface-1: #131d33;
  --surface-2: #1b2741;
  --surface-3: #243154;

  /* ── Color: text (cool neutral) ───────────────────────── */
  --text:      #eaf0ff;
  --text-soft: #c8d2e8;
  --muted:     #8492b0;

  /* ── Color: lines ─────────────────────────────────────── */
  --line:        rgba(234, 240, 255, 0.09);
  --line-strong: rgba(234, 240, 255, 0.18);

  /* ── Color: PRIMARY (signal blue) ─────────────────────── */
  --primary:        #4f8cff;
  --primary-bright: #7aaaff;
  --primary-soft:   rgba(79, 140, 255, 0.16);
  --primary-deep:   #2e6bdc;
  /* PRIMARY AS A BUTTON FILL, which is a different job from primary as a
     link colour and needs a different value on this theme.

     --primary is tuned to be read AS TEXT against a dark page: #4f8cff
     clears 5.77:1 there. Fill a button with it and put white on top and
     the relationship inverts — white on #4f8cff is 3.22:1, which fails
     AA for the 13px and 15px bold labels the two CTAs actually use. A
     real-DOM audit of both themes found exactly these two elements and
     nothing else.

     --primary-deep is already in this palette and resolves both sides:
     white on it reads 4.94:1, and it holds 3.76:1 against the masthead
     it sits in, clearing the 3:1 a control's edge owes. No new hue.

     The light theme needs no such split — its --primary is already
     darkened to #1f5bd1 for AA and carries white at 6.04:1 — so the
     light block below points this token straight back at --primary. */
  --btn-primary-bg: var(--primary-deep);

  /* ── Color: ACCENT (gold/yellow — sparing use only) ───── */
  --gold:        #f1c400;
  --gold-bright: #f7d44d;
  --gold-soft:   rgba(241, 196, 0, 0.16);
  /* Gold as TEXT keeps its own token even though both themes now
     carry the same value. Use --gold-text wherever gold carries
     words; --gold for everything else. The split is what makes a
     future re-darkening a one-line change instead of a hunt through
     every rule that paints gold — see the light block, where the
     override used to live and where the contrast this costs is
     written down. */
  --gold-text:   #f1c400;
  /* Gold as a LINE that has to be seen against the page, which is a
     third job again and needs a third token. --gold is a fill and
     --gold-text is words; both carry the brand value on light, where
     it measures 1.56:1 and the note in that block says outright what
     that costs. A 2px reference rule cannot absorb that — it simply
     vanishes — and the same block names the remedy: give the mark
     its own accent rather than quietly re-darkening the others.
     This is that accent, and it is the darkened amber the light
     theme used to carry throughout (5.11:1 on --bg). Dark keeps the
     brand gold, which needs no help there. */
  --gold-mark:   #f1c400;
  /* THE DOWNTOWN AREA'S OWN GOLD, and the fourth and last job gold
     does here: a FILLED FIELD on a map, big enough to be a region and
     pale enough to sit over a choropleth.

     It gets its own token for the reason --gold-mark does — "give the
     mark its own accent rather than quietly re-darkening the others."
     On dark that is the bright accent every other dark-ground mark
     uses. On light it is darkened, and the light block says by how
     much and why; nothing else on the page moves with it.

     Both the field and the dash around it take this one value, so the
     disc is a single colour wherever it is drawn — the two maps and
     the explainer's disc. downtownAreaGold() in render.js reads it,
     and viz.css, map.css and scene.css reference it directly. */
  --area-gold:   #f7d44d;
  /* THE INK UNDER THE GOLD, and the answer the two notes above keep
     pointing at: "more stroke rather than less gold."

     RETIRED ONCE, AND BACK IN A DIFFERENT SHAPE. The downtown-area
     ring is the one mark on every map that says which ground the
     ranking is measured on, so it is a graphical object WCAG 2.2 owes
     3:1, and on the light basemap the dash alone measured 1.47:1.
     Casing it — drawing it a second time underneath, WIDER and dark —
     is what a cartographer does with a pale line on a pale ground,
     and it measured 4.68:1. It was rejected, correctly: at a 2px dash
     a wider dark line underneath does not read as a heavier rule, it
     reads as an OUTLINE drawn around every dash, which is a different
     mark from the one the site means.

     The width was the whole fault. This casing is CONTINUOUS and
     exactly as wide as the gold, never wider, so it cannot appear
     outside a dash — it shows only in the GAPS between them. What a
     reader sees is one unbroken ring with gold dashes riding on it,
     which is the mark the site means, and the ring is dark enough to
     be found on any ground the disc lands on. It is translucent, so
     it reads as the gold's own shadow rather than as a second line.

     The area also carries itself: the disc is FILLED on all three
     surfaces that draw it, and a filled field is a far larger target
     than any edge. The casing is the edge's share of the job, not the
     whole of it. Do not widen it past the gold. */
  --area-casing: #0b1322;

  /* ── Masthead ──────────────────────────────────────────
     The header has its own six-token palette rather than reusing the
     page's, because it is the one band on the site whose surface is
     not --bg. Consumers (base.css, .theme-toggle) reference only these
     names, so a theme changes the masthead by re-pointing them and
     nothing else has to know.

     DARK: INRIX navy, one shade below the page, ink and accent taken
     straight from the body palette. There is no border — --header-edge
     is transparent — because the sticky settings bar below supplies
     the hairline that separates the top of the page from its content.

     LIGHT: see the light block. The header used to be pinned to these
     navy values on BOTH themes, on the argument that light was the
     least branded state the site had and the navy was the only surface
     available to fix it. It is no longer: a navy band above a near-white
     page inverted the one control that has to read as part of the
     theme — the theme toggle sat on dark while the page it toggles sat
     on light — and the brand now carries on the light theme through the
     primary CTA, the signal-blue links and the hero art. The masthead
     follows the theme, and the edge token below is what replaces the
     colour change as the boundary. */
  --header-bg:           #0b1322;
  --header-ink:          #eaf0ff;
  --header-ink-muted:    #8492b0;
  --header-line:         rgba(234, 240, 255, 0.18);
  --header-accent:       #4f8cff;
  --header-accent-bright: #7aaaff;
  /* The header's lower boundary. Transparent on dark, where the colour
     step from navy to the page already draws it; a hairline on light,
     where the two surfaces are within a few percent of each other and
     nothing else would separate them. */
  --header-edge:         transparent;

  /* ── Color: availability ramp (the ONLY multi-hue scale) ──
     Availability is a continuous measurement, so this is a
     continuous ramp with five stops — NOT a set of category
     colors. There are no letter grades in this scorecard:
     A–F Level of Service is a traffic-engineering convention
     with no counterpart in parking practice.

     Stops sit at 0 / 25 / 50 / 75 / 100 percent and are
     interpolated in between — see availColor() in js/format.js
     and the Mapbox 'interpolate' ramp in js/render.js. */
  --avail-0:   #b91c1c;   /* nothing free */
  --avail-25:  #ef4444;
  --avail-50:  #f59e0b;
  --avail-75:  #84cc16;
  --avail-100: #10b981;   /* wide open    */
  --no-data:   #6a5d47;   /* not measured — deliberately off-ramp */

  /* ── Semantic direction (trend pills, positive/negative) ──
     Aliased to the ramp endpoints so the multi-hue rule holds. */
  --ok:        var(--avail-100);
  --ok-soft:   rgba(16, 185, 129, 0.16);
  --alert:     var(--avail-25);
  --alert-soft: rgba(239, 68, 68, 0.16);

  /* ── Text on solid fills (theme-independent) ──────────── */
  --on-fill:   #ffffff;   /* text on --primary / LOS swatches */
  --on-accent: #1a1305;   /* text on --gold fills */

  /* ── Overlays / glass (THEME-FLIPPABLE) ────────────────
     These are overridden in the [data-theme="light"] block. */
  --card-grad:  linear-gradient(180deg, rgba(234, 240, 255, 0.035), rgba(234, 240, 255, 0.012));
  --hover-tint: rgba(234, 240, 255, 0.07);
  --shimmer:    rgba(234, 240, 255, 0.06);
  --shadow-pop: 0 24px 56px rgba(0, 0, 0, 0.5);

  /* ── Embedded third-party surface ─────────────────────────
     The report form is a cross-origin document with black label ink
     baked in and a transparent body — it is built to inherit a light
     host and cannot be restyled from here. So it gets a surface that
     does not follow the theme either: one paper colour, both themes,
     deliberately NOT repeated in the light block below. Named for the
     job rather than the colour, so a second embed can reuse it. */
  --embed-paper:      #ffffff;
  --embed-paper-line: rgba(16, 26, 46, 0.14);

  /* Modal scrim. ::backdrop has nothing to reach for without a token,
     and no component file may write a colour of its own. */
  --overlay-scrim: rgba(11, 19, 34, 0.72);

  /* Chart ink — consumed by js (read via getComputedStyle). */
  --chart-grid: rgba(234, 240, 255, 0.06);
  --chart-zero: rgba(234, 240, 255, 0.12);
  --chart-line: rgba(234, 240, 255, 0.18);

  /* ── Map overlays (THEME-FLIPPABLE) ────────────────────
     Scrollytelling + detail map surfaces. The Mapbox basemap
     style itself flips via js (mapStyleForTheme); these tokens flip
     the card / legend / tooltip glass and the choropleth scrim /
     border / label inks read by render.js. */
  --map-panel-grad:   linear-gradient(180deg, rgba(19, 29, 51, 0.92), rgba(19, 29, 51, 0.78));
  /* DECORATION ONLY. Any overlay that carries words takes -strong: over
     a map the effective background is whatever tile is beneath, and at
     this alpha --muted bottoms out at 2.40:1 on the lightest tile. The
     legend and the locked CTA card both used to sit here and both moved.
     See the note on .map-legend in css/components/map.css. */
  --map-glass:        rgba(11, 19, 34, 0.72);
  --map-glass-strong: rgba(11, 19, 34, 0.92);
  --map-glass-soft:   rgba(11, 19, 34, 0.45);
  --map-ctrl-icon-filter: invert(1) brightness(1.4);
  /* basemap layer inks (read by js via getComputedStyle) */
  --map-scrim:        #0b1322;
  --map-border:       rgba(11, 19, 34, 0.55);
  --map-hover-line:   rgba(234, 240, 255, 0.55);
  --map-point-stroke: #0b1322;
  --map-label:        #ffffff;
  --map-label-soft:   #eeeeee;
  --map-label-halo:   rgba(11, 19, 34, 0.85);

  /* ── Spacing scale ────────────────────────────────────── */
  --space-1:  4px;
  --space-2:  8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 36px;
  --space-7: 56px;
  --space-8: 88px;

  /* ── Control interiors ──────────────────────────────
     A button's inset is set by its TYPE SIZE, not by the layout grid, so
     it gets its own ladder. The scale above cannot express this one: the
     large button wants ~26px of horizontal inset and the nearest layout
     steps are 24 (cramped) and 36 (bloated). Forcing it would flatten
     .btn--xl into .btn.

     These are the values that were already in use, named rather than
     changed — so the ladder is auditable in one place instead of being
     re-derived across four component files. Horizontal runs ~1.7x
     vertical, which is what keeps a pill reading as a pill at every size.

     Use these for anything the reader clicks or types into. Use the
     --space-* scale for everything BETWEEN elements. */
  --control-py-pill:  6px;   /* .chart-toggle — compact tab pills */
  --control-px-pill: 14px;
  --control-py-sm:    8px;   /* .btn--sm — chrome-weight button */
  --control-py-md:   10px;   /* .btn--tertiary */
  --control-px-md:   16px;
  --control-py-lg:   12px;   /* .btn — the default */
  --control-px-lg:   20px;
  --control-py-xl:   16px;   /* .btn--xl — hero CTA */
  --control-px-xl:   26px;

  /* ── Radii ────────────────────────────────────────────── */
  --radius-sm: 10px;
  --radius-md: 16px;
  --radius-lg: 24px;
  --radius-pill: 999px;

  /* ── Typography ───────────────────────────────────────── */
  --font-display: "Lato", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-body:    "Lato", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono:    "JetBrains Mono", ui-monospace, "SF Mono", Consolas, monospace;

  /* ── Type scale (display via clamp() in components) ──── */
  --text-xs:  12px;
  --text-sm:  13px;
  --text-md:  15px;
  --text-lg:  17px;
  --text-xl:  20px;

  /* ── Figures ──────────────────────────────────────────
     ONE size for every number that shares a row with other numbers.
     Before this, a stat-lead row ran 68px headline beside 24px support
     and city.html stacked a 38px band on a 21px one: same treatment
     above (12px caps label), 2-2.8x apart below, which reads as an
     accident rather than a rank.

     Lato 900 and JetBrains Mono 700 have the same digit cap-height at
     a given size (measured: 24px of ink at 32px, ratio 1.000), so one
     value genuinely lands both families on the same visual height --
     no per-family optical correction needed.

     32px is the ceiling the narrowest column allows: "$1 : $1.62" is
     126px at 24px in a 210px column, so 32px puts it at 168px and
     leaves 42px for a wider ratio.

     Hierarchy still exists -- it is carried by label colour, order and
     the ramp, not by size. */
  --figure-size: clamp(24px, 2.6vw, 32px);
  /* Units and suffixes. 0.45em x 32px = 14.4px, which is exactly what
     "/hr" and the range "%" already rendered at, so those two do not
     move; it only rescues the superscript that would have fallen to
     9px when its figure came down from 68px. */
  --figure-unit: 0.45em;
  /* Figures align on one baseline across a row, so they need one
     leading. */
  --figure-leading: 1.05;

  /* ── Motion ───────────────────────────────────────────── */
  --ease-out-spring: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-out:        cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast:   180ms;
  --dur-med:    320ms;
  --dur-slow:   560ms;

  /* ── Layout ───────────────────────────────────────────── */
  --max-width: 1440px;

  /* ── The 12-column grid ────────────────────────────────
     Every page gutter is --space-5, so the content width at max is
     1440 - 48 = 1392, and twelve columns with 24px gutters gives a
     94px column on a 118px step. Column edges therefore land at

       24 · 142 · 260 · 378 · 496 · 614 · 732 · 850 · 968 · 1086 · 1204 · 1322

     and the content's right edge at 1416.

     WHY THIS EXISTS. Before it, the site had exactly one alignment —
     the 24px left margin — and nothing else. countries.html carried
     EIGHT different right edges (416, 566, 701, 744, 849, 859, 1400,
     1401), and on city.html two bands stacked directly on top of each
     other sat on a 3-up and a 4-up rhythm, so eight figures in two rows
     produced seven different vertical edges. Each section had been
     solved locally and correctly; none had been solved against a shared
     structure, so the eye could never establish one.

     HOW TO USE IT. A block that spans the full content width and
     divides itself uses `repeat(N, minmax(0, 1fr))` with
     `gap: var(--grid-gutter)` — any two such blocks with the SAME N
     align, because they share a container and a gutter. A block that
     does not fill the width takes a --span-* below, which is that
     column count measured out in pixels so it ends ON a column edge
     rather than wherever its content happened to stop.

     WHAT THE GRID GOVERNS, AND WHAT IT DOES NOT. It governs LAYOUT —
     the containers that divide a page's width and decide where blocks
     begin. It does not govern a COMPONENT'S INTERNALS: the range
     headline on countries.html (name · ramp · name) and the .mx-rate
     dt/dd rows on methodology.html size themselves from their own
     content inside a capped block, and snapping them to page columns
     would either truncate a country name or open a gap for no reason.
     A pill, a heading and a table cell are content too. If a rule here
     would force a piece of text to a width its text did not ask for,
     it is the wrong rule.

     It also does not govern 2026/index.html. That page is the
     scrollytelling narrative built on scenes.css — full-viewport pinned
     panels rather than a stack of sections — and applying a section
     grid to it would be normalisation for its own sake.

     Note for anyone extending this: a 3-up and a 4-up cannot share
     interior edges — 12/3 lands on columns 1,5,9 and 12/4 on 1,4,7,10,
     which coincide only at the first. Two adjacent bands that must
     align have to share a column count. And a container that sits on a
     column line is only half the job: if it is itself a grid, its
     column count has to divide its span evenly or its children land
     nowhere. That is why .stat-lead is 6/6 rather than 7/5 — six
     divides by both 3 and 2, which are the two arrangements its support
     figures actually take. */
  /* 12 is baked into the --span-* percentages below rather than read
     from a variable, because calc() cannot divide by a custom property
     and produce a percentage. This is here as the documented number the
     spans were derived FROM, not as a knob — changing it alone does
     nothing. */
  --grid-cols: 12;
  --grid-gutter: var(--space-5);          /* 24px */

  /* SPANS ARE PERCENTAGES, NOT PIXELS, and that is load-bearing.

     Written as fixed pixels off a 1392px content width they were wrong
     the moment a scrollbar appeared: the real content width at a 1440
     viewport is ~1377, so a "684px" six-column block missed the column
     edge its neighbour's `1fr` grid had actually landed on by 7px. A
     span has to be derived from the same width the fr columns divide,
     which means a percentage of the container.

     The algebra, for n of 12 columns with gutter g:
       span(n) = n*(W - 11g)/12 + (n-1)g
               = W*n/12 + g*(n/12 - 1)
     so the percentage is n/12 and the pixel term is always negative.
     Exact at every viewport width, scrollbar or no scrollbar.

     Use these ONLY on a block whose parent is the full content width —
     they are a fraction of the parent, so inside a nested grid column
     they would divide that column instead. */
  /* A COMPLETE SCALE, DELIBERATELY — most of these are unused today.
     That is the difference between a scale and a dead rule: --span-5
     exists so the next two-column section takes it instead of inventing
     41.6667%, which is exactly how the six unrelated right edges this
     grid was built to fix came about in the first place. Do not prune
     to the ones currently referenced. */
  --span-3:  calc(25%      - 18px);
  --span-4:  calc(33.3333% - 16px);
  --span-5:  calc(41.6667% - 14px);
  --span-6:  calc(50%      - 12px);
  --span-7:  calc(58.3333% - 10px);
  --span-8:  calc(66.6667% -  8px);
  --span-9:  calc(75%      -  6px);
  --span-12: 100%;

  /* Both were magic numbers that landed between column edges — 1080
     falls 42px past column 9 and 720 falls 36px past column 6, which is
     why prose blocks never lined up with anything beside them. */
  /* Kept as PIXELS, not spans: both are used as `max-width` on blocks
     whose parent is not always the full content width (.mx-doc sits in
     a grid column, for one), and a percentage there would divide the
     wrong thing. These are the nine- and six-column widths measured at
     the 1440 breakpoint, so they land on grid edges where it matters
     and simply cap the measure everywhere else.
     Both were magic numbers before — 1080 fell 42px past column 9 and
     720 fell 36px past column 6. */
  --content-width: 1038px;                /* 9 cols, was 1080px */
  --reading-width: 684px;                 /* 6 cols, was 720px  */

  /* ── Prose measures ────────────────────────────────────
     THREE widths, drawn from the column grid, and every prose block
     takes one of them.

     They were ch-based before — 55ch, 52ch, 72ch, 56ch — which is the
     right instinct and the wrong unit for this problem. A ch measure is
     relative to the block's own font size, so four sensible caps at four
     type sizes produced SIX different pixel widths stacked down one
     page: 416, 566, 650, 701, 708, 859. Individually defensible,
     collectively noise.

     Column measures repeat instead. The same token gives the same right
     edge whatever size the type is, so a reader scanning down the page
     sees the same few edges again and again — which is what reads as
     order. The column count for each was chosen so the resulting ch
     measure still lands in the comfortable 45-75 band at the size that
     role is set in. */
  --measure-note: 330px;                  /* 3 cols — ~51ch at 13px */
  --measure-text: 566px;                  /* 5 cols — ~65ch at 15px */
  --measure-long: var(--reading-width);   /* 6 cols — document bodies */

  /* ── Chrome heights (measured at runtime, defaulted here) ──
     .psc-section-nav is position: fixed with a content-driven height: its
     rows are 44px controls, and the nav drops to a second, horizontally
     scrolling row below 640px. js/header-offset.js observes it and
     overwrites --header-h; these values are the settled measurements
     (76px / 100px), so first paint is already correct and a JS failure
     degrades to the right number rather than a guess. Re-measure them if
     --space-4 moves again: the one-row header is padded with it, which is
     what took this from 80 to 76.

     --ps-bar-h is the sticky settings bar, published by
     js/sticky-controls.js. It stays 0 on pages that have no bar. */
  --header-h: 76px;
  --ps-bar-h: 0px;

  /* ── Chrome, for a page that is not ours ──────────────────
     --header-h is OUR nav and nothing else. That was the only
     chrome that existed while the site stood alone, so the two
     ideas were never separated. They have to be now: the bundle
     is vendored into the INRIX WordPress theme, where the site's
     own masthead sits above our nav and an editor may also be
     looking at it under the WP admin bar.

     Two numbers, because chrome does two unrelated things and a
     slide has to ask about them separately. A HOST SETS THESE TWO:

       --chrome-h       how much of the VIEWPORT is covered at the
                        top AT ANY SCROLL POSITION — the pinned
                        chrome, fixed or sticky. What a slide has
                        to give up so that all of it is visible,
                        and what a snap stop has to land below.

       --chrome-flow-h  how much IN-FLOW content sits above our
                        first section — chrome that has already
                        pushed the document down rather than
                        sitting on top of it. Only the hero reads
                        it, because the hero is the only section
                        with nothing above it to scroll away.

     THEY ARE NOT TWO HALVES OF ONE NUMBER, and an early draft of
     this file wrongly derived one from the other. A host's masthead
     can be in flow and NOT pinned (it scrolls away: it counts in
     --chrome-flow-h and not in --chrome-h), and a WP admin bar can
     be pinned and NOT above us in flow (the reverse). Standalone,
     .psc-section-nav is position: fixed, so it is the whole of
     --chrome-h and none of --chrome-flow-h.

     Derive the hero's padding from them rather than asking a host
     for a third number it would have to keep consistent: at rest
     the hero's top edge sits at document y = --chrome-flow-h, and
     pinned chrome covers the top --chrome-h of the viewport, so the
     hero is overlaid by whatever the pinned chrome has that the
     in-flow chrome does not. Never negative — hence max().

     See docs/embedding.md for what a host publishes. */
  --chrome-h: var(--header-h);
  --chrome-flow-h: 0px;
  --chrome-overlay-h: max(0px, calc(var(--chrome-h) - var(--chrome-flow-h)));

  /* ── Layering ─────────────────────────────────────────────
     One scale, so a new overlay never has to guess. The fixed
     header always wins; popovers are the only thing allowed above
     it, because a dropdown clipped by the header is unusable. */
  --z-base:         0;   /* in-flow content                        */
  --z-raised:       1;   /* sticky thead inside its own scroller   */
  --z-map-scrim:    2;
  --z-map-overlay:  3;
  --z-map-control:  4;
  --z-sticky:      20;   /* page-level sticky settings bar         */
  --z-progress:    40;   /* landing-page scroll-progress rail      */
  --z-header:      50;   /* fixed site header                      */
  --z-popover:     60;   /* dropdowns that must clear the header   */

  /* Sticky-bar separation. --shadow-pop is a modal-weight shadow;
     on a 56px bar it reads as a dialog rather than a held row. */
  --shadow-sticky: 0 6px 16px -10px rgba(0, 0, 0, 0.55);

  /* Edge fade for horizontally scrollable rows. A mask, not a color,
     but it lives here so no component file needs a raw hex. */
  --mask-fade-x: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
}

/* Two-row header. Same breakpoint as the layout switch in base.css. */
@media (max-width: 1120px) {
  :root { --header-h: 100px; }
}

/* Portrait tablet takes the nav back to one row — see the matching
   block in base.css for why 1120px no longer describes it. Same 60px
   as the landscape-phone case below and for the same reason: a 44px
   control plus --space-2 above and below. It must follow the two-row
   rule above to win, since the two queries both match a tablet.
   header-offset.js re-measures and overwrites this at runtime; it is
   here so first paint is already right. */
@media (min-width: 721px) and (orientation: portrait) {
  :root { --header-h: 60px; }
}

/* ── Landscape phone: height is the scarce axis ────────────
   A phone on its side is 852x393. That is WIDE, so every width query
   on the site reads it as a small laptop and hands it the two-row
   header meant for narrow screens — 101px of nav plus a 96px settings
   bar, 197px of a 393px viewport. Half the screen was chrome and four
   table rows were visible.

   Both arms matter. max-height is the condition that is actually true;
   it is also sufficient on its own, because no portrait phone is 500px
   tall. 501px separates every phone landscape (the tallest, a 430x932
   Pro Max turned, gives 430) from the shortest laptop, so no desktop
   ever enters this block.

   60px = a 44px control plus --space-2 above and below. header-offset.js
   re-measures and overwrites this at runtime; it is here so first paint
   is already right and a JS failure degrades to the correct number. */
@media (max-height: 500px) {
  :root { --header-h: 60px; }
}

/* ============================================================
   LIGHT THEME — applied via <html data-theme="light">.
   Only the tokens that must change for a light surface are
   overridden; spacing, radii, motion, type and the LOS scale
   are theme-independent and inherited from :root.

   The scrollytelling + detail Mapbox basemaps flip to a light
   style in js; the map-overlay glass and basemap inks below flip
   with them so the whole map reads light.
   ============================================================ */
:root[data-theme="light"] {
  color-scheme: light;

  /* surfaces — cool off-white */
  --bg:        #f5f8fc;
  /* Not pure white. Every other surface on this theme is tinted toward
     the signal blue, and a card that drops the tint reads as a hole in
     the page rather than a raised plane. Verified against the inks it
     carries: --muted 5.61:1, --primary 5.93:1. --gold-text used to be
     measured here too, at 5.34:1; it is now the brand gold on both
     themes and reads 1.63:1 on this surface — see the accent block
     below for what that was traded for. */
  --surface-1: #fbfdff;
  --surface-2: #eef2f9;
  --surface-3: #e1e8f3;

  /* text — navy ink */
  --text:      #101a2e;
  --text-soft: #38465f;
  --muted:     #586781;

  /* lines */
  --line:        rgba(16, 26, 46, 0.10);
  --line-strong: rgba(16, 26, 46, 0.20);

  /* primary — darkened for AA contrast on white (links + text) */
  --primary:        #1f5bd1;
  --primary-bright: #1748ac;
  --primary-soft:   rgba(31, 91, 209, 0.12);
  --primary-deep:   #15388a;
  /* Already AA as a fill: white on #1f5bd1 is 6.04:1. See the dark
     block for why the two themes differ here. */
  --btn-primary-bg: var(--primary);

  /* accent — kept as vibrant as dark mode (same values, no deepening) */
  --gold:        #f1c400;
  --gold-bright: #f7d44d;
  --gold-soft:   rgba(241, 196, 0, 0.16);
  /* THE SAME GOLD AS DARK, BY DECISION, and it is the one ink on this
     theme that does not clear AA. The brand gold measures 1.56:1
     against --bg; the darkened amber it replaced (#856500) measured
     5.11:1 on --bg, 5.44:1 on white and 4.85:1 on --surface-2.

     What that costs, in the four places this token lands:

       · the thousands comma and the decimal point inside the big
         counters, and the full stop after "surveyed" in the hero —
         punctuation, but punctuation that all but disappears here;
       · .sp-empty .eyebrow on the state pages, which is real words
         at 13px and is the one to watch;
       · the field horizon's named field on this theme (see viz.css),
         which is a picture rather than ink — the darker amber was
         carrying that outline against a near-white page at the tenth
         of an alpha the contrast budget allows, and the brand gold at
         the same alpha draws pale cream.

     Kept as one token on purpose. If the outline needs its weight back
     without darkening the words again, the move is to give the drawing
     its own accent in viz.css — not to split this value quietly. */
  --gold-text:   #f1c400;
  /* THE BRAND GOLD ON BOTH THEMES, like every other accent above, and
     the last of the four to give up its light-theme substitute. It
     used to hold #856500 — a darkened amber measuring 5.11:1, clear
     of the 3:1 a graphical mark owes — because the mark it paints is
     the odds scene's 2px reference rule and the brand gold reads
     1.56:1 against this page.

     That is now a deliberate trade, made the same way --gold-text
     made it: the accent's identity is worth more here than the
     margin over 3:1, and the rule is a line the eye finds by its
     colour rather than one it has to read. It is the weakest mark in
     that drawing on this theme, and the one to check first if the
     scene ever stops reading.

     The token stays. It is still the only thing painting that rule
     (viz.css, .viz-cs-mean line), so restoring weight is this line
     alone — either back to a darkened value or, better, by giving
     the rule more stroke rather than less gold. */
  --gold-mark:   #f1c400;
  /* THE DOWNTOWN AREA'S OWN GOLD, DARKENED. The dark block says why
     this mark gets a token of its own; this is the value that pays
     for it.

     Brand gold (#f1c400) filling the disc read as almost nothing on
     the cream basemap — 1.47:1 for the dash and a wash too pale to
     find for the field — and raising the alpha did not fix it,
     because the problem is the hue's lightness and not its coverage.
     Two remedies were tried on the edge first and both failed on
     their own terms: a dark casing drawn WIDER than the dash read as
     an outline around every dash, and --gold-on-wash reads brown
     rather than as this brand's gold.

     So the hue is darkened, and ONLY for this mark — but no further
     than it has to be, because it does not carry the contrast alone.
     The first darkening went to #8a6500, which cleared 3:1 against
     the cream basemap on its own and read more bronze than gold for
     it. With a continuous casing under the dash (see --area-casing
     below) the edge is legible without the hue having to do all of
     the work, so the gold comes back up to a value that still reads
     as this brand's gold: the same hue angle as --gold, taken down in
     lightness, not rotated.

     The pair is what meets the 3:1 WCAG 2.2 owes a graphical object.
     The casing measures about 3.2:1 against the cream basemap and
     about 3.0:1 against the middle of the availability ramp — the
     mid orange that is the ground under the disc in a city like
     Washington — which is the worst ground the mark lands on, and the
     ground no gold could clear on its own at any lightness that still
     looked gold. The gold is the mark's colour and the casing is its
     legibility; neither is asked to be both.

     --gold, --gold-text and --gold-mark are all untouched. That is
     the whole point of a fourth token — the one gold that had to move
     moved, and the brand value stayed everywhere a reader meets it in
     type or in a chart. */
  --area-gold:   #c69200;
  /* The light theme's ink under the gold. See the long note in the
     dark block for why this exists and, more importantly, for the one
     rule it has to keep: CONTINUOUS, and never wider than the gold it
     sits under. It shows in the gaps between dashes and nowhere else.

     Warmer than the dark theme's, because here it is read as the
     gold's own shadow on a cream ground rather than as a dark line on
     a dark map. Painted translucent — see AREA_CASING_ALPHA in
     render.js — which is what keeps it a shadow and not a second
     ring. */
  --area-casing: #241a00;

  /* GOLD ON A GOLD WASH, which is the one place the brand value cannot
     go. The closing map card paints its own gold ground, so the accent
     sitting on it is the rare mark whose background is not --bg — and
     brand gold on a gold wash is unreadable rather than merely low
     contrast. Darkened to carry the same hue at text weight.

     Distinct from --gold-text on purpose: that one deliberately kept
     the brand value on both themes, and this is not a substitute for
     it. It applies only where gold is the SURFACE. */
  --gold-on-wash: #7a5c00;

  /* ── Masthead on light ────────────────────────────────
     The header follows the theme now (see the masthead block at the
     top of this file for why it stopped being navy on both).

     White rather than --bg: the band has to be distinguishable from
     the page it sits above, and #fff against #f5f8fc is the smallest
     step that does it without introducing a fourth surface. The step
     alone is a couple of percent, so --header-edge carries a real
     hairline here — on dark the colour change was the boundary and
     none was needed.

     Inks are the body palette's, so the header measures what the rest
     of the page measures: --muted 5.24:1 on white for the nav links,
     --text 16.3:1 for the brand and the current page, --primary 6.04:1
     for the edition mark and the hover state. No new pair. */
  --header-bg:           #ffffff;
  --header-ink:          #101a2e;
  --header-ink-muted:    #586781;
  --header-line:         rgba(16, 26, 46, 0.16);
  --header-accent:       #1f5bd1;
  --header-accent-bright: #1748ac;
  --header-edge:         rgba(16, 26, 46, 0.12);

  /* direction pills — re-point to AA-safe values on light */
  --ok-soft:    rgba(16, 185, 129, 0.16);
  --alert-soft: rgba(239, 68, 68, 0.16);

  /* A lighter scrim: on a light page a 0.72 wash reads as a blackout. */
  --overlay-scrim: rgba(16, 26, 46, 0.42);

  /* overlays / glass — dark ink on light */
  --card-grad:  linear-gradient(180deg, rgba(16, 26, 46, 0.045), rgba(16, 26, 46, 0.015));
  --hover-tint: rgba(16, 26, 46, 0.055);
  --shimmer:    rgba(16, 26, 46, 0.06);
  --shadow-pop: 0 24px 56px rgba(16, 26, 46, 0.16);
  --shadow-sticky: 0 6px 16px -10px rgba(16, 26, 46, 0.28);

  /* chart ink */
  --chart-grid: rgba(16, 26, 46, 0.10);
  --chart-zero: rgba(16, 26, 46, 0.18);
  --chart-line: rgba(16, 26, 46, 0.24);

  /* map overlays — light glass + light basemap inks */
  --map-panel-grad:   linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.82));
  /* Decoration only — see the dark block. 3.75:1 worst case here. */
  --map-glass:        rgba(255, 255, 255, 0.82);
  --map-glass-strong: rgba(255, 255, 255, 0.95);
  --map-glass-soft:   rgba(255, 255, 255, 0.62);
  --map-ctrl-icon-filter: none;
  --map-scrim:        #ffffff;
  --map-border:       rgba(16, 26, 46, 0.22);
  --map-hover-line:   rgba(16, 26, 46, 0.55);
  --map-point-stroke: #ffffff;
  --map-label:        #101a2e;
  --map-label-soft:   #33415e;
  --map-label-halo:   rgba(255, 255, 255, 0.9);
}
