/* app.css — the shared component layer. WP2.
 * ===========================================================================
 * WHAT THIS FILE IS FOR, AND THE MISTAKE IT EXISTS TO STOP.
 *
 * Before this file there were EIGHT independent implementations of the same
 * six components. `.route-card` (style.css:1554), `.map-key` (style.css),
 * `.bhead` (hub_sd.css), `.card` (t1_deep.css), `.panel` (t1_explorer.css) and
 * three more are all "a titled box on a raised surface". Each was written by
 * copying the last one and nudging it, so the tree carries a 6px radius, a
 * 4px radius, a 3px radius and a 2px radius for the same object, and four
 * uppercase-label treatments at 9px/9.5px/10px/11px with four different
 * letter-spacings. None of that was a decision; it is what happens when there
 * is nowhere shared to put the decision.
 *
 * So: a component here is a class, not a copy. If a page needs a box with a
 * title, it uses `.panel`. If it needs the box to be 40px shorter, that is a
 * modifier in the page's own stylesheet on top of the class — never a second
 * `.whatever-card` with its own borders and its own radius.
 *
 * COLOURS. Not one literal. tokens.css is the only file allowed to hold one
 * (R1.1), and tests/test_web_tokens.py fails this file on any hex, rgb(),
 * hsl() or named colour outside a comment. A component that "just needs a
 * slightly darker line here" needs --line-strong, or it needs a new SEMANTIC
 * token in tokens.css — it does not need a literal.
 *
 * SIZES AND SPACE. Only --t-* (R2.2) and --sp-* (R4.1). The five ad-hoc sizes
 * this file replaced (9px, 9.5px, 10px, 11.5px, 12.5px) were all within a
 * point of each other and of --t-label/--t-data, which is the tell: they were
 * not distinctions, they were drift.
 *
 * LOAD ORDER: tokens.css, then app.css, then the page's own stylesheet. Every
 * selector here is a single class or an element inside one, so a page rule of
 * equal specificity that comes later wins — which is the intended escape
 * hatch. Nothing here uses !important and nothing here is an id.
 * ========================================================================= */


/* ---------------------------------------------------------------------------
 * Base.
 *
 * Deliberately thin. app.css is loaded by pages that already own their own
 * <body> layout (the map is absolutely positioned; hub_sd is a grid), so this
 * sets the inherited type and colour and stops.
 * ------------------------------------------------------------------------ */
.app {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--t-body);
  -webkit-font-smoothing: antialiased;
}

/* `hidden` MUST OUTRANK ANY COMPONENT THAT SETS `display`. WP4.4.
 *
 * `[hidden]{display:none}` lives in the user-agent stylesheet, and an author
 * rule beats the UA at ANY specificity — so the moment `.drawer{display:flex}`
 * three hundred lines below exists, every `<aside class="drawer" hidden>` in
 * the tree is on screen. This is not hypothetical: linking app.css into
 * t1_explorer.html put that page's closed source drawer over the right 560px
 * of the viewport, and the page still reported `drawer.hidden === true`. The
 * attribute was set, respected by every script that reads it, and silently
 * overruled by the cascade.
 *
 * The doubled attribute is a SPECIFICITY BUMP, not a typo: [hidden][hidden] is
 * (0,2,0), which clears any single class here. `!important` would also work
 * and is what most resets use, but this file's contract (see the header) is
 * that a page's own stylesheet can always override it by loading later, and an
 * !important would be the one rule no page could reach.
 *
 * A component below that genuinely wants to stay laid out while hidden must
 * say so with its own later, more specific rule — which is a visible decision
 * rather than an accident of rule order. */
[hidden][hidden] {
  display: none;
}

/* ---------------------------------------------------------------------------
 * WP4.4 — THE SHARED TOP BAR.
 *
 * Static markup copied into each page rather than a fetched partial: the repo
 * has no build step, and fetching chrome puts a round-trip in front of the
 * page's own navigation. The markup is six lines; what actually drifts between
 * copies is the theme snippet, and that one is pinned by test_web_tokens.py.
 *
 * THE CURRENT PAGE IS MARKED WITH aria-current="page", NOT WITH A CLASS, and
 * the styling hangs off the attribute. A class would let the two disagree —
 * one page highlighted for sighted readers and a different one announced to a
 * screen reader — and there is no reason to carry two sources for one fact.
 * On the published site model/site_build.py's mark_current_tab() writes it,
 * after the bar is injected; the partial itself carries none.
 *
 * TABS, NOT A SENTENCE (UI-06, Thomas 2026-10-29: "I want the header to be
 * cleaner for the tabs. They should be separated like Grid Status. Currently
 * ours looks like a sentence of words."). Each link is its own full-height
 * hit area with 12px either side and a hairline between it and the next, so
 * the row reads as separate tabs; the current one is --ink at 600 with a 2px
 * rule on the bar's bottom edge (Grid Status's tab, the same one the crude
 * sub-bar in css/shell.css uses), the others --ink-body, darkening on hover.
 * The height is written down, 45px, because the US map's layout is anchored
 * under it (site_build._flows_us: 45 + the sub-bar's 36), and a bar whose
 * height fell out of its content would move the map whenever a font did.
 *
 * The mark is --ink at full weight and the links are --ink-body: this is
 * chrome, and R3.4's rule that colour goes on the mark and not the label
 * applies to navigation too. Nothing here is themed by hue; the whole bar is
 * the ink ramp plus hairlines.
 * ------------------------------------------------------------------------ */
.topnav {
  display: flex;
  align-items: stretch;
  gap: 0;
  height: 45px;
  box-sizing: border-box;
  padding: 0 var(--sp-4);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-ui);
  font-size: var(--t-body);
}

.topnav a {
  color: var(--ink-body);
  text-decoration: none;
}

/* A tab: the whole height of the bar is the hit area, and the 2px rule that
   marks the current one sits on the bar's own bottom border. */
.topnav a:not(.topnav-mark) {
  position: relative;
  display: flex;
  align-items: center;
  padding: 2px var(--sp-3) 0;
  border-bottom: 2px solid transparent;
  font-weight: 500;
  white-space: nowrap;
  transition: color var(--dur-ui) var(--ease),
              border-color var(--dur-ui) var(--ease);
}

/* The hairline between two tabs: a third of the bar's height, centred, in
   --line-strong, so it parts the words without boxing them. */
.topnav a:not(.topnav-mark) + a::before {
  content: "";
  position: absolute;
  left: 0;
  top: 33%;
  bottom: 33%;
  width: 1px;
  background: var(--line-strong);
}

.topnav .topnav-mark {
  display: flex;
  align-items: center;
  margin-right: var(--sp-3);
  font-family: var(--font-display);
  font-size: var(--t-title);
  color: var(--ink);
}

.topnav a[href]:not(.topnav-mark):hover {
  color: var(--ink);
  border-bottom-color: var(--line-strong);
}

/* The rule under the current page is --ink, not an accent. R3.3 retired
   the brand orange and R3.1 keeps green for measured data; "you are here" is
   neither a warning nor a reading, so it is carried by weight and a rule. */
.topnav a[aria-current="page"],
.topnav a[aria-current="page"]:hover {
  color: var(--ink);
  font-weight: 600;
  border-bottom-color: var(--ink);
}

/* Keyboard focus: the app's ring (--ink, 2px), drawn INSIDE the tab so the
   phone bar's own scroll box cannot clip it. */
.topnav a:focus-visible,
.topnav .topnav-toggle:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
}

/* ANY item naming a page THIS HOST cannot serve. js/topnav.js sets the
   attribute and drops the href, so what is styled here is a LABEL and not a
   link — hence the default cursor rather than not-allowed, which would imply a
   refused action where there is now no action at all.

   --ink-faint is the same muted the .eyebrow takes: it recedes from the live
   items without vanishing, and not vanishing is the whole point — the item is
   still there so a reader learns the page EXISTS and needs something running,
   rather than concluding the site is three pages wide.

   Struck through as well as dimmed, because colour alone would carry the
   difference only for readers who can see that contrast. Neither the :hover
   nor the [aria-current] rule above can apply once the href is gone, which is
   correct: there is nothing here to respond to.

   THE SELECTOR LOST ITS `.topnav` PREFIX, and that was not tidying. The script
   used to mark only inside the bar, so this rule only ever had bar items to
   match; when the script widened to the document, a footer link would have had
   its href removed and its title set while still being PAINTED as a live link
   — dimming is the half a reader actually sees, and the half that was scoped to
   the wrong thing. A rule and its selector have to agree about their subject,
   and the subject here is the attribute, never where it sits. */
a[data-needs-service] {
  color: var(--ink-faint);
  text-decoration: line-through;
  cursor: default;
}

.topnav-toggle { margin-left: auto; }

/* The theme switch is a CONTROL, not a tab (UI-06): a bordered button in the
   middle of the bar at the right, smaller than the tabs, so it never reads as
   a fourth page. Two classes, because .btn-ghost (one) clears the border;
   .btn:hover, later in this file, still darkens it. */
.topnav .topnav-toggle {
  align-self: center;
  padding: var(--sp-1) var(--sp-3);
  border-color: var(--line-strong);
  font-size: var(--t-data);
  font-weight: 500;
}

/* PHONE AND SMALL TABLET (820px and under, 2026-10-24). The older pages'
   bar (hub_sd.html, egress.html, the T-1 pages) carries the wordmark, six
   links and the theme button, about 530px of content in one flex row with no
   wrap and nothing to scroll it. At 375px it ran off the right edge: "T-1"
   broke onto two lines and Corrections, About and the theme button were cut
   off, both on the standalone hub page and inside the US map's hub sheet,
   which frames that page. The bar now scrolls INSIDE ITSELF rather than
   wrapping: a wrapped bar is two or three rows tall, and the hub sheet's
   height budget (hub_sd.css) and the map's anchors were written against one.
   Items keep their size and stay on one line, the 16px side padding above is
   the gutter, and the theme button sticks to the right edge so the one
   control that is not a link is always on screen. The published bar
   (partials/topbar.html, three links) fits at 375px, so on it these rules
   change nothing a reader can see. Same 820px as the US and Texas maps; a
   desktop window does not reach this.

   UI-06: the tabs drop to --t-data with 8px either side here, and the bar to
   42px, the height it measured at 375 before the tabs had boxes of their own
   (style.css budgets the US map's phone layout against 42 + 36). At that
   size the published bar's wordmark, three tabs and the button take about
   355px of the 375, so it still fits without scrolling. */
@media (max-width: 820px) {
  .topnav {
    height: 42px;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
  }

  .topnav > * {
    flex: 0 0 auto;
    white-space: nowrap;
  }

  .topnav a:not(.topnav-mark) {
    padding: 2px var(--sp-2) 0;
    font-size: var(--t-data);
  }

  .topnav .topnav-mark {
    margin-right: var(--sp-2);
  }

  /* Two shadows in the bar's own --surface: the first fills the 16px gutter
     to its right, so a link scrolling under the button does not show again
     beside it; the second fades the links into it from the left. Two
     classes, because .btn-ghost (below, same weight) clears the background. */
  .topnav .topnav-toggle {
    position: sticky;
    right: 0;
    padding: var(--sp-1) var(--sp-2);
    background: var(--surface);
    box-shadow: var(--sp-4) 0 0 var(--surface),
                calc(-1 * var(--sp-3)) 0 var(--sp-2) var(--surface);
  }
}

/* ---------------------------------------------------------------------------
 * R2.3 — THE LABEL TREATMENT, as a component.
 *
 * "--t-label is always uppercase, letter-spaced .12em, --font-data, colour
 * --ink-faint. That treatment IS the eyebrow/label component; never restyle it
 * locally." Six places restyled it locally; they now say class="eyebrow".
 *
 * MONO, not the UI face, and that is the part most likely to get "corrected"
 * back. A label sits directly above a column of --font-data numerals, and a
 * proportional label over tabular figures makes the column look misaligned
 * even when it is not. R2.1 gives the numerals to mono; the header of a
 * numeral column belongs to the same family for the same reason.
 *
 * --ink-faint is the DEFAULT, not the only option: a section title that has to
 * be found takes --ink-body, and so does a table header that names a series —
 * the series colour goes into the header's SWATCH, never into its letters
 * (R3.4, see --swatch below). The override is the colour and nothing else,
 * which is what "never restyle it" means: the size, the case, the tracking and
 * the family are not negotiable.
 * ------------------------------------------------------------------------ */
.eyebrow {
  font-family: var(--font-data);
  font-size: var(--t-label);
  font-weight: 500;
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* A unit inside a capitalised label keeps its own case (units.js unitSpan):
 * kb/d, never KB/D. */
.unit {
  text-transform: none;
}

/* The muted `Limits` link at the foot of /flows/us/ and /flows/texas/, to
 * /methods/#limits (UI-01 WP8). /balance/ carried it too until the page was
 * removed (Thomas, 2026-10-28), and with it went its .balance-tabs strip. */
.limits-link,
.limits-link a,
a.limits-link {
  font-size: var(--t-label);
  color: var(--ink-faint);
}

.eyebrow-strong {
  color: var(--ink-body);
}

/* ---------------------------------------------------------------------------
 * R2.1 — the numeral rule, as a class.
 *
 * "If it's a number the model produced, it's mono." tabular-nums is not
 * decoration: without it a column of proportional digits does not line up on
 * the decimal, and a reader comparing 1,240 against 984 down a column is
 * reading the LENGTH of the number as much as the value.
 * ------------------------------------------------------------------------ */
.num {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* R2.2 / R2.4 — the headline figure. --font-display is only ever allowed here
 * and at --t-display or larger, which is the whole of R2.4's constraint
 * expressed as a class rather than as a convention. If the WP2 gate rules the
 * serif out on dark, exactly one rule changes: the dark override at the bottom
 * of this file. Nothing else in the tree names --font-display. */
.figure {
  font-family: var(--font-display);
  font-size: var(--t-display);
  font-weight: 500;
  line-height: 1.05;
  letter-spacing: -0.01em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
 * PANEL — the titled box. R5.5's "--surface panels with --t-label headers".
 *
 * .panel            the box
 *   .panel-header   title bar; takes .eyebrow on its title element
 *   .panel-body     padded content
 *   .panel-foot     captions, counts, provenance
 *
 * R4.2 gives panels --r-panel and controls --r-ctl, and the distinction is
 * load-bearing rather than cosmetic: at a glance the radius is what says
 * "this is a region of the page" versus "this is a thing you press".
 * ------------------------------------------------------------------------ */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-panel);
  box-shadow: var(--shadow-panel);
  overflow: hidden;
}

.panel-header {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}

/* Pushes everything after it to the right edge. One class instead of the four
 * `.spacer` / `.right` / `margin-left:auto` variants the tree had. */
.panel-header .spacer,
.drawer-bar .spacer {
  margin-left: auto;
}

.panel-body {
  padding: var(--sp-3);
}

.panel-foot {
  padding: var(--sp-1) var(--sp-3);
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
  font-family: var(--font-data);
  font-size: var(--t-label);
}

/* A caption that must not change the box's height when its text is long —
 * read-counts and "n of m pipelines" strings, which vary per hub. */
.panel-foot-1line {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---------------------------------------------------------------------------
 * BADGE — the evidence grade. R3.1 + R3.2.
 *
 * The wording is the API's ("MEASURED", "DERIVED", a basis code) and this
 * NEVER re-labels it (R3.2). All four variants take their colour on
 * `currentColor` through the border, so a variant is one declaration.
 *
 * WHY MEASURED IS NOT GREEN. R3.1 assigns --accent-measured to observed data,
 * and the temptation is to paint every MEASURED badge with it. That inverts
 * the signal: most numbers on these pages ARE measured, so a green badge on
 * each of them is a page of green that says nothing, and the DERIVED badge —
 * the one a reader must not miss — has to compete with it. The exceptional
 * case takes the colour; the normal case sits in --ink-body. --accent-measured
 * is spent where measured-ness is genuinely in question against a neighbour:
 * the per-row basis tag in a table where the rows differ.
 * ------------------------------------------------------------------------ */
.badge {
  display: inline-block;
  font-family: var(--font-data);
  font-size: var(--t-label);
  font-weight: 500;
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  padding: 1px var(--sp-1);
  border: 1px solid currentColor;
  border-radius: var(--r-ctl);
  white-space: nowrap;
  color: var(--ink-body);
}

.badge-measured { color: var(--accent-measured-ink); }
.badge-modelled { color: var(--accent-modelled); }
.badge-warn     { color: var(--warn); }
.badge-neg      { color: var(--data-neg); }

/* A badge that is also a control (it opens the basis prose). It must not take
 * button chrome — a bordered pill that opens a drawer next to a real button
 * that closes the page is two different actions wearing one costume — so it
 * inherits the badge outright and only gains a cursor and a hover. */
.badge-button {
  background: none;
  font-family: var(--font-data);
  cursor: pointer;
}

.badge-button:hover {
  border-color: var(--ink);
}

/* ---------------------------------------------------------------------------
 * CONTROLS — R4.2 --r-ctl, R5.4's "styled controls".
 * ------------------------------------------------------------------------ */
.btn {
  background: var(--surface);
  color: var(--ink-body);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-ctl);
  padding: var(--sp-1) var(--sp-2);
  font-family: var(--font-ui);
  font-size: var(--t-data);
  cursor: pointer;
  transition: color var(--dur-ui) var(--ease),
              border-color var(--dur-ui) var(--ease);
}

.btn:hover {
  color: var(--ink);
  border-color: var(--ink-body);
}

/* No box, no border — for actions that sit inside a bar that already has a
 * background of its own. */
.btn-ghost {
  background: none;
  border-color: transparent;
}

.select {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-ctl);
  padding: 2px var(--sp-1);
  font-family: var(--font-data);
  font-size: var(--t-data);
}

.select:hover {
  border-color: var(--ink-body);
}

/* A pill link: "next level down" navigation, not a call to action. R4.2's
 * third radius, and its only user. */
.chip {
  display: inline-block;
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  padding: 2px var(--sp-2);
  font-family: var(--font-ui);
  font-size: var(--t-data);
  color: var(--ink-body);
  text-decoration: none;
  cursor: pointer;
}

.chip:hover {
  border-color: var(--ink-body);
  color: var(--ink);
}

/* Segmented toggle (Weekly | Monthly). One border around the group, dividers
 * between, so it reads as one control with two states rather than two
 * buttons that happen to touch. */
.seg {
  display: inline-flex;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-ctl);
  overflow: hidden;
}

.seg-btn {
  background: var(--surface);
  color: var(--ink-faint);
  border: none;
  padding: 2px var(--sp-2);
  font-family: var(--font-ui);
  font-size: var(--t-data);
  cursor: pointer;
}

.seg-btn + .seg-btn {
  border-left: 1px solid var(--line-strong);
}

.seg-btn:hover:not(.is-on) {
  color: var(--ink-body);
}

.seg-btn.is-on {
  background: var(--surface-2);
  color: var(--ink);
}

/* An inline link, in prose or a footer. R3.11: INTERACTION IS NOT A DATA
 * CHANNEL AND GETS NO HUE — so a link is --ink plus an underline, and hover
 * promotes the underline rather than introducing a colour.
 *
 * This rule replaces an earlier version of this comment which argued the
 * opposite: that --accent-measured-ink was safe here because "a link is
 * chrome, not a claim about a number, and no data ever renders in a link's
 * colour." The second half of that was false in this product. These links sit
 * in T-1 footers and drawer prose ALONGSIDE tables where --accent-measured is
 * the evidence grade meaning "parsed, page-1 checked, written" — so the green
 * appears twice on one screen meaning two unrelated things, and the reader has
 * no way to tell a navigation affordance from a verdict about a filing. The
 * underline is the affordance; it is unambiguous and it costs no hue. */
.link {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--line-strong);
}

.link:hover {
  text-decoration-color: var(--ink);
}

/* A control that reveals prose rather than acting on the page (Sources &
 * method). Understated on purpose: it is a way IN to provenance, not a call
 * to action, and it usually sits between a table and a chart where anything
 * loud breaks the read. */
.disclosure {
  display: block;
  margin: var(--sp-2) 0 0;
  padding: 0;
  background: none;
  border: none;
  border-bottom: 1px dotted var(--line-strong);
  color: var(--ink-faint);
  font-family: var(--font-data);
  font-size: var(--t-label);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  cursor: pointer;
}

.disclosure:hover {
  color: var(--ink-body);
  border-bottom-color: var(--ink-body);
}

/* ---------------------------------------------------------------------------
 * FOCUS. QA line: "keyboard focus visible on toggle, selects, hub chips,
 * Close." One rule for the whole app rather than per-component, because the
 * failure mode is a component that forgot — and :focus-visible (not :focus)
 * so a mouse click does not leave a ring behind.
 *
 * --ink, not an accent: a focus ring is not a claim about data, and using an
 * evidence colour for it would put --accent-measured on a control at the exact
 * moment the reader is looking straight at it.
 * ------------------------------------------------------------------------ */
.app :focus-visible,
.panel :focus-visible,
.drawer :focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 1px;
}

/* Leaflet maps: a click focuses the map container, and in Chrome also the SVG
 * shape clicked (a region, a pipeline). The browser then draws its default
 * focus ring round the shape's bounding box, a blue rectangle that flashes
 * until the panel re-renders. A mouse click gets no ring here. Keyboard focus
 * (:focus-visible) keeps the ring above. */
.leaflet-container:focus:not(:focus-visible),
.leaflet-container .leaflet-interactive:focus:not(:focus-visible) {
  outline: none;
}

/* ---------------------------------------------------------------------------
 * DATA TABLE.
 *
 * The rules that are NOT style:
 *  - every numeral is mono and tabular (R2.1/R7.3)
 *  - negatives take --data-neg and a TRUE MINUS (R7.3) — the glyph is the
 *    renderer's job, the colour is this file's
 *  - an unfiled cell is an em dash in --ink-faint, never blank. A blank cell
 *    in a grid reads as a zero somebody forgot to type, and filed-zero versus
 *    never-filed is the single distinction these pages exist to keep
 *  - a column head that names a series carries that series' SWATCH so the
 *    table and the chart beneath it read as one picture. The swatch, not the
 *    letters — see --swatch below
 * ------------------------------------------------------------------------ */
.data-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-family: var(--font-data);
  font-size: var(--t-data);
}

.data-table th,
.data-table td {
  padding: var(--sp-1);
  text-align: right;
  white-space: nowrap;
}

.data-table th:first-child,
.data-table td:first-child {
  text-align: left;
}

/* Sticky over long histories. A row read at the far right of a fifteen-column
 * scroll is unidentifiable without its period, and a header 147 rows up is not
 * a header. */
.data-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line-strong);
  font-family: var(--font-data);
  font-size: var(--t-label);
  font-weight: 500;
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--ink-faint);
  text-align: center;
}

/* THE COLUMN SWATCH — R3.4, ratified 2026-08-07.
 *
 * A header that names a series draws an 0.62em block in the series colour and
 * sets its own letters in --ink-body. It does NOT paint the letters in the
 * series colour, and the second reason is the load-bearing one:
 *
 *  (i)  colour as the ONLY channel fails any reader who cannot resolve it, and
 *       doubling the hue onto the text buys no information the swatch has not
 *       already given;
 *  (ii) it classifies the series tokens as NON-TEXT under R3.10 permanently,
 *       so they owe 3:1 rather than 4.5:1 — which is what makes the six-hue
 *       alternating ramp solvable at all. Painting one label in --series-3
 *       anywhere on the site re-imposes 4.5:1 on the whole ramp and the ramp
 *       does not have that much room. This is not a preference.
 *
 * MECHANISM. A column rule sets two custom properties and nothing else:
 *
 *     th.th-imports { --swatch: var(--series-2); --swatch-on: inline-block; }
 *
 * --swatch-on is what makes the block appear; without it `display` falls back
 * to none and the ::before is inert, so this one rule can sit on every header
 * without every header sprouting a bullet. Both are needed because CSS has no
 * way to ask "is --swatch set" — the pair IS the question. Custom properties
 * inherit, which is why setting them on the <th> reaches its ::before.
 *
 * The swatch is sized in em so it tracks --t-label rather than pinning a pixel
 * count that a type-scale change would silently orphan. */
.data-table thead th::before {
  content: "";
  display: var(--swatch-on, none);
  width: 0.62em;
  height: 0.62em;
  margin-right: 0.45em;
  vertical-align: baseline;
  background: var(--swatch);
  border-radius: 1px;
}

.data-table tbody td {
  border-bottom: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.data-table tbody tr:hover td {
  background: var(--surface-2);
}

/* The row's identity column, frozen. Its own background is mandatory: a
 * transparent sticky cell paints the scrolled content through itself.
 *
 * WHICH IS WHY IT IS NOT ON THE MAP PANEL'S HISTORY TABLES, and that is a
 * decision rather than an oversight. Those tables stripe by year and by month
 * parity, and the stripe is painted on the <tr>; an opaque cell background
 * knocks a hole in it, so the identity column would sit at one flat tone
 * beside a striped body. The trade only pays where the table actually scrolls
 * sideways, and at --t-data the single-edge history fits its 386px wrap
 * outright (measured — see style.css). The T-1 tables, which are 15 columns
 * wide and unstriped, are where this belongs. */
.data-table .col-key {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface);
  text-align: left;
  color: var(--ink-body);
  border-right: 1px solid var(--line);
}

.data-table thead .col-key {
  z-index: 3;
  background: var(--surface-2);
}

.data-table tbody tr:hover .col-key {
  background: var(--surface-2);
}

/* Rules off a block of columns from its neighbours — on these pages, what was
 * filed to the left of the rule and arithmetic on those filings to the right.
 * A reader must never add a total back into the terms that produced it. */
.data-table .col-rule {
  border-left: 1px solid var(--line-strong);
}

/* Cell states. */
.data-table td.cell-total { color: var(--ink); font-weight: 600; }
.data-table td.cell-neg   { color: var(--data-neg); }
.data-table td.cell-warn  { color: var(--warn); }
.data-table td.cell-nd    { color: var(--ink-faint); }

/* A whole column whose subject has never been read in ANY period. It stays in
 * the table — dropping it would make a pipeline that files nothing identical
 * to one that is not connected at all — but it is dimmed to obviously inert. */
.data-table th.is-dead,
.data-table td.is-dead {
  color: var(--ink-faint);
  background: var(--surface-2);
}

/* The basis code, verbatim (R7.1). Never truncated, never title-cased away
 * from the code itself — FERC_ANNUAL_SPREAD is 18 characters and it stays 18
 * characters, which is why it gets --t-label rather than --t-data. */
.data-table td.cell-basis {
  color: var(--ink-faint);
  font-size: var(--t-label);
}

/* PRELIMINARY months (map.js PRELIM_TITLE): real, incomplete numbers. Marked
 * in words, not by colour alone, and in --ink-faint like the basis codes. */
.prelim-mark {
  color: var(--ink-faint);
  display: block;
  font-size: var(--t-label);
  font-style: italic;
  white-space: normal;
}
.cell-prelim {
  font-style: italic;
}
.prelim-note {
  color: var(--ink-faint);
  font-size: var(--t-label);
  margin: 6px 0 0;
}

/* ---------------------------------------------------------------------------
 * DRAWER — the docked sheet.
 *
 * The chrome only: a bar, an optional prose strip under it, and a body that
 * takes the rest. What goes in the body is the page's business.
 * ------------------------------------------------------------------------ */
.drawer {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border-left: 1px solid var(--line-strong);
}

.drawer-bar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-2);
  height: 30px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-data);
  font-size: var(--t-data);
  color: var(--ink-body);
  white-space: nowrap;
  overflow: hidden;
}

.drawer-title {
  color: var(--ink);
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* Prose under the bar: a reading measure and a real line-height, capped and
 * scrollable. An expanding strip that pushes the content down by an arbitrary
 * amount changes the layout depending on which subject you clicked. */
.drawer-note {
  flex: 0 0 auto;
  max-height: 92px;
  overflow: auto;
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  color: var(--ink-body);
  font-family: var(--font-data);
  font-size: var(--t-label);
  line-height: 1.6;
}

.drawer-note-src {
  display: block;
  margin-top: var(--sp-1);
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------------------
 * R7.2 — EMPTY AND ERROR STATES.
 *
 * "Say what happened and what to do, in --t-body, no sad-face glyphs, no
 * emoji." --ink-body and not --data-neg: an empty state is not an error in the
 * data, and painting it --data-neg would put the "negative value" colour on a
 * sentence about a server. That variant is for a state that IS a failure
 * of the number (a stale source, a failed freshness check).
 *
 * The COPY is not this file's to write, and CLAUDE.md is explicit that an
 * error message asserts a cause: no rule here may make a handler sound more
 * certain than it is.
 * ------------------------------------------------------------------------ */
.state {
  padding: var(--sp-3);
  font-family: var(--font-ui);
  font-size: var(--t-body);
  line-height: 1.5;
  color: var(--ink-body);
}

.state-bad {
  color: var(--data-neg);
}

/* ---------------------------------------------------------------------------
 * R6.1 / R6.2 — motion.
 *
 * The only durations that exist are --dur-ui and --dur-theme, and this is the
 * one place the reduced-motion opt-out is written for shared components.
 * Page-level transitions (the map/dock slide) carry their own matching block,
 * because a media query cannot reach into another stylesheet.
 * ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .btn,
  .chip,
  .seg-btn,
  .badge-button,
  .topnav a:not(.topnav-mark) {
    transition: none;
  }
}

/* ---------------------------------------------------------------------------
 * R2.4 — THE SERIF-ON-DARK SWITCH, PRE-WIRED AND CURRENTLY OFF.
 *
 * "Dark mode: --font-display may only appear at --t-display and above. If the
 * WP2 gate screenshot shows serif reading gothic on dark, dark mode falls back
 * to Inter 600 for display and the serif becomes light-only. Decide AT the
 * gate with screenshots, not before."
 *
 * The first half is already structural: .figure is the only class in web/ that
 * names --font-display, and it is locked to --t-display. So the rule cannot be
 * broken by a page adding a small serif heading — there is no small serif.
 *
 * The second half is one commented rule. It is written out rather than left as
 * a note so that "act on the gate decision" is uncommenting three lines, not
 * re-deriving what the fallback was supposed to be. Do NOT uncomment it
 * without the human call the spec reserves.
 *
 *   html[data-theme="dark"] .figure {
 *     font-family: var(--font-ui);
 *     font-weight: 600;
 *   }
 * ------------------------------------------------------------------------ */
