/* tokens.css — the ONLY file in web/ where a color literal may appear.
 *
 * Enforced by tests/test_web_tokens.py, which scans every other .css/.js/.html
 * under web/ and fails on any hex / rgb() / hsl() / named color in a style
 * position. If you need a new color, add a SEMANTIC token here and use the var
 * — do not reach for a literal at the call site, and do not add a third theme
 * block (R1.3).
 *
 * Naming rule (R1.2): tokens say what a color MEANS, never what it looks like.
 * `--data-neg`, never `--red`. That is what makes a theme a pure re-assignment
 * of the same names rather than a second design.
 *
 * Provenance of the light palette (recorded 2026-08-07): authored from
 * CLAUDE_CODE_TASK_ui_template.md's hex list, NOT lifted from the landing page
 * — the landing is not in this repo (no `Newsreader` anywhere under web/, no
 * file matching *landing*). When it arrives, diff its palette against this file
 * before WP4.3. Two palettes that disagree is the "third palette" R1.3 bans.
 */

/* -------------------------------------------------------------------------
 * Fonts.
 *
 * SELF-HOSTED since 2026-10-03 (SEC-1): web/css/fonts.css and the woff2
 * files beside it. Source pages still name the Google Fonts CDN, which is
 * fine for a local preview; on the published site model/site_build.py swaps
 * that link for fonts.css at post-render, and the CSP no longer allows the
 * Google hosts at all. (This used to argue the opposite, from the precedent
 * of Leaflet on unpkg; Leaflet is vendored now too.)
 *
 * Note for anyone auditing a diff: reconcile.css named "IBM Plex Mono" in five
 * places from the day it was written while NOTHING in web/ ever loaded a font
 * file. It had been silently falling back to the system monospace. All three
 * families are genuinely delivered for the first time here.
 * ------------------------------------------------------------------------- */
:root {
  /* R2.1 — three families, by ROLE. A fourth family is a rule violation. */
  --font-display: "Newsreader", Georgia, "Times New Roman", serif;
  --font-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-data: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* R2.2 — the whole app type scale. There are no other sizes. */
  --t-display: 1.35rem;
  --t-title: 1.05rem;
  --t-body: .9rem;
  --t-data: .8rem;
  --t-label: .68rem;

  /* R2.3 — the label/eyebrow treatment is a token, not a local restyle. */
  --label-tracking: .12em;

  /* R4.1 — app spacing scale. 4/8/12/16/24 and nothing between. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;

  /* R4.2 — three radii exist: panel, control, pill. */
  --r-panel: 10px;
  --r-ctl: 6px;
  --r-pill: 999px;

  /* R6.1 — the only durations allowed to animate anything. */
  --dur-ui: 180ms;
  --dur-theme: 400ms;
  --ease: cubic-bezier(.2, .6, .3, 1);

  /* Pre-existing layout var, kept: the S&D dock width on the map page. */
  --sd-dock-width: 66%;
}

/* -------------------------------------------------------------------------
 * LIGHT — editorial (pine on paper)
 * ------------------------------------------------------------------------- */
:root {
  --bg: #FCFBFA;
  --surface: #FFFFFF;
  --surface-2: #F7F6F4;

  /* WP2.5 — THE NEUTRAL RAMP, RE-SOLVED WITH CONTRAST AS THE HARD CONSTRAINT.
   * R3.10 outranks R3.7, so this ramp is spaced by what survives AA rather
   * than by what looks balanced.
   *
   * --ink is PINNED (near-black on paper is the editorial anchor) and
   * --ink-faint is dragged inward until it clears 4.5:1 on --surface-2 — the
   * LIGHTEST panel, which is where a dark foreground has least to work with
   * and is where .bhead / .top / thead th actually paint their labels. That
   * leaves a span of L 0.216..0.546, divided into three equal steps of 0.110.
   *
   * WHY --surface-2 AND NOT --surface. The WP2 gate measured every foreground
   * against --surface (#FFFFFF) and reported four failures. --surface is the
   * whitest panel in the theme, so that choice flattered light mode and hid a
   * fifth: --ink-body was 4.81:1 on white and 4.46:1 on the panel headers it
   * is actually printed on. R3.10 says "the surface it is ACTUALLY painted
   * on", and the difference between those two readings is the difference
   * between a passing token and a failing one.
   *
   * WHAT MOVED, and every one of these is a visible change to the product:
   *   --ink-faint  #A6A9A5 -> #6E716E   2.20 -> 4.57:1
   *   --ink-body   #6F7370 -> #4E5253   4.46 -> 7.32:1
   *   --accent-modelled #404444 -> #313535   9.14 -> 11.50:1
   * The ramp is DARKER and more compressed than it was. Captions, notes and
   * axis labels are no longer a pale grey; that pale grey was unreadable, and
   * "quiet" is now carried by size and weight rather than by fading text out
   * until it fails a legibility floor.
   *
   * THE MERGE THE SPEC ANTICIPATED DID NOT HAPPEN, and here is the number.
   * WP2.5 warned that --ink-faint at 4.5:1 might land within ΔL 0.10 of
   * --ink-body, in which case the honest answer is to merge them. Measured
   * against #FFFFFF that is exactly what happens: faint lands at L 0.563 and
   * body sits at L 0.551, ΔL 0.012 — indistinguishable. It is only by moving
   * --ink-body DOWN as well, which the worst-case surface forces anyway, that
   * three steps survive at ΔL 0.110. Four neutral slots were kept because they
   * were affordable at the harder floor, not because the target was defended.
   * ------------------------------------------------------------------------ */
  --ink: #191A18;
  --ink-body: #4E5253;
  --ink-faint: #6E716E;

  --line: #E9E8E4;
  --line-strong: #D8D7D3;

  --pine: #0B2B23;
  /* THE INK THAT GOES ON --pine, AND WHY IT IS ITS OWN TOKEN.
   * --pine is the one fill in the system that is DARK IN BOTH THEMES (#0B2B23
   * light, #0F3A2E dark) — it is a brand mark, not a surface, so it does not
   * invert. Its foreground therefore does not invert either, and any token that
   * does is wrong in one theme by construction. --bg was used, which is exactly
   * that mistake: correct at 14.68:1 on paper and 1.52:1 in dark, where the
   * product mark rendered as a blank green block with the word inside it
   * invisible. Measured on t1_atlas.html 2026-08-08, present in hub_sd.css too,
   * and about to be copied into t1_product.css a third time.
   * A pair whose CONTRAST is the invariant needs both halves named. */
  --on-pine: #FCFBFA;

  /* R3.1 — the evidence-grade colors. These four carry the product's whole
   * claim about how good a number is, so they are the tokens most likely to be
   * misused. measured = observed; modelled = derived/allocated/carried-forward;
   * warn = unexplained or stale ONLY; neg = a negative value. */
  --accent-measured: #1D9E6E;
  --accent-measured-ink: #157A55;
  /* WP0.5, twice. Was #9A9D99, which sat dL 0.04 from --ink-faint — "modelled
   * data" and "text you may ignore" rendered as the same grey, which is a
   * claim about evidence, not a style detail (R3.2/R3.6). The first fix moved
   * it to #717676 and landed it on --ink-BODY instead (dL 0.010): a modelled
   * number now matched its own row label. That second collision survived a
   * 66-pair check that simply never gated modelled against ink-body, so the
   * pass was a statement about the pair LIST, not the palette.
   *
   * There is exactly one gap wide enough in the neutral ramp — between --ink
   * and --ink-body — and this sits at its midpoint: dL 0.167 from ink, 0.168
   * from ink-body, 0.348 from ink-faint. Chroma 0.005 keeps it a true neutral
   * (giving it a hue pushes it into the series corridor; tried, 4 failures).
   *
   * Being DARKER than body text is deliberate: a modelled number is data and
   * should read as data. Its lower confidence is carried by the dashed stroke
   * and the Modelled badge, not by washing the number out. In DARK the same
   * slot puts it LIGHTER than body text: R3.6 says "darker/cooler", but darker
   * on a #0C0F11 ground is invisible. The invariant is the dL; the direction
   * is whatever moves it away from the background.
   *
   * WP2.5 moved this with the rest of the ramp (#404444 -> #313535) to hold
   * its position at the first step below --ink. The ordering it has to keep is
   * stated as CONTRAST, not lightness — --ink > --accent-modelled > --ink-body
   * > --ink-faint — which is one rule for two themes rather than two mirrored
   * ones, since light descends in L and dark ascends. Enforced by
   * tests/test_token_pairs.py, which failed this ramp's ORDER when an earlier
   * WP2.5 draft placed dark --ink-body above --accent-modelled. */
  --accent-modelled: #313535;
  /* WP2.5 — both darkened to clear AA on --surface-2 (2.89 -> 4.56:1 and
   * 3.19 -> 4.56:1). Darkened along their OWN hue rather than shifted, because
   * the ~52 degrees between amber and rose is the palette's best-earned
   * property: it is what lets an unexplained residual and a negative sit in
   * adjacent columns of the same table. Measured after the move it is 53.2
   * degrees (H 81.0 vs H 27.8), so the move paid for itself.
   *
   * Chroma was held and re-fitted to the sRGB gamut rather than clamped. A
   * naive darkening of --warn produced #9B6D00, whose blue channel had been
   * silently clipped to zero — the hex still renders, it is just no longer the
   * colour the maths asked for, and the reported hue is then a description of
   * the clipping. #936900 is inside the gamut at C 0.114. */
  --warn: #936900;
  /* WP5 — ROSE -> A CONVENTIONAL RED (#A75B52 -> #8B1E1C), and the audience is
   * the reason. This reader has been looking at EIA, Platts and Bloomberg
   * tables for years and reads RED as down; a dusty rose reads as a design
   * choice, which is a thing to notice rather than a thing to use. Chroma
   * .101 -> .145 and lightness .558 -> .419 is the whole move: same hue arc
   * (27.8 -> 26.9), more red, darker.
   *
   * THE PROPERTY THAT WAS PROTECTED THROUGH THE MOVE is the ~52 degrees to
   * --warn, which WP2.5 called the palette's best-earned and which is what lets
   * an unexplained residual and a negative sit in adjacent columns of one
   * table. Measured after: 54.1 degrees (H 80.95 vs 26.90). It went UP.
   *
   * ONE PROXIMITY, MEASURED AND RECORDED RATHER THAN DISCOVERED LATER: this now
   * shares the red arc with --series-3 (#A6392B, brick). They are not gated —
   * --data-neg lives in the drawer view and --series-3 in the chart view — and
   * they cannot swap roles, because R3.4 forbids a series token from painting
   * text and this token paints nothing else. What separates them is the FLOOR
   * EACH OWES, which is a real reason and not a coincidence: --series-3 is
   * non-text at 3:1 and sits at 6.00:1, --data-neg is text at 4.5:1 and sits at
   * 8.45:1, so the two land ΔL 0.079 / ΔE 0.080 apart — above R3.7's 0.06 ΔE
   * term with a third to spare. TRIP: any view that puts a series swatch beside
   * a signed numeral makes them peers, and then this pair has to be re-solved
   * rather than argued. WP0.5 shipped --series-6 BYTE-IDENTICAL to --data-neg
   * once; the distance here is deliberate so that never reads as the same
   * mistake. */
  --data-neg: #8B1E1C;
  /* R3.1 — positive is the DEFAULT STATE and gets no colour of its own. This
   * was #157A55 (green), which made every positive number carry the same
   * signal as a MEASURED badge; WP0.5 proposed keeping it as a declared alias
   * and that was overruled, correctly — colouring both signs spends the
   * reader's attention on the half that needs none, and the sign is already
   * carried by the true minus sign (R7.3).
   *
   * Kept as a NAME rather than deleted so call sites can say "this cell is a
   * signed quantity" without knowing the rule; var(--ink) means the rule lives
   * here, in one place, and re-deciding it is one line. A view that genuinely
   * needs affirmative positive colour (a diverging scale) draws from the
   * series corridor, never from green.
   *
   * WP2.5 note: this needed no edit, which is the var() earning its keep —
   * --ink moved zero and would have carried --data-pos with it if it had. */
  --data-pos: var(--ink);

  /* R3.4 — exactly six series slots plus the tail bucket. A seventh category
   * becomes "Other"; it does not get a seventh color.
   *
   * WP5 REPLACED THE WHOLE RAMP AGAIN, and this time the driver is not a
   * collision, it is the REGISTER (R3.15). The ramp this replaces was six hues
   * in a 145-degree corridor running H 205 -> 344: teal, cyan-blue, periwinkle,
   * violet, orchid, magenta. Every word in that list is a word about a design
   * project. The reader is a physical trader or a midstream analyst who has
   * read EIA, Platts, Genscape and Bloomberg charts for a decade; those charts
   * are boring on purpose, and a lavender pipeline costs credibility in the
   * first two seconds, before a single number has been read.
   *
   * The corridor was not wrong given its own premise. R3.6 reserved green
   * (H162), amber (H80) and rose (H28) with +/-40 degrees each, which leaves
   * exactly one open arc, and that arc is the violet one. WP5 amends the
   * premise instead: the state tokens keep their meanings, but a series may
   * SHARE a warm hue family with a state token as long as it never shares a
   * ROLE with one. R3.4 is what makes that safe — a series token draws a
   * SWATCH beside --ink-body text and never colours text — so "amber" in a
   * chart legend and "amber" on a residual cannot be confused, because one is
   * a 10px square and the other is a number. R3.13 is the other half: a
   * genre-wide colour is not a verdict, a varying one is.
   *
   * THE SIX ARE NOW ONE PER CONVENTIONAL ARC, which is why they fit at a full
   * 40-degree gate instead of alternating lightness to squeeze into 145
   * degrees:
   *   1  steel blue   H 249.5   the default first series
   *   2  gold         H  82.0
   *   3  brick red    H  30.3
   *   4  slate        H 251.0   light, low-chroma — the quiet one
   *   5  olive        H 128.7
   *   6  dark teal    H 206.8
   * Every chromatic pair clears 40 degrees outright; the tightest is 1/6 at
   * 43.5 and 4/6 at 44.2. No pair depends on the ΔL OR-term, which is the
   * structural improvement over the old ramp — that one had NO slack anywhere
   * (every adjacent pair sat at ΔL 0.159-0.181 against a 0.15 floor), so a
   * single token could not move without breaking a neighbour.
   *
   * TWO SLOTS ARE NOT THE SPEC'S OWN HEX, AND BOTH MOVES ARE MEASURED:
   *
   * --series-6 was to be #1F6F72 (H 198.7). That is 36.3 degrees from
   *   --accent-measured-ink (H 162.4) — under the gate, and they co-occur in
   *   the drawer, where the stock-draw swatch sits beside a MEASURED badge.
   *   Moved along the teal arc to H 206.8, which clears --accent-measured-ink
   *   by 44.4 and --series-1 by 42.7. The window is [202.4, 209.5] and it is
   *   only 7 degrees wide, so this token has no room and should not be nudged.
   *   The green did not move: it is a bound state token and the ramp is not.
   *
   * --series-4 was to be #6B7280 (slate grey, L 0.551, C 0.023). At C 0.023 it
   *   is a NEUTRAL, so R3.7's neutral clause gates it on ΔL/ΔC against the ink
   *   ramp — and L 0.551 is 0.006 from --ink-faint (0.545), which is also
   *   --series-other. "Category 4" and "Other" would have been the same grey in
   *   one legend. The neutral ramp is full: ink 0.216 / modelled 0.325 / body
   *   0.435 / faint 0.545 at ΔL 0.11 steps leaves no fifth slot below faint, so
   *   the only free space is ABOVE it. Two routes were measured. Stay neutral
   *   at C 0.023 and the token must reach L 0.645 for ΔL 0.100 — exactly the
   *   floor, and 3.05:1 on --surface-2, 0.05 over the non-text floor: two knife
   *   edges. Take chroma to 0.066 instead and it clears --ink-faint on ΔC
   *   (0.060, floor 0.05) rather than on ΔL, which frees the lightness; at
   *   L 0.639 it is 3.11:1 and ΔL 0.168 from --series-1 (floor 0.15). Route two
   *   was taken. It is a light slate BLUE rather than a slate grey, and that is
   *   the visible cost, recorded rather than glossed.
   *
   * WHAT THE MAP DOES WITH THESE is one alias and four deliberate divergences —
   * see the map block below. Contrast, light, on --surface-2 (3:1 non-text,
   * because R3.4 means none of these paints text): 6.28 / 3.24 / 6.00 / 3.11 /
   * 4.50 / 5.65. The two thin ones are slots 2 and 4, and both are above the
   * floor by design rather than by luck — a saturated gold cannot be lighter
   * than this on near-white paper and still clear 3:1. */
  --series-1: #2E5E8C;
  --series-2: #B0810F;
  --series-3: #A6392B;
  --series-4: #6E8FB3;
  --series-5: #5E7A3A;
  --series-6: #156C76;
  /* R3.9 DECLARED ALIAS of --ink-faint — the only one. Both mean
   * "de-emphasised", so one value for both is R1.2 working, not failing.
   * TRIP CONDITION: any --ink-faint text drawn INSIDE a plot area, which
   * would make a label over the Other band vanish. Re-check at WP3 when
   * in-plot labels and tooltips are built.
   *
   * The allowlist that permits this lives in tests/test_web_tokens.py, not
   * here, so that adding a second alias is a visible diff in a test rather
   * than a comment nobody reviews.
   *
   * WP2.5 tracked --ink-faint down (#A6A9A5 -> #6E716E) to hold the alias. The
   * move is an improvement here for a reason that has nothing to do with text:
   * as a chart FILL the old value was 2.20:1 against the panel, under R3.10's
   * 3:1 non-text floor as well. The Other band was failing two rules at once
   * and only one of them had been noticed. */
  --series-other: #6E716E;

  --shadow-panel: 0 1px 2px rgba(20, 25, 22, .05),
                  0 10px 28px -12px rgba(20, 25, 22, .12);

  /* -----------------------------------------------------------------------
   * MAP (WP1). The map key IS the co-occurrence list (R3.8): nine genres sit
   * in one legend, side by side, so they are mutually gated by construction —
   * derived from usage rather than authored, which is the whole point.
   *
   * AREA IDENTITY RIDES ON THE STROKE, NOT THE FILL. R5.3 caps area fill at
   * .18 alpha, so two washed fills over the same basemap differ by only ~.18x
   * their token difference — well under any separability floor no matter which
   * tokens they use. Overlapping area layers therefore carry a FULL-OPACITY
   * stroke that holds the identity, and the fill is a wash that only says
   * "something is here". Gating the fills against each other would be gating
   * the wrong thing.
   *
   * R3.7 RESULT, measured 2026-08-07 from rendered getComputedStyle in both
   * themes, on the STROKE colours: 36 pairs, 0 failures. Closest surviving
   * pairs are --map-region/--map-hub and --map-hub/--map-refinery-region, both
   * dE .085 on dH 56 (floor 40). The sequential ramp is checked separately and
   * differently — see --prod-* below.
   *
   * TWO THINGS THE MAP FORCED THAT R3.7 AS WRITTEN DOES NOT COVER, flagged
   * rather than quietly assumed:
   *
   * (1) NEUTRAL vs CHROMATIC. R3.7 gates on (dH >= 40 OR dL >= 0.15). Three
   *     genres here are achromatic ON PURPOSE — --map-boundary, --map-no-data
   *     and --map-selected are the reference frame, not data categories, and
   *     "not a colour" is the signal. Hue angle is meaningless below ~.02
   *     chroma (--map-boundary computes H 154 light / 174 dark from a chroma of
   *     .007 — noise), so a neutral-vs-chromatic pair has no dH to test and
   *     must separate on lightness or on CHROMA. --map-boundary in particular
   *     sits mid-ramp at L .551 and clears every chromatic genre on chroma
   *     alone; it cannot be moved to a lightness extreme because its L is
   *     pinned by the BASEMAP measurement below, which outranks this. The gate
   *     applied above is therefore (dC >= 0.05 OR dL >= 0.15) AND dE >= 0.06
   *     when either member is under .02 chroma. That is an extension of R3.7,
   *     not a reading of it.
   *
   * (2) A SEQUENTIAL RAMP IS NOT A SET OF CATEGORICAL PEERS. Gating --prod-1..5
   *     against each other on (dH OR dL >= 0.15) is both unsatisfiable — five
   *     steps at dL .15 need a lightness range of .60, which no ramp has — and
   *     backwards, since adjacent steps are SUPPOSED to read as adjacent. The
   *     ramp contributes one member to the matrix above (--prod-5, the
   *     choropleth's stroke for every bin) and its internal separation is
   *     checked as a ramp: monotonic L, and adjacent-step dE measured AS
   *     COMPOSITED at the .75 fill opacity the reader actually sees —
   *     .071/.078/.087/.092 light, .076/.078/.084/.097 dark, all clear of .06.
   *
   * ONE MEASURED NON-PROBLEM, recorded so it is not re-discovered: Ontario's
   * no-data wash and a lowest-bin producing state composite to dE .052 in
   * light, under the .06 floor. It is not a defect and no token moved for it.
   * Both are FILLS, which the paragraph above excludes from gating; the two
   * areas are told apart by their full-opacity strokes (--map-no-data L .731 vs
   * --prod-5 L .487, dL .244). Raising the wash's alpha makes it WORSE, not
   * better — grey and pale mint converge as both darken (dE .052 -> .033 by
   * alpha .26), so alpha is the wrong lever and reaching for it is the trap.
   * The six lowest-bin states are also Virginia/Missouri/Tennessee/Idaho/
   * Arizona/Nevada — none borders Ontario.
   *
   * Genre assignment, per R5.2/R5.3:
   *   national outline    --map-boundary (= --ink-body), hollow, weight 3
   *   PADD boundaries     --map-boundary, weight 1.2, DASHED  <- see note
   *                       (since UI-01 WP4.1: solid 1px; UI-02's fade to 0.6
   *                       opacity was undone by UI-04, 2026-10-28)
   *   provinces & states  --map-region   (= --series-1)
   *   producing basins    --map-basin    (= --series-6, the headline layer)
   *   state production    --prod-1..5    (sequential, below)
   *   pipelines, modelled --map-pipe     (= --series-2), 3px since 2026-10-28
   *   pipelines, no edge  --map-no-data
   *   market hubs         --map-hub      (= --series-3, per R5.2)
   *   refineries          --map-no-data; --map-refinery dots since UI-02
   *                       (2026-10-28, see that token below)
   *   CER refining rgns   --map-refinery-region (= --series-5)
   * That leaves --series-4 unspent, for charts.
   *
   * NO EVIDENCE TOKEN APPEARS ON THIS MAP, and that is the substantive change
   * from the first draft, which had pipelines as --accent-measured solid /
   * --accent-modelled dashed. A pipeline's evidence grade is a property of
   * (edge, MONTH) — the same line is T1_MEASURED in one month and allocated in
   * the next — so ONE static stroke colour cannot carry it, and painting the
   * whole layer green would assert "measured" for every month including the
   * ones that are not. The dashed line likewise means "no network.yaml edge",
   * i.e. we hold no number at all, which is ABSENCE and not derivation.
   * Evidence grade belongs on the panel row and the badge, where it can vary
   * with the period being shown. The map's colours name GENRES.
   *
   * --map-no-data is deliberately ONE token used three times, not three
   * synonyms: on this map it means "geometry we draw but hold no flow number
   * for" — a pipeline with no matched edge, a refinery (EIA-820 is a capacity
   * snapshot, there is no per-plant monthly throughput anywhere free), and
   * Ontario / New Brunswick, which no longer produce. Naming those separately
   * would be three names for one meaning, which is R1.2 backwards.
   * Two uses since 2026-10-28: the refinery left it for its own token,
   * --map-refinery (Thomas wants the plants to lead the map). The meaning
   * above still holds for the other two, and the refinery panel still says
   * it is capacity only.
   *
   * NOTE ON PADDs, which IS a visible change and not just a re-skin: the five
   * PADDs were five saturated fills. R5.2 assigns them to boundary strokes,
   * which frees five corridor slots — without that the nine genres do not fit
   * in six. The regions stay fully legible (the panel still names the PADD you
   * clicked); what is lost is telling P2 from P4 by hue alone at a glance.
   * Called out rather than slipped in, because the map key's five-colour chip
   * becomes one dashed-line chip. PADD and national share --map-boundary
   * because they ARE one family; weight and dash separate them, and the key
   * draws them as two different chip shapes for the same reason.
   *
   * WHY --map-boundary IS --ink-body AND NOT --line-strong. It was
   * --line-strong for one draft, on the reasoning that a boundary is a line
   * and --line-strong is the strong line. That reasoning is wrong because the
   * neutral ramp is tuned against --surface, and this stroke is not on
   * --surface: it is on a CARTO basemap. Measured on 2026-08-07 by fetching
   * light_all/4/3/6.png and dark_all/4/3/6.png and histogramming luminance
   * (fetch + createImageBitmap, because the <img> the tile layer creates has
   * no crossorigin attribute and taints the canvas):
   *
   *              ground L   CARTO's OWN admin lines
   *   Positron      250              217
   *   Dark Matter     9               38
   *
   * --line-strong lands on L 215.0 light / 54.5 dark. Against the ground that
   * is a survivable dL of 35 / 45 — which is why "faint but fine" was the
   * tempting read. The number that actually matters is the other column:
   * dL 2 from Positron's own state boundaries. Not faint, CAMOUFLAGED. A
   * PADD line the reader cannot tell from a state line is worse than a PADD
   * line they cannot see, because they will read the basemap's borders as
   * ours and count six PADDs. --ink-body (L 113.9 / 160.9) clears CARTO's ink
   * by dL 103 / 123 while staying dL 54 / 58 from --map-no-data, so the
   * boundary family and the no-data family remain separable from each other
   * too. Any future re-tune of this token must be measured against the
   * BASEMAP, not against --surface.
   * --------------------------------------------------------------------- */
  --map-boundary: var(--ink-body);
  /* WP5 re-pointed this alias from --series-1 to --series-6, i.e. from the old
   * ramp's teal to the new ramp's teal. Same genre, same reasoning, one hue
   * family: a province is a producing area drawn quieter than a named play. It
   * is the ONE map genre WP5's hex list does not name, and the teal arc is the
   * one conventional arc the five named genres leave open — brick 28, gold 74,
   * olive 123, navy 249 and the neutrals take everything else. */
  --map-region: var(--series-6);
  --map-no-data: var(--ink-faint);
  /* THE MAP FAMILY, RE-REGISTERED (WP5). Five genres, five conventional hues,
   * and they are NOT aliases of the chart ramp even where they share its hue.
   * That divergence is WP3.2's finding generalised: a chart series is measured
   * against --surface-2 and a map stroke against CARTO Positron's #FAFAFA, so
   * one value cannot serve both grounds, and here the map's ground is the
   * harder one at 1.5-3px. Measured against #FAFAFA, all five clear the 3:1
   * non-text floor: pipe 3.21, basin 5.66, hub 3.20, refining 5.10,
   * region (= --series-6) 5.85. (UI-05, 2026-10-28: pipe and hub darkened to
   * 4.09 and 4.12 so the PADDs could take a real wash — see the PADD fill
   * note below. The other three are unchanged.)
   *
   * THE PIPELINE IS A GOLD AND CANNOT BE A YELLOW ON THIS GROUND. The original
   * map's pipeline was #F2C94C, a plain yellow, on a dark basemap where it is
   * 12.55:1. On Positron's #FAFAFA the SAME colour is 1.52:1 — a 1.5px hairline
   * with almost nothing under it. Dark theme has that yellow back (see the dark
   * block); light cannot have it, and this value is the answer to "how yellow
   * can light get here": #B58400, L 0.644 C 0.133 H 82.3, 3.21:1 on Positron.
   * Tom asked for the original yellow on 2026-08-08 and chose this split
   * knowing the cost, so the standing instruction is narrow: it may be tuned
   * WITHIN the floor, never through it. The temptation is always the swatch —
   * 10px on a panel, where a deep gold looks muddy — but the floor is about the
   * 1.5px stroke on the basemap, which is the thing a reader actually traces.
   * (#A8710C, the 4.00:1 version this replaces, is where to go back to if the
   * hairline ever proves too thin in print.)
   * TWO CONSTRAINTS BIND IT, NOT ONE, and the second is the one that is easy to
   * forget while staring at a contrast checker. The brightest gold in this
   * family that clears 3:1 at all is #B48608 (L 0.647, H 83.8, 3.17:1) — it was
   * committed, and test_token_pairs failed it: ΔH to --map-refinery-region's
   * olive (H 123.5) is 39.6, four tenths of a degree under R3.7's gate, with
   * ΔL 0.126 too small to rescue it on the other clause. So the light pipeline
   * is pinned from BELOW by the basemap floor and from ABOVE-IN-HUE by the
   * olive, and the usable window is roughly H 68-83 at L ~0.645. #B58400 sits
   * at the yellow end of it with 1.2 degrees of margin and slightly MORE chroma
   * than the value it replaces. Anyone re-tuning this toward yellow again will
   * hit the olive before they hit the floor; the check that catches it is
   * `python3 tests/test_token_pairs.py`, not a contrast tool.
   * DARKER SINCE UI-05 (Thomas, 2026-10-28: approved darker pipelines for
   * stronger light-mode PADD colours). #B58400 sat on the 3:1 floor over the
   * bare tile, which left the PADD wash no luminance to spend (the fill note
   * below). It is now #A17204: L 0.585 C 0.120 H 79.6, 4.09:1 on Positron,
   * the same gold hue a step darker rather than a browner one — H moved 2.7
   * degrees and chroma held at 0.12, which is what keeps it amber and not
   * mud. Both old constraints still hold with room: ΔH 43.9 to the olive, and
   * 0.153 in L over --map-padd-2, which moved down with it (see the PADDs).
   * Its binding pair is now the wash: 3.03:1 over a hovered PADD 1, the
   * lowest stroke-over-ground figure in the light theme.
   *
   * THE HUB IS THE PROJECT'S OWN ORIGINAL BLUE, #4A90D9, and it is ONE VALUE
   * FOR BOTH THEMES — the only token here that manages it. It was this map's
   * PADD-1 fill before the token migration, it clears the basemap floor in both
   * directions (3.20:1 on Positron, 5.96:1 on Dark Matter) and it clears every
   * peer in the legend and both ink ramps. It replaces a navy (#1F4E79 light /
   * #6FA8DC dark) that was two values saying one thing and read as ink rather
   * than as a genre.
   * NOT THE ORIGINAL HUB COLOUR, and the difference matters: that was #E07AD9,
   * a magenta, at H 337 — inside R3.15's banned 265-355 arc, which is the one
   * rule in this file that is a numeric ban rather than a preference. Tom asked
   * for "the old blue" on 2026-08-08 with the magenta named and excluded. The
   * SHAPE the magenta was chosen for (the rotated square, kept from the
   * 2026-07-31 merge) is untouched — see map.js. What moved is one hue.
   * NO LONGER ONE VALUE FOR BOTH THEMES (UI-05, 2026-10-28). Light needed
   * the same luminance the pipeline gave up, so light is now #197CD5: the
   * same hue (H 251.6 against 251.4), darker and a little more saturated
   * (L 0.580 C 0.161), 4.12:1 on Positron — still the blue diamond, read
   * against a wash instead of bare tile. Dark keeps #4A90D9, where a wash
   * adds luminance and nothing had to move. Its tight pair is --map-padd-5
   * at ΔL 0.151 (ΔH 5): darker than that and the West Coast PADD would have
   * to move too.
   *
   * SEPARABILITY, all five plus the three neutrals, in one legend: every
   * chromatic pair clears 40 degrees of hue outright. Tightest is
   * --map-pipe/--map-refinery-region at 41.2 — see the pipeline note above for
   * why that one is the binding constraint and not the contrast floor. The
   * neutrals (--map-boundary, --map-no-data, --map-selected) carry no hue by
   * design and separate on chroma, which R3.7's neutral clause covers.
   *
   * WP5 REFUSED A SIXTH MAP GENRE AND THAT REFUSAL STILL STANDS, but it is
   * narrower than it was written, so read it precisely before citing it. What
   * was asked for was --map-padd #C0762B, ONE tan-orange dashed stroke, and it
   * does not fit — measured on the real R3.7 predicate rather than judged:
   *   - as given, H 61.6 / L 0.633. Against --map-pipe (H 74.3, L 0.592) that
   *     is ΔH 12.7 and ΔL 0.041 — fails both clauses.
   *   - it cannot go LIGHTER: it is already 3.43:1 on Positron and the 3:1
   *     floor is 0.43 away.
   *   - it cannot go DARKER far enough to clear the pipeline (needs L <= 0.442)
   *     without then colliding with --map-basin (H 28.3, L 0.519): ΔH 33.3,
   *     ΔL 0.077. Pushing past BOTH needs L <= 0.369, which lands ΔL 0.150 from
   *     the basin — exactly the floor, a knife edge, and by then it is a dark
   *     brown rather than a tan.
   * The generalisation the note THEN drew from that was too strong, and it is
   * corrected below: it said R3.15's red/orange/gold arcs "hold at most TWO
   * mutually-separable hues", which is true only if every new colour must sit
   * in the SAME LIGHTNESS BAND as the five genres. R3.7 does not say that. Its
   * first clause is (ΔH >= 40 OR ΔL >= 0.15), and the OR is the whole point —
   * see --map-padd-1..5 below, which take the second branch. */
  --map-pipe: #A17204;
  --map-basin: #A8443A;
  --map-hub: #197CD5;
  --map-refinery-region: #5F7233;
  /* REFINERY PLANTS, a genre of their own since UI-02 (2026-10-28; Thomas:
   * "We want to highlight pipelines and refineries"). They were --map-no-data
   * grey, then for one commit --ink, which at 13-36px made the Gulf Coast a
   * black mass; HQ ruled small coloured dots in a dedicated token.
   *
   * WHY A GREEN, AND WHY THIS ONE. Searched rather than picked: every OKLCH
   * colour at 0.01 L / 0.01 C / 1-degree steps was run through this file's
   * own gates (tests/test_token_pairs.py) — R3.7 against every map-key token
   * and the ink ramp, R3.10's 3:1 on Positron AND over every PADD ground at
   * both fills, R3.15's permitted arcs. With the five PADDs filling the dark
   * band and the five genres the mid band, the ONLY chromatic slot left on
   * this theme is H 163-167 at L 0.40-0.62; the rest of what passes is
   * near-black. #147859 is L 0.51 C 0.10 H 166, 5.21:1 on Positron, and far
   * from the gold pipelines (ΔE 0.21), which is the pair a reader most needs.
   *
   * WHAT IT MUST NOT BE READ AS. H 166 is within a few degrees of
   * --accent-measured (#1D9E6E, the drawer's "measured" colour) and of the
   * --prod-* ramp. No evidence token appears on this map (see above), and
   * this is not one: it names a genre, "a refinery", and claims nothing
   * about how a number was got. It is darker and less chromatic than
   * --accent-measured so the two are not the same swatch, but they are one
   * hue family; if that ever confuses a reader, the honest fixes are to give
   * the PADDs back a band or to drop a genre, not to nudge this. Over the
   * state-production choropleth (off by default; same hue) the dots lose
   * contrast like every other stroke there (see that layer's open item). */
  --map-refinery: #147859;

  /* THE FIVE PADDS, COLOURED AGAIN (2026-08-08, Tom: "bring back the coloured
   * padd regions that you can toggle too"). R5.2 had collapsed them into one
   * dashed --map-boundary and the note above concluded no PADD colour could
   * exist at all; the second half of that conclusion was wrong, for one
   * reason: it searched only the lightness band the five genres already
   * occupy. Every chromatic map genre sits between L 0.488 and L 0.644 in this
   * theme. Drop BELOW that band and R3.7's ΔL >= 0.15 clause is satisfied
   * against all five at once, which frees the hue axis entirely — so these
   * five can sit at 30 / 69 / 141 / 212 / 257 degrees, directly on top of the
   * genre hues, and still separate. That is not a loophole. A dark brick PADD
   * outline and a mid brick basin fill are different lightnesses of the same
   * hue, which is exactly what R3.7's second clause is FOR.
   *
   * Measured, light: L 0.279 / 0.432 / 0.358 / 0.334 / 0.428, every one
   * 7.88:1 or better on Positron (P1 14.62, P2 7.88, P3 10.10, P4 11.29,
   * P5 7.90). Tightest PADD pair is P1/P2: ΔL 0.153 on ΔH 38.6, ΔE 0.168.
   * P1 and P2 are UI-05's (2026-10-28): P2 went from #885400 (L 0.493) to
   * #714502 to stay 0.15 under the darker pipeline, and P1 from #641810
   * (L 0.337) to #520401 to stay 0.15 under P2. Same hues; P3-P5 unchanged.
   *
   * THE FILL WAS A ROUNDING ERROR ON THIS THEME UNTIL UI-05, AND WHY IS NOT A
   * STYLE CHOICE. CARTO Positron is #FAFAFA, relative luminance 0.961, and any
   * colour laid over it can only SUBTRACT luminance, which every stroke drawn
   * over a PADD then has to clear 3:1 against (R3.10, asserted by
   * tests/test_token_pairs.py). With --map-hub (3.20:1) and --map-pipe
   * (3.21:1) both sitting on the 3:1 floor over the bare tile, the whole
   * budget was a 4% luminance drop: the wash stopped at 0.03 (hub 3.03:1),
   * 0.04 broke R3.10, and the colour was carried by the outline alone. This
   * note named the two tills: darken the hub and the pipe, or give those
   * strokes a casing (still not built — see STATE.md).
   * THE FIRST TILL WAS TAKEN (Thomas, 2026-10-28: approved darker pipelines
   * for stronger light-mode PADD colours). With --map-pipe at #A17204 (4.09:1
   * on the tile) and --map-hub at #197CD5 (4.12:1), the wash is 0.12 — four
   * times what it was, roughly the visual weight dark's 0.20 has on Dark
   * Matter — and every stroke still clears every PADD ground: worst at 0.12
   * is --map-pipe over PADD 1 at 3.18:1 (hub 3.20, no-data 3.68, the rest
   * 3.97 and up). The search that found these ran every candidate through
   * this file's own gates (R3.7 in the map key, R3.10 on the tile and over
   * all five grounds at both alphas, R3.15); 0.12 is the top of the range
   * where the pipeline keeps its chroma. Going further means a browner pipe
   * or the casing.
   *
   * WHY THEY ARE NOT THE ORIGINAL FIVE. The pre-migration PADD_COLORS were
   * P1 #4a90d9, P2 #8e6ac2, P3 #d97a4a, P4 #5cb87a, P5 #d4b95a. P2 is a purple
   * at H 303, inside R3.15's banned 265-355 arc, and P1's blue is now
   * --map-hub — reusing it would put the same colour on a region and on a
   * point genre. What is preserved is the THING the original palette got
   * right: five distinct hues walking warm-to-cool as the PADD number rises,
   * which is east-to-west on the ground and reads as an ordering a US crude
   * reader already has. */
  --map-padd-1: #520401;
  --map-padd-2: #714502;
  --map-padd-3: #16490D;
  --map-padd-4: #003E48;
  --map-padd-5: #194E92;
  /* 0.12 (UI-05, 2026-10-28; see the fill note above). History: 0.03 on main,
   * faded to 0.015 by UI-02 to make the PADDs background, back to 0.03 by
   * UI-04 (Thomas: "I like the original colours we had for each pad and
   * province it made it nicer to look at"), and raised here once the pipe
   * and hub were darkened to pay for it. */
  --map-padd-fill: 0.12;
  /* Hover and selection. 0.144 is --map-padd-fill x 1.2 (Tom, 2026-08-08:
   * "make the padd colours 20% more colour when hovered over"), and it is
   * where this theme stops: the worst stroke-over-ground pair, --map-pipe
   * over hovered PADD 1, is 3.03:1 (hub 3.06). So the literal request and
   * the R3.10 ceiling are again about the same number, as they were at
   * 0.036. SELECTION SHARES IT rather than getting a third value, because
   * there is no third value to give — on light the fill cannot express hover
   * AND selection, so selection is carried by the stroke (weight 3.4,
   * opacity 1, brought to front) and hover by a lighter one (2.4). See
   * mapStyle() in map.js. */
  --map-padd-fill-hover: 0.144;

  /* Selection carries NO claim about the data, so it gets no evidence colour.
   * Max contrast against both basemaps plus the weight bump does the work. */
  --map-selected: var(--ink);

  /* Sequential production ramp, low -> high. Anchored on the MEASURED hue
   * (H 162) because production IS measured data; magnitude is encoded on
   * LIGHTNESS, which is the correct channel for a sequential scale and is why
   * this does not "collide" with --accent-measured — it IS that hue, and a
   * reader is never asked to distinguish "measured" from "measured".
   * Five decade bins, not a continuous scale (see map.js PRODUCTION_BINS).
   * Adjacent-step dE .094 / .103 / .111 / .120 as TOKENS — but the reader never
   * sees the tokens, they see them at .75 fill opacity over the basemap, which
   * scales the gaps to .071 / .078 / .087 / .092. Still clear of .06, and that
   * is the number that had to clear it. Re-measure composited, not raw, if
   * these ever move. */
  --prod-1: #C6EAD7;
  --prod-2: #9AD1B5;
  --prod-3: #68B691;
  --prod-4: #2F976C;
  --prod-5: #007247;

  /* Basin fills are alpha-composited over the basemap, so they carry their own
   * alpha (R5.3 caps fill at .18) rather than relying on a fill-opacity set at
   * the call site. */
  /* .14 -> .12 (WP5). The brick red is a stronger fill than the magenta it
   * replaces, and R5.3's cap is a ceiling rather than a target. */
  --map-basin-alpha: .12;
  --map-halo: rgba(255, 255, 255, .72);
}

/* -------------------------------------------------------------------------
 * DARK — instrument
 * ------------------------------------------------------------------------- */
html[data-theme="dark"] {
  --bg: #0C0F11;
  --surface: #12161A;
  --surface-2: #1A2026;

  /* WP2.5 — same method as light, mirrored: --ink pinned, --ink-faint lifted
   * until it clears 4.5:1 on --surface-2 (2.92 -> 4.59:1), the remaining span
   * L 0.621..0.935 divided into three steps of 0.105.
   *
   * --surface-2 is the worst case in BOTH themes, for opposite reasons. In
   * light it is the lightest panel and the foreground is dark; in dark it is
   * still the lightest panel and the foreground is light, so it is the surface
   * that closes the gap from the other side. One rule, both themes, no
   * special-casing.
   *
   *   --ink-faint       #5E6A66 -> #7E8A86   2.92 -> 4.59:1
   *   --ink-body        #9AA3A0 -> #A1A8A8   6.35 -> 6.79:1
   *   --accent-modelled #C0C5C7 -> #C2C9C9   9.43 -> 9.77:1
   *
   * Dark moved far less than light did, and that is not luck: a light
   * foreground on a dark ground starts with more contrast to spend, which is
   * why the WP2 gate found only one dark failure against four in light. */
  --ink: #E8EAE8;
  --ink-body: #A1A8A8;
  --ink-faint: #7E8A86;

  --line: #232A2F;
  --line-strong: #2F383E;

  --pine: #0F3A2E;
  /* Light ink here too — see the light block. This is --ink's value, not
   * --bg's: 10.44:1 on #0F3A2E against the 1.52:1 --bg was giving. */
  --on-pine: #E8EAE8;

  --accent-measured: #2FBF87;
  --accent-measured-ink: #2FBF87;
  /* First step below --ink by CONTRAST, matching light's ordering. WP2.5
   * re-spaced the whole dark ramp at ΔL 0.105 per step; that is now the
   * tightest margin in the palette (it was 0.113) and it is the number to
   * check first if a future edit makes a neutral pair fail. It clears R3.7's
   * neutral clause, which asks ΔL >= 0.10 — the clause exists precisely
   * because a ramp compressed by a contrast floor has no hue to separate on. */
  --accent-modelled: #C2C9C9;
  --warn: #D8A44A;
  /* Same move as light, mirrored: the hue arc is held (26.5 -> 26.9) and the
   * chroma lifted (.112 -> .130) so it reads as red rather than as dusty pink,
   * with lightness raised to keep the TEXT floor on this ground (6.85:1 on
   * --surface-2). ΔH to --warn is 51.7 here. */
  --data-neg: #F38A7F;
  /* Re-stated so the two blocks declare the same token set, not because the
   * value differs — var(--ink) resolves against THIS block's --ink. */
  --data-pos: var(--ink);

  /* R3.5 — same six hues as light (250/82/30/252/129/206), lightness lifted for
   * the dark ground and chroma nudged up slightly to survive it. One hue list,
   * two tunings.
   *
   * THE 1/4 PAIR IS THE ONE TO WATCH IN BOTH THEMES. Steel blue and slate are
   * 2.5 degrees apart, so they are the only pair in the ramp living on R3.7's
   * ΔL OR-term rather than on hue, at ΔL 0.170 here and 0.168 in light against
   * a 0.15 floor. Everything else clears 40 degrees outright. If a future edit
   * has to move one of the six, move 2, 3 or 5 — they have the slack.
   *
   * AND IT IS NOT FIXABLE BY REARRANGING, WHICH IS WHY IT IS STATED AS A
   * PROPERTY RATHER THAN AS A TODO. R3.15's permitted hues are the contiguous
   * arc 20-265 degrees. --accent-measured-ink sits at 162.4 inside it and owes
   * every series slot the same 40-degree clearance, which cuts the arc into
   * [20, 122] and [202, 265] — 102 degrees and 63 degrees. At a 40-degree gate
   * those hold three slots and two, so FIVE hue-separable series is the
   * ceiling, not six, and the sixth slot has to be a lightness step wherever it
   * is put. (Reserve --warn's 81.0 as well and the ceiling drops to four; that
   * is exactly why --series-2 at 82.0 cannot be used on any page that also
   * carries an amber, which t1_explorer.css and t1_deep.css both record from
   * the other end.) Widening the arc means re-opening the 265-355 ban, which is
   * R3.15's whole point; narrowing the gate means R3.7. So the honest options
   * are the lightness step or a smaller ramp — and a smaller ramp is the one
   * this project keeps choosing anyway, via --series-other.
   *
   * Contrast on --surface-2 (3:1 non-text): 4.89 / 7.56 / 4.37 / 9.10 / 5.85 /
   * 5.45. Dark has more room than light throughout, for the same reason WP2.5
   * found: a light foreground on a dark ground starts with more to spend. */
  --series-1: #5D90C4;
  --series-2: #DAA843;
  --series-3: #D4604F;
  --series-4: #A2C4EB;
  --series-5: #85A460;
  --series-6: #48A1AC;
  /* Follows dark --ink-faint, per the R3.9 alias. See the light block. */
  --series-other: #7E8A86;

  --shadow-panel: none;

  /* MAP — same genre assignment as light, re-stated so both blocks declare the
   * same token set. The var() indirections resolve against THIS block.
   *
   * WP5 gave the four named genres their own dark values rather than aliasing
   * them to the ramp, matching light. The reason is the same on both grounds
   * and is not about contrast here — Dark Matter is #090909 and everything
   * clears with room (pipe 12.55, basin 5.56, hub 5.96, refining 7.09,
   * region 6.61) — it is that a map stroke and a chart series are DIFFERENT
   * DECISIONS that happen to share a hue family. Aliasing them made every
   * future move to one of them a silent move to the other, which is how the old
   * light ramp ended up dragging three map strokes under the non-text floor.
   * --map-region stays an alias because it is the one genre with no value of
   * its own; see the light block. */
  --map-boundary: var(--ink-body);
  --map-region: var(--series-6);
  --map-pipe: #F2C94C;
  --map-basin: #D2685A;
  --map-hub: #4A90D9;
  --map-refinery-region: #8CA352;
  /* Refinery plants (UI-02): the same hue as light, H 166, lifted for the
   * dark ground — L 0.63 C 0.12, 6.05:1 on Dark Matter. Dark has many more
   * passing slots (cyans and blues too); the green is kept so the plants are
   * one colour family in both themes. Read the light block for the search
   * and for what this green must not be read as. */
  --map-refinery: #26A078;
  --map-no-data: var(--ink-faint);
  --map-selected: var(--ink);

  /* THE FIVE PADDS. Same construction as light — five hues sitting directly on
   * the genre hues and separating on ΔL instead — but the band moves the other
   * way: dark's chromatic genres run L 0.640 to 0.850, so the PADDs go BELOW at
   * L 0.480-0.582 rather than below L 0.488. Read the light block for why the
   * lightness clause is the load-bearing one and why these are not the original
   * five colours.
   *
   * The window here is narrow at BOTH ends and that is worth knowing before
   * re-tuning. The ceiling is ΔL >= 0.15 under --map-hub and --map-basin (both
   * L 0.640), so L <= 0.490; the floor is R3.10's 3:1 against Dark Matter's
   * #090909, which needs roughly L >= 0.475. P1, P3 and P5 all live in that
   * 0.015-wide band and clear the basemap by 3.05, 3.21 and 3.08 — real
   * margin, but not much. P2 escapes upward because --map-pipe's yellow is at
   * L 0.850, leaving it room to reach 0.582 and 4.56:1. P1's chroma (0.081)
   * and P4's (0.087) are lower than the other three not by preference but
   * because the sRGB gamut runs out at that lightness in those hues.
   *
   * --map-padd-fill is 0.20 here against light's 0.12 (0.03 until UI-05),
   * and the asymmetry is the whole difference between the two basemaps:
   * adding colour to #090909 ADDS luminance, so a wash helps the strokes
   * above it instead of eating them. Measured at 0.20, the worst
   * stroke-over-PADD-ground pair on this theme is --map-no-data at 4.61:1 —
   * against light's 3.18:1 at 0.12, which light paid for with a darker pipe
   * and hub (see the light block). Nothing here moved in UI-05.
   *
   * UI-02 faded this to 0.06 / 0.10 (the PADDs as background); UI-04 put the
   * main-branch 0.20 / 0.24 back (Thomas, 2026-10-28: "I like the original
   * colours we had for each pad and province it made it nicer to look at"). */
  --map-padd-1: #884E44;
  --map-padd-2: #A86C00;
  --map-padd-3: #306E24;
  --map-padd-4: #007280;
  --map-padd-5: #2C5EA4;
  --map-padd-fill: 0.20;
  /* 0.24 = 0.20 x 1.2, the same "20% more colour on hover" as light, and here
   * the fill is the medium so the request lands where it was aimed.
   *
   * IT IS ALSO A FIX. The hover alpha used to be computed in map.js as
   * fill x 2.2 = 0.44, which was never measured against anything: at 0.44 the
   * worst stroke-over-hovered-PADD pair is --map-no-data over --map-padd-2 at
   * 2.31:1, well under R3.10's 3:1 non-text floor, so hovering a PADD used to
   * black out the unmatched pipelines crossing it. 0.24 measures 3.17:1; 0.30
   * would be 2.90 and already broken. A hover state is transient but it is
   * still a state a reader looks AT, which is exactly when they are trying to
   * read what is inside the region. Asserted by tests/test_token_pairs.py. */
  --map-padd-fill-hover: 0.24;

  /* Production ramp, same measured hue (H 162), lightness inverted for the
   * dark ground: low production is now the DARK end, so "more" still reads as
   * "brighter" against the ground it sits on rather than as "darker ink".
   * Adjacent-step dE: .098 / .100 / .107 / .122. */
  --prod-1: #163527;
  --prod-2: #1C523A;
  --prod-3: #277152;
  --prod-4: #34946C;
  --prod-5: #4DBD8C;

  --map-basin-alpha: .18;
  --map-halo: rgba(0, 0, 0, .55);
}
