/* Fraunces, self-hosted. SIL OFL 1.1 — see assets/fonts/Fraunces-OFL.txt.
 *
 * A single 67KB variable file covering weight 400-700 and optical size 9-144,
 * latin subset. Self-hosted rather than linked, because the whole site loads no
 * third-party requests and a font is not the thing to break that for; the file
 * is committed, so it is versioned with the pages that use it.
 *
 * `font-display: swap` shows the fallback immediately and swaps when the face
 * arrives. The headline reflows once on a cold load, which is a better trade
 * than a page that renders no title for 200ms. */
@font-face {
  font-family: "Fraunces";
  src: url("fonts/fraunces-latin-var.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Design tokens.
 *
 * Every colour is declared once, as `light-dark(light, dark)`. This file used to
 * carry three copies of the palette — a light block, a `prefers-color-scheme`
 * block and a `[data-theme="dark"]` block — and the last two were byte-identical
 * duplicates that had to be edited in lockstep. They did drift, which is how a
 * dark value can end up correct in one block and stale in the other with nothing
 * to catch it. One declaration per colour makes that class of bug unavailable.
 *
 * `color-scheme: light dark` is what tells `light-dark()` which branch to take,
 * so the two override rules below set the scheme rather than restating values.
 *
 * The dark side is stepped for its own ground, never flipped: the page is a
 * brown-black rather than a blue-black, so both themes read as the same
 * publication at different times of day.
 */
:root {
  color-scheme: light dark;

  /* Warm ivory rather than neutral grey. The page is text and thin marks on a
     large empty field, so the field's temperature is most of the character it
     has. The ink is warmed to match — a pure #0b0b0b on ivory reads as a
     printing error rather than as black. */
  --page:       light-dark(#faf8f3, #131210);
  --surface-1:  light-dark(#fefdfa, #1d1b17);
  --ink-1:      light-dark(#17150f, #faf7f0);
  --ink-2:      light-dark(#514c40, #c9c1b0);

  /* Carries every eyebrow, field label, hint, table header and axis label on the
     site — all of it around 11px, so all of it owes 4.5:1. An earlier #898781
     was 3.41:1 and failed on 56 elements at once. Still clearly lighter than
     --ink-2, which is what keeps the hierarchy. */
  --ink-muted:  light-dark(#6d6759, #948b78);

  --grid:       light-dark(#e6e1d5, #2f2c26);
  --axis:       light-dark(#c8c1af, #3d3931);
  --border:     light-dark(rgba(23, 21, 15, 0.11), rgba(250, 247, 240, 0.11));
  /* A visible hairline, for rules that separate rather than enclose. */
  --rule:       light-dark(rgba(23, 21, 15, 0.16), rgba(250, 247, 240, 0.17));

  /* Single-hue mark for bars and lines. Indigo rather than the old cyan-blue, so
     the marks and the interface stop being two unrelated blues. */
  --accent:     light-dark(#2f5fd0, #7d97ee);
  --accent-dim: light-dark(#8fa9ea, #3a4f9e);

  /* The fill to put white text on. --accent is a *mark* colour, held to 3:1 as a
     mark; under white text it owes 4.5:1 and does not reach it in either theme,
     so filled controls take this instead. */
  --accent-solid: light-dark(#2a4fae, #33468f);
  /* Ink for anything sitting on --accent-solid. Fixed rather than light-dark(),
     because the accent is a dark blue in both themes. */
  --on-accent: #fff;

  /* Link text, held to the 4.5:1 text floor rather than the 3:1 mark floor. */
  --link:       light-dark(#2a4fae, #93a9f2);

  /* Sequential ramp — heatmap and choropleth, where colour is the sole encoding.
     Dark runs the same seven steps in reverse so "more" always reads as the
     higher-contrast end against whichever ground is behind it. */
  --seq-1: light-dark(#dbe2fa, #1c2a6b);
  --seq-2: light-dark(#b4c2f2, #2c419b);
  --seq-3: light-dark(#8ba0e8, #3f5cc4);
  --seq-4: light-dark(#5f7ddc, #5f7ddc);
  --seq-5: light-dark(#3f5cc4, #8ba0e8);
  --seq-6: light-dark(#2c419b, #b4c2f2);
  --seq-7: light-dark(#1c2a6b, #dbe2fa);

  /* Status pair. Decoration beside a word that already states the case — the
     freshness dot, the surplus rail — never the sole carrier of a fact, and
     never a series colour. */
  --good:     light-dark(#3d7a33, #68a458);
  --critical: light-dark(#b8422f, #dc7259);

  /* Diverging pair for seasonality, which reads around a zero midpoint rather
     than up from a floor. Blue against orange is the canonical colourblind-safe
     diverging choice; the midpoint is neutral, never a third hue. */
  --div-cool:   light-dark(#2f5fd0, #7d97ee);
  --div-warm:   light-dark(#b5620d, #e0913f);
  --div-mid:    light-dark(#bdb6a4, #57524a);

  /* Map: no-data counties must read as "unmeasured", never as a ramp step. */
  --map-nodata:     light-dark(#e8e3d7, #2b2823);
  --map-hairline:   light-dark(rgba(255, 255, 255, 0.6), rgba(0, 0, 0, 0.45));
  --map-state-line: light-dark(#8d8779, #6d675b);

  --warn-bg:     light-dark(#fdf6e6, #2c2517);
  --warn-border: light-dark(#e5c877, #6f5d26);

  /* Elevation. Two steps only: panels sit on the page, and things that float
     over content sit above panels.

     `light-dark()` is a <color> function, so it cannot wrap a whole shadow list
     — only the colours inside one. Splitting the tint out keeps the geometry
     shared and lets the two themes differ where they actually need to: a soft
     warm grey on ivory, and something near black in dark, because a light
     shadow on a dark ground conveys no depth at all. */
  --shadow-tint-1: light-dark(rgba(23, 21, 15, 0.05), rgba(0, 0, 0, 0.34));
  --shadow-tint-2: light-dark(rgba(23, 21, 15, 0.07), rgba(0, 0, 0, 0.42));
  --shadow-1: 0 1px 2px var(--shadow-tint-1), 0 4px 12px var(--shadow-tint-1);
  --shadow-2: 0 2px 6px var(--shadow-tint-2), 0 14px 34px var(--shadow-tint-2);

  /* Fraunces carries the display line; the interface stays on the system sans,
     which costs nothing to load and is already tuned for small sizes on each
     platform. Serif for the voice, sans for the controls. */
  --font: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-display: "Fraunces", ui-serif, Georgia, "Times New Roman", serif;

  /* Paper grain. Held as an ink rather than an opacity so it can go through
     `light-dark()` like every other value here — the noise is applied as a mask
     over this colour, not as an image with its own alpha. A dark ground needs
     slightly more of it, because grain reads as light specks on dark and as
     shadow on light, and the eye forgives the second more readily. */
  --grain-ink: light-dark(rgba(23, 21, 15, 0.055), rgba(250, 247, 240, 0.055));

  /* Text selection. Tinted from the accent so a dragged selection belongs to the
     page; kept pale enough that --ink-1 on top of it still clears 4.5:1. */
  --selection: light-dark(#c9d6f7, #33468f);

  --radius: 10px;
  --radius-lg: 18px;
}

/* `light-dark()` resolves off the used colour scheme, so the toggle sets the
   scheme and every token follows from it. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

* { box-sizing: border-box; }

::selection { background: var(--selection); color: var(--ink-1); }

/* The scrollbar is furniture, and default furniture on a warm ground reads as a
   grey strip glued to the side of the page. Firefox takes the two-keyword form;
   Chromium and Safari take the pseudo-elements. Both are given the same colours
   rather than one being left to the platform. */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--axis) transparent;
}

::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }

::-webkit-scrollbar-thumb {
  background: var(--axis);
  border-radius: 99px;
  /* A transparent border with background-clip is the only way to inset a
     scrollbar thumb; padding does nothing to it. */
  border: 3px solid transparent;
  background-clip: content-box;
}

::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); background-clip: content-box; }

/* Paper grain over the whole page.
 *
 * The noise is an inline SVG turbulence filter used as a *mask* over a flat
 * ink, not as an image in its own right — which is what lets its colour come
 * from a token and follow the theme, and keeps the whole thing to one
 * same-origin-free data URI rather than a texture file to serve.
 *
 * It sits above everything, including the open county list, because grain that
 * stops at a popup's edge announces itself as a layer. `pointer-events: none`
 * keeps it out of every hit test on the page. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 100;
  pointer-events: none;
  background: var(--grain-ink);
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  mask-size: 140px 140px;
}

/* Long headings break where the line runs out, which routinely leaves one word
   alone on the last line of a display-size title. `balance` costs nothing on
   the short lines these are and fixes exactly that; `pretty` does the weaker
   job of preventing a single-word last line in running text. */
h1, h2, h3, .verdict-lead { text-wrap: balance; }
p, li, .sub, .hint { text-wrap: pretty; }

/* A class that sets `display` beats the UA stylesheet's `[hidden] { display: none }`,
   so hiding the loading panel needs to win explicitly. */
[hidden] { display: none !important; }

/* One focus treatment for everything, so no control can end up reachable by
   keyboard but invisible once reached. `:focus-visible` keeps it off mouse
   clicks, where a persistent ring reads as a stuck state. */
:where(a, button, summary, input, select, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 6px;
}

.skip-link {
  position: absolute;
  left: 8px;
  top: -60px;
  z-index: 60;
  padding: 9px 14px;
  border-radius: 8px;
  background: var(--surface-1);
  color: var(--ink-1);
  border: 1px solid var(--border);
  font-size: 0.85rem;
  text-decoration: none;
  transition: top 120ms ease;
}

.skip-link:focus { top: 8px; }

/* Transitions only. Animations are left to their own rules, because the loading
   spinner is slowed rather than stopped — a loading indicator that does not move
   reads as a hung page, which is a worse outcome than a slow rotation. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 1ms !important; }
}

/* A wash across the top of every page, so the largest empty area on the site has
   some depth in it rather than being flat ground.
 *
 * It is a background layer on `body` rather than a bleeding pseudo-element on
 * the hero. A pseudo-element would have to reach past the wrap's measure with
 * negative `vw` insets, and although `overflow: clip` hides the result, the
 * document's `scrollWidth` still grows — which is exactly what the layout test
 * measures, and a real horizontal scrollbar in any browser that lacks `clip`.
 * A background paints to the element's box and can overflow nothing.
 *
 * The far stop is `--page` and not `transparent`: a transparent stop
 * interpolates through transparent *black* in sRGB, greying the middle of the
 * fade into a smudge. */
body {
  margin: 0;
  background:
    radial-gradient(58% 480px at 24% 0%,
      color-mix(in oklab, var(--accent) 9%, var(--page)) 0%,
      var(--page) 72%) no-repeat top center / 100% 560px,
    var(--page);
  color: var(--ink-1);
  font-family: var(--font);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

.wrap {
  max-width: 1080px;
  margin: 0 auto;
  /* Less bottom padding than the page used to carry: the footer band below now
     provides the closing space, and 88px of air on top of it read as a gap. */
  padding: 40px 24px 52px;
}

/* ---- top nav ---- */

.topnav {
  position: sticky;
  top: 0;
  z-index: 30;
  background: color-mix(in srgb, var(--page) 88%, transparent);
  backdrop-filter: saturate(1.6) blur(10px);
  border-bottom: 1px solid var(--border);
}

/* Read-progress hairline along the bottom of the nav, driven by a scroll
   timeline — no JS, no scroll listener, and nothing to throttle. It rides the
   border the nav already has rather than adding a bar of its own.

   Gated on `@supports` because the fallback for an unsupported
   `animation-timeline` is an animation that runs on *time* — a progress line
   that fills itself over 0.3s while the page sits still, which is worse than
   no line at all. */
@supports (animation-timeline: scroll()) {
  .topnav::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -1px;
    height: 2px;
    background: linear-gradient(90deg, var(--accent), var(--div-warm));
    transform-origin: 0 50%;
    scale: 0 1;
    animation: read-progress linear both;
    animation-timeline: scroll(root block);
  }
}

@keyframes read-progress {
  to { scale: 1 1; }
}

.topnav-inner {
  max-width: 1060px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.brand {
  font-family: var(--font-display);
  font-variation-settings: "opsz" 14;
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: -0.008em;
  color: var(--ink-1);
  text-decoration: none;
}

.topnav-links {
  display: flex;
  align-items: center;
  gap: 4px;
}

.topnav-links a {
  color: var(--ink-2);
  text-decoration: none;
  font-size: 0.84rem;
  padding: 6px 10px;
  border-radius: 7px;
  transition: color 120ms ease, background 120ms ease;
}

.topnav-links a:hover { color: var(--ink-1); background: var(--surface-1); }

.topnav-links a[aria-current="page"] {
  color: var(--ink-1);
  background: var(--surface-1);
  box-shadow: inset 0 0 0 1px var(--border);
  font-weight: 560;
}

/* The nav is the one row with no scroll container of its own, so on a phone it
   has to give way rather than push the whole page wider.
   Three things happen, in order of preference:
     1. it drops the link to the page you are already on, and the Source link
        that the footer repeats
     2. the brand truncates instead of forcing the row wide
     3. whatever is left may scroll sideways, so no link becomes unreachable
   Adding a nav item must never be able to reintroduce a page-level overflow. */
@media (max-width: 620px) {
  .topnav-inner { padding: 8px 14px; gap: 10px; min-width: 0; }

  .brand {
    font-size: 0.78rem;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .topnav-links {
    gap: 2px;
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .topnav-links::-webkit-scrollbar { display: none; }

  .topnav-links a { padding: 5px 7px; font-size: 0.78rem; white-space: nowrap; }
  .topnav-links a[aria-current="page"],
  .topnav-links a[href^="https"] { display: none; }

  #theme-toggle { padding: 5px 9px; font-size: 0.78rem; }
}

/* ---- hero ---- */

.hero { padding: 34px 0 6px; }

/* Above 900px the headline stops around half the container and leaves the rest of
   the row empty. Setting the supporting text beside it rather than beneath fills
   that column and bottom-aligns the two, so the hero reads as one composed block
   instead of a wide measure of text that happens to stop early. Below 900px the
   grid never applies and the source order — eyebrow, headline, tagline, date —
   is already the right stacking order. */
@media (min-width: 900px) {
  .hero {
    display: grid;
    /* The headline is set large enough that an even split pushes it to four
       lines and strands the supporting text at the bottom of a tall row. Giving
       the display column the larger share keeps it to two or three. */
    grid-template-columns: minmax(0, 1.32fr) minmax(0, 1fr);
    column-gap: 56px;
    align-items: end;
    padding-bottom: 8px;
  }

  .hero .eyebrow { grid-column: 1; grid-row: 1; }
  .hero h1 { grid-column: 1; grid-row: 2; margin-bottom: 0; }
  .hero .tagline { grid-column: 2; grid-row: 2; font-size: 0.97rem; }

  /* The pill spans both columns even though it only ever fills part of one. It
     is a single run of text — counties, source, vintage — and penned into the
     left column it wrapped to three ragged lines with the date stranded on its
     own. The span buys it room; `inline-flex` still sizes it to its content. */
  .hero .freshness {
    grid-column: 1 / -1;
    grid-row: 3;
    margin-top: 24px;
    /* Grid items stretch by default, which overrides the pill's inline-flex
       shrink-to-fit and draws it as a full-width bar. */
    justify-self: start;
  }
}

.eyebrow {
  font-size: 0.71rem;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  color: var(--ink-muted);
  margin: 0 0 12px;
  font-weight: 600;
}

h1 {
  font-family: var(--font-display);
  /* Fraunces exposes optical size as an axis. Set high, the display cut has
     finer hairlines and tighter spacing, which is what makes a large headline
     look drawn rather than scaled up from body text. */
  /* The subset ships two axes, opsz and wght — SOFT and WONK are not in it, so
     a declaration naming them does nothing. Optical size at the top of its range
     is where the display cut lives: finer hairlines, tighter fitting, and the
     high-contrast thick/thin that makes a headline look drawn rather than
     enlarged. */
  font-variation-settings: "opsz" 144;
  font-size: clamp(2.5rem, 5.8vw, 4.15rem);
  line-height: 1.02;
  margin: 0 0 16px;
  letter-spacing: -0.028em;
  font-weight: 600;
}

.tagline {
  color: var(--ink-2);
  margin: 0;
  max-width: 62ch;
  font-size: 1.04rem;
  line-height: 1.62;
}

/* ---- loading ---- */

.loading {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 40px;
  padding: 26px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-1);
  color: var(--ink-2);
  font-size: 0.9rem;
}

.spinner {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid var(--axis);
  border-top-color: var(--accent);
  animation: spin 720ms linear infinite;
  flex: none;
}

@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 2.4s; }
}

.freshness {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-1);
  font-size: 0.82rem;
  color: var(--ink-2);
}

/* The live dot, wherever freshness is claimed — the masthead pill and the
   footer both make the claim, so the mark that carries it is not scoped to
   either one. Decoration beside a sentence that already states the date, never
   the sole carrier of it. */
.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--good);
  flex: none;
}

#theme-toggle {
  flex: none;
  background: var(--surface-1);
  color: var(--ink-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 12px;
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

#theme-toggle:hover { color: var(--ink-1); }

/* ---- panels ---- */

section.panel {
  position: relative;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  padding: 38px 40px;
  margin-top: 30px;
  transition: box-shadow 200ms ease, border-color 200ms ease;
}

/* A panel is not a button and must not pretend to be one, so it lifts by light
   rather than by moving: the shadow deepens, nothing translates. Moving the
   whole panel under the cursor would drag a chart out from under a tooltip. */
section.panel:hover {
  box-shadow: var(--shadow-2);
  border-color: var(--rule);
}

/* Panels fade in as they enter the viewport, on a view timeline — no observer,
   no scroll handler, no JS at all.
 *
 * Opacity only. An earlier version also translated the panel 26px, which looks
 * better in isolation and is wrong here: a panel is up to a thousand pixels of
 * map and chart, and sliding it while someone is reaching for it moves the
 * target out from under the pointer. It also made every test that drives the
 * mouse by coordinate racy, which is the same defect wearing a lab coat —
 * geometry that is still moving when it is read.
 *
 * Both guards are load-bearing. Without `@supports`, a browser lacking view
 * timelines runs the same keyframes on a *time* timeline: every panel fades in
 * once on load and the ones below the fold are invisible until then. Without
 * the motion query, it animates for people who have asked the system not to.
 * `both` holds the end state, so a panel already on screen at load is simply
 * finished rather than waiting to be scrolled to. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    section.panel {
      animation: panel-rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 34%;
    }
  }
}

@keyframes panel-rise {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Paper does not scroll, so a view timeline never advances and every panel
   below the first would print blank. The grain goes too — it is a texture for
   a screen, and on a printer it is a page of speckles. */
@media print {
  section.panel { animation: none; opacity: 1; }
  body::after { display: none; }
  .topnav::after { display: none; }
}

/* The folio numeral a printed piece would set in the corner of a section
   opening. It is drawn from a CSS counter rather than written into the markup,
   so a panel added or reordered renumbers itself and no HTML carries a number
   that could go stale against the "Step one / two / three" it sits beside.

   `--grid` is the faintest ink on the page and this is the one mark allowed to
   use it for type: it is decoration with a text equivalent immediately below
   it, so `aria-hidden` semantics come free — generated content is not announced
   as content, and nothing is lost if it never renders. */
#app { counter-reset: panel; }

/* Only the numbered steps take a folio. The chart panels below them are a
   gallery, not a sequence — the same distinction the rule beside the eyebrow
   already draws — so they neither carry a numeral nor advance the count. */
section.panel:has(> .eyebrow.step) { counter-increment: panel; }

section.panel:has(> .eyebrow.step)::before {
  content: counter(panel, decimal-leading-zero);
  position: absolute;
  top: 22px;
  right: 26px;
  font-family: var(--font-display);
  font-variation-settings: "opsz" 96;
  font-weight: 500;
  font-size: 3.2rem;
  line-height: 1;
  color: var(--grid);
  pointer-events: none;
  user-select: none;
}

section.panel .eyebrow { margin-bottom: 6px; }

/* The three numbered panels are a sequence; the charts below them are a gallery.
   A rule and a numeral mark the difference the way a printed piece would. The
   colour stays on the rule rather than the label, because tinting 11px text
   would put it at 4.35:1 on this surface, under the 4.5 floor. */
.eyebrow.step {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
  /* Clear the folio numeral in the corner. The rule is a flex spacer that grows
     to the full content width, so without this it runs straight through the
     numeral rather than stopping short of it. */
  padding-right: 58px;
}

.eyebrow.step::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--rule);
}

h2 {
  font-family: var(--font-display);
  font-variation-settings: "opsz" 48;
  font-size: 1.62rem;
  line-height: 1.16;
  margin: 0 0 8px;
  letter-spacing: -0.012em;
  font-weight: 600;
}

h3 {
  font-family: var(--font-display);
  font-variation-settings: "opsz" 24;
  font-size: 1.18rem;
  margin: 0 0 6px;
  letter-spacing: -0.006em;
  font-weight: 600;
}

.subsection {
  margin-top: 36px;
  padding-top: 28px;
  border-top: 1px solid var(--rule);
}

.sub {
  color: var(--ink-2);
  font-size: 0.9rem;
  line-height: 1.62;
  margin: 0 0 22px;
  max-width: 72ch;
}

/* ---- controls ---- */

.control-group { margin-bottom: 20px; }

.control-label {
  display: block;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  font-weight: 600;
  margin-bottom: 8px;
}

.control-note {
  margin: 9px 0 0;
  font-size: 0.78rem;
  color: var(--ink-muted);
  line-height: 1.5;
  max-width: 70ch;
}

.presets {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.presets button {
  background: var(--page);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 7px 14px;
  font: inherit;
  font-size: 0.85rem;
  color: var(--ink-2);
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease, border-color 140ms ease,
    transform 140ms ease, box-shadow 140ms ease;
}

/* A chip is a control and may move under the cursor — unlike a panel, there is
   nothing inside it to drag out from under a pointer. Held to 1px: enough to
   register as pressable, not enough to reflow the row. */
.presets button:hover {
  color: var(--ink-1);
  border-color: var(--axis);
  background: var(--surface-1);
  transform: translateY(-1px);
  box-shadow: var(--shadow-1);
}

.presets button:active { transform: none; box-shadow: none; }

@media (prefers-reduced-motion: reduce) {
  .presets button:hover { transform: none; }
}

.presets button[aria-pressed="true"] {
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--on-accent);
  box-shadow: var(--shadow-1);
}

/* "Other" is not one more company — it is the row's escape hatch. A hairline
   separates it so the sourced presets read as a closed set. */
.preset-sep {
  width: 1px;
  align-self: stretch;
  min-height: 20px;
  background: var(--border);
  margin: 0 2px;
}

.presets .preset-custom { border-style: dashed; }

.presets .preset-custom[aria-pressed="true"] { border-style: solid; }

.fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 14px;
}

.field label {
  display: block;
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
  margin-bottom: 5px;
}

.field input,
.field select {
  width: 100%;
  padding: 10px 11px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--page);
  color: var(--ink-1);
  font: inherit;
  font-variant-numeric: tabular-nums;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}

.field input:hover:not(:disabled),
.field select:hover:not(:disabled) { border-color: var(--axis); }

/* The native select control is the one place the page cannot help looking like
   whichever OS it is running on: macOS draws a rounded double-chevron slab, and
   next to it our own text input reads as a different species of control. Turning
   the appearance off and drawing the mark ourselves is what makes the four
   pickers in the row look like four of the same thing.

   The chevron is two borders on a rotated square rather than an image, so it
   inherits the theme's ink and costs no request. */
.select-wrap {
  position: relative;
  display: block;
  color: var(--ink-muted);
}

.select-wrap select {
  appearance: none;
  padding-right: 32px;
}

.select-wrap::after {
  content: "";
  position: absolute;
  right: 13px;
  top: 50%;
  width: 6px;
  height: 6px;
  border-right: 1.6px solid currentColor;
  border-bottom: 1.6px solid currentColor;
  /* Rotating a square puts its centre below the visual centre of the arrow it
     becomes, so the translate is over half to bring the point back to the middle. */
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
  transition: color 120ms ease;
}

.select-wrap:hover { color: var(--ink-2); }
.select-wrap:has(select:disabled) { color: var(--axis); }

.field input:focus,
.field select:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
}

/* A unit marker inside the box, so the field says what it wants without spending
   a line of the hint on it. The input padding clears the mark rather than sitting
   under it, because a long salary would otherwise run through the symbol.

   The mark is positioned against a wrapper around the input alone, not against
   the field: centring inside the field would have to account for the label above
   and the hint below, both of which change height when text wraps. */
.input-wrap { position: relative; display: block; }

.field.money input { padding-left: 25px; }
.field.percent input { padding-right: 27px; }

.field .adorn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-muted);
  font-size: 0.92rem;
  pointer-events: none;
  line-height: 1;
}

.field.money .adorn { left: 11px; }
.field.percent .adorn { right: 11px; }

/* A disabled control must look disabled. Unit size switches off on the Zillow
   basis, which publishes no bedroom split, and a greyed-out label alone is easy
   to read as "empty" rather than "not applicable here". */
.field select:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  background: var(--surface-1);
}

.field .hint {
  font-size: 0.72rem;
  color: var(--ink-muted);
  margin-top: 4px;
}

/* ---- combobox ---- */

.combo { position: relative; }

.combo input { padding-right: 32px; }

/* The toggle is a target, not a tab stop: the input beside it already opens the
   list on focus and on the down arrow, so a second stop in the tab order would
   only be somewhere for the keyboard to get stuck. */
.combo-toggle {
  position: absolute;
  right: 1px;
  top: 1px;
  bottom: 1px;
  width: 30px;
  border: 0;
  border-radius: 0 8px 8px 0;
  background: none;
  color: var(--ink-muted);
  cursor: pointer;
  padding: 0;
}

.combo-toggle::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 6px;
  height: 6px;
  border-right: 1.6px solid currentColor;
  border-bottom: 1.6px solid currentColor;
  transform: translate(-50%, -70%) rotate(45deg);
}

.combo-toggle:hover { color: var(--ink-1); }

.combo input[aria-expanded="true"] + .combo-toggle::after {
  transform: translate(-50%, -30%) rotate(225deg);
}

.combo-list {
  position: absolute;
  /* Above the sticky topnav: an open popup outranks page furniture. */
  z-index: 40;
  top: calc(100% + 5px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 4px;
  list-style: none;
  max-height: 320px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface-1);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
}

.combo-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 7px 9px;
  border-radius: 7px;
  cursor: pointer;
  scroll-margin: 4px;
}

.combo-main { min-width: 0; flex: 1; }

.combo-name {
  display: block;
  font-size: 0.9rem;
  color: var(--ink-1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The postal code rides with the name so the row reads "King County WA" at a
   glance, without spending the width a second full state name would cost. */
.combo-st {
  margin-left: 6px;
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  color: var(--ink-muted);
}

.combo-where {
  display: block;
  margin-top: 1px;
  font-size: 0.72rem;
  color: var(--ink-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.combo-rent {
  flex: none;
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

.combo-rent.none {
  font-size: 0.72rem;
  color: var(--ink-muted);
}

/* The active row is pointed at by aria-activedescendant, so it is styled from
   aria-selected rather than from :hover — the keyboard and the mouse must light
   up the same row, and only ever one of them at a time. */
.combo-row[aria-selected="true"] {
  background: var(--accent-solid);
}

.combo-row[aria-selected="true"] .combo-name,
.combo-row[aria-selected="true"] .combo-st,
.combo-row[aria-selected="true"] .combo-where,
.combo-row[aria-selected="true"] .combo-rent {
  color: var(--on-accent);
}

.combo-row mark {
  background: none;
  color: inherit;
  font-weight: 650;
}

.combo-empty {
  padding: 9px;
  font-size: 0.82rem;
  color: var(--ink-muted);
}

.combo-more {
  padding: 7px 9px 4px;
  font-size: 0.72rem;
  color: var(--ink-muted);
  border-top: 1px solid var(--border);
  margin-top: 4px;
}

/* ---- stat tiles ---- */

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: 12px;
  margin-top: 24px;
}

/* Tiles sit inside a panel that is already --surface-1, so they take --page:
   a nested card on the same fill as its container is a border with nothing
   behind it. Same reasoning as .verdict and .callout. */
.stat {
  position: relative;
  background: var(--page);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 18px 18px 19px;
  overflow: hidden;
  transition: border-color 160ms ease, box-shadow 160ms ease;
}

.stat:hover {
  border-color: var(--rule);
  box-shadow: var(--shadow-1);
}

/* The last tile flips between "Left over" and "Short by", which is the only place
   on the page where a number changes meaning rather than magnitude. The rail
   marks that flip, and is decorative on purpose, for two measured reasons.

   Neither --good nor --critical clears 4.5:1 against this fill, so neither may
   set the value text. And the pair is red against green: the validator puts them
   at ΔE 4.1 (light) and 2.3 (dark) under deuteranopia, far under the 8 it wants.
   No red/green pair survives that check — which is fine here only because the
   two never appear together. One tile is in one state at a time, and the label
   above it already says which. Read the rail as emphasis on a word, never as the
   word itself; anything that made colour load-bearing here would be wrong. */
.stat::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--rail, transparent);
}

.stat.is-good  { --rail: var(--good); }
.stat.is-short { --rail: var(--critical); }

.stat .label {
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
  margin-bottom: 6px;
}

.stat .value {
  font-family: var(--font-display);
  font-variation-settings: "opsz" 72;
  font-weight: 600;
  font-size: 2.2rem;
  line-height: 1.05;
  letter-spacing: -0.022em;
  /* Tiles sit in a row and are read down the column as much as across it, so the
     digits have to line up. */
  font-variant-numeric: tabular-nums;
}

.stat .note {
  font-size: 0.78rem;
  color: var(--ink-2);
  margin-top: 4px;
}

/* ---- charts ---- */

/* Scrolling shadows, appearing only on the edge there is more content past.
   Two layers do the work: the covers are attached `local`, so they travel with
   the content and slide off the shadow as you scroll away from an edge, while
   the shadows are attached `scroll` and stay pinned to the box. When nothing
   overflows, both covers sit over both shadows and nothing shows — so this needs
   no script and cannot fall out of sync with the actual scroll position.

   Worth the four gradients because the widest thing on the page, the heatmap,
   shows three of its seven columns on a phone, and a grid that gives no sign it
   continues simply reads as a grid with three columns. */
.chart-scroll {
  overflow-x: auto;
  background:
    linear-gradient(to right, var(--surface-1), transparent) 0 0 / 28px 100% no-repeat local,
    linear-gradient(to left, var(--surface-1), transparent) 100% 0 / 28px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, rgba(11, 11, 11, 0.16), transparent) 0 0 / 12px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, rgba(11, 11, 11, 0.16), transparent) 100% 0 / 12px 100% no-repeat scroll;
}

/* `height: auto` is not optional next to `max-width: 100%`. With both a width and
   a height attribute on the SVG, capping the width alone leaves the height at its
   attribute value, so `preserveAspectRatio` centres the drawing and pads it with
   dead space — the heatmap was 636px tall around 523px of content. Letting the
   height follow the aspect ratio keeps the box the size of the picture. */
svg { display: block; max-width: 100%; height: auto; }

/* The one chart that must not be squeezed. Seven columns of "13.2%" need about
   700px however they are laid out, so on a phone the honest options are to
   scale it into illegibility or to let it overflow and be panned. It overflows;
   `.chart-scroll` is the scroller, and the page itself still never scrolls. */
svg.chart-wide { max-width: none; }

svg text { font-family: var(--font); }

.axis-label { fill: var(--ink-muted); font-size: 11px; }
.tick-line   { stroke: var(--grid); stroke-width: 1; }
.baseline    { stroke: var(--axis); stroke-width: 1; }

.cat-label   { fill: var(--ink-2); font-size: 12px; }

/* A value label sits at the end of its bar, and the 30% threshold rule crosses
   the plot wherever it falls — on a narrow screen the two land on top of each
   other. Painting the stroke first knocks the label out of whatever is behind
   it, so the rule appears to pass under the text instead of through it. The
   stroke is the surface colour, so it is invisible except where it overlaps. */
.value-label {
  fill: var(--ink-2);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  paint-order: stroke fill;
  stroke: var(--surface-1);
  stroke-width: 3px;
  stroke-linejoin: round;
}

.bar { fill: var(--accent); }
.bar-row:hover .bar { fill: var(--accent-dim); }

.threshold-line {
  stroke: var(--ink-muted);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}

.threshold-label {
  fill: var(--ink-muted);
  font-size: 10.5px;
  font-weight: 600;
  paint-order: stroke fill;
  stroke: var(--surface-1);
  stroke-width: 3px;
  stroke-linejoin: round;
}

.cell-value {
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.spark-line { fill: none; stroke: var(--accent); stroke-width: 2; }

.ts-dot { fill: var(--accent); opacity: 0; }
.ts-point:hover .ts-dot,
.ts-point:focus .ts-dot { opacity: 1; }
.ts-point:focus { outline: none; }

.div-bar:focus { outline: none; }
.div-bar:hover rect { opacity: 1 !important; }
.spark-dot  { fill: var(--accent); }

.legend {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 14px;
  font-size: 0.78rem;
  color: var(--ink-2);
}

.legend .ramp {
  display: flex;
  border-radius: 3px;
  overflow: hidden;
  border: 1px solid var(--border);
}

.legend .ramp i {
  width: 26px;
  height: 12px;
  display: block;
}

/* ---- small multiples ---- */

/* Stands in for a chart that has nothing to draw. Sized like a chart so the
   panel does not collapse and jump the page around as a value is retyped. */
.chart-empty {
  display: flex;
  align-items: center;
  min-height: 96px;
  margin: 0;
  padding: 18px 20px;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--page);
  color: var(--ink-2);
  font-size: 0.85rem;
  line-height: 1.55;
  max-width: 64ch;
}

.multiples {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: 16px;
}

.multiple {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px;
  /* Panels stretch to equal height in the grid; pushing the plot to the bottom
     of each one lines every plot area up on a common baseline. Without this,
     a longer description shifts its chart down the page and a panel can look
     lower than a neighbour whose value is actually smaller — which defeats the
     shared vertical scale the SVGs already use. */
  display: flex;
  flex-direction: column;
}

.multiple h3 {
  font-size: 0.86rem;
  margin: 0 0 2px;
}

.multiple .meta {
  font-size: 0.74rem;
  color: var(--ink-muted);
  margin: 0 0 8px;
  min-height: 2.2em;
}

.multiple .plot { margin-top: auto; }

/* ---- map ---- */

/* The frame is the positioning context for the controls and the status strip,
   both of which sit over the map rather than beside it — a map that gives up a
   column of its width to a button rail is a smaller map. */
.map-frame {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--page);
  overflow: hidden;
}

.map-svg {
  /* No min-width, and no horizontal scroller around it: zoom replaced both. On a
     phone the whole country is small, and the answer is to zoom into a region
     rather than to make the reader drag a 640px canvas sideways.
     `height: auto` keeps the aspect ratio exact, which is what lets pointer
     coordinates map into viewBox units by plain proportion — letterboxing would
     silently offset every zoom-at-cursor. */
  width: 100%;
  height: auto;
  display: block;
  /* Vertical page scrolling still belongs to the browser; horizontal drag and
     pinch belong to the map. */
  touch-action: pan-y;
}

.map-frame.is-panning .map-svg,
.map-frame.is-panning .map-county { cursor: grabbing; }

.map-county {
  stroke: var(--map-hairline);
  stroke-width: 0.2;
  vector-effect: non-scaling-stroke;
  cursor: pointer;
}

.map-county:hover { stroke: var(--ink-1); stroke-width: 1.2; }

/* ---- map controls ---- */

.map-controls {
  position: absolute;
  top: 10px;
  right: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 2;
}

.map-btn-group {
  display: flex;
  flex-direction: column;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: var(--shadow-2);
}

.map-btn-group .map-btn { border-radius: 0; box-shadow: none; }
.map-btn-group .map-btn + .map-btn { border-top: 1px solid var(--border); }

.map-btn {
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  padding: 0;
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--ink-2);
  cursor: pointer;
  box-shadow: var(--shadow-2);
  transition: color 120ms ease, background 120ms ease;
}

.map-btn svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.map-btn:hover:not(:disabled) { color: var(--ink-1); background: var(--page); }

/* Fade the glyph, never the button. `opacity` on the element takes its background
   down with it, so at 1× the map showed straight through the zoom-out control —
   which is precisely the "failed to render" look this rule exists to avoid. The
   surface stays opaque and only the icon recedes. */
.map-btn:disabled { cursor: default; }
.map-btn:disabled svg { opacity: 0.32; }

.map-status {
  position: absolute;
  left: 10px;
  bottom: 10px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--surface-1) 92%, transparent);
  border: 1px solid var(--border);
  font-size: 0.72rem;
  color: var(--ink-muted);
  pointer-events: none;
  z-index: 2;
}

.map-zoom-level {
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* On a phone the controls sit over New England, so their footprint is worth
   shrinking: smaller targets still clear the 24px minimum, and the gesture hint
   goes entirely because the buttons already say the same thing. */
@media (max-width: 560px) {
  .map-hint { display: none; }
  .map-controls { gap: 6px; }
  .map-btn { width: 26px; height: 26px; }
  .map-btn svg { width: 14px; height: 14px; }
}

/* State outlines are a stroked overlay only — they must never swallow a hover
   aimed at the county underneath. */
.map-states { pointer-events: none; }

.map-state {
  fill: none;
  stroke: var(--map-state-line);
  stroke-width: 0.7;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}

/* When the map shades whole states, the outline layer becomes the data layer:
   states take the fill and the pointer, and the counties beneath step out of the
   way entirely. Nothing else about the map changes — same geometry, same zoom,
   same pan. The fill itself is set inline by `paintStates`, because a `fill`
   attribute would lose to the `fill: none` above. */
.map-frame.is-state-mode .map-states { pointer-events: auto; }
.map-frame.is-state-mode .map-counties { pointer-events: none; }

.map-frame.is-state-mode .map-state {
  stroke-width: 0.9;
  cursor: pointer;
}

.map-frame.is-state-mode .map-state:hover { stroke: var(--ink-1); stroke-width: 1.6; }
.map-frame.is-state-mode.is-panning .map-state { cursor: grabbing; }

.map-selected {
  fill: none;
  stroke: var(--ink-1);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}

.map-legend { margin-top: 16px; }

.map-legend-caption {
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
  margin-bottom: 8px;
}

.map-legend-scale {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  font-size: 0.76rem;
  color: var(--ink-2);
}

.map-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-variant-numeric: tabular-nums;
}

.map-legend-item i {
  width: 15px;
  height: 15px;
  border-radius: 3px;
  border: 1px solid var(--border);
  display: block;
  flex: none;
}

/* ---- verdict + ladder ---- */

.picker-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: 14px 16px;
  margin-bottom: 18px;
}

/* Four controls that are not four equal questions. State narrows, county is the
   choice being made, and the two rent controls qualify it — so county gets the
   width, and the row stays one line rather than wrapping 2×2 and separating the
   state filter from the list it filters. Below this the auto-fit above takes
   over and each control gets a full row. */
@media (min-width: 860px) {
  .picker-row { grid-template-columns: 0.8fr 1.5fr 0.85fr 0.85fr; }
  /* The timing page asks only the first two of those questions. Stretched across
     the full width they would read as a form with fields missing, so the pair
     keeps its proportions and stops. */
  .picker-row-two {
    grid-template-columns: 0.8fr 1.5fr;
    max-width: 560px;
  }
  /* The state page asks three, and the state is the choice being made here
     rather than a filter on one, so it takes the width the county gets elsewhere. */
  .picker-row-three {
    grid-template-columns: 1.3fr 1fr 1fr;
    max-width: 800px;
  }
}

/* A chart with one row per county or per state is a table's worth of rows, and
   a state page holding two of them would be several screens of scrolling before
   the reader reached anything else. Capping the height and letting the grid
   scroll inside keeps every row present — nothing is sampled away — while the
   page stays navigable. The cap is generous enough that most states fit whole. */
.chart-tall {
  max-height: 620px;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  border-radius: var(--radius);
}

/* The horizontal scroller nested inside must not add its own vertical one. */
.chart-tall > .chart-scroll { overflow-y: visible; }

.state-detail {
  display: grid;
  gap: 20px;
}

@media (min-width: 900px) {
  .state-detail { grid-template-columns: 1.15fr 1fr; align-items: start; }
}

/* The verdict is the answer to the question in the headline, so it gets the band
   colour as a rule down its edge and a step more type than the prose around it.
   A 14px swatch used to carry the band; a chip carrying the band's *name* says
   the same thing in words, which survives both colourblindness and a screenshot
   pasted into a chat. */
/* This is the answer the page exists to give, and it used to be typeset as one
   more paragraph with a coloured edge. It now gets the band colour as a wash
   across the whole block and a step of display type, so the eye lands on it
   before it lands on the charts underneath.

   The wash is 9% of the band mixed into the page, which is a tint rather than a
   fill: --ink-1 on top of it still clears the 4.5:1 floor in both themes, which
   a saturated band colour behind text would not. */
.verdict {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 16px;
  padding: 20px 22px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background:
    linear-gradient(
      100deg,
      color-mix(in oklab, var(--band, var(--border)) 9%, var(--page)) 0%,
      var(--page) 78%);
  box-shadow: inset 4px 0 0 var(--band, var(--border));
  font-size: 1.02rem;
  line-height: 1.5;
}

.verdict-chip {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 14px 6px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-1);
  box-shadow: var(--shadow-1);
  font-size: 0.86rem;
  font-weight: 640;
  letter-spacing: -0.005em;
  color: var(--ink-1);
  white-space: nowrap;
}

.verdict-chip::before {
  content: "";
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
  background: var(--band, var(--border));
  box-shadow: 0 0 0 1px var(--border) inset;
}

.verdict-text { flex: 1 1 18ch; min-width: 0; }

.ladder-tick { stroke: var(--ink-1); stroke-width: 1; opacity: 0.45; }

.ladder-marker { stroke: var(--ink-1); stroke-width: 2.5; }

.ladder-marker-label {
  fill: var(--ink-1);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.callout {
  margin: 16px 0 0;
  padding: 10px 13px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--page);
  color: var(--ink-2);
  font-size: 0.8rem;
  line-height: 1.5;
}

.callout.warn {
  border-color: var(--warn-border);
  background: var(--warn-bg);
}

tfoot td { border-bottom: none; padding-top: 10px; }

/* ---- tooltip ---- */

#tooltip {
  position: fixed;
  z-index: 20;
  pointer-events: none;
  opacity: 0;
  transition: opacity 90ms ease;
  background: var(--surface-1);
  color: var(--ink-1);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 9px 11px;
  font-size: 0.8rem;
  box-shadow: var(--shadow-2);
  max-width: 260px;
}

#tooltip.on { opacity: 1; }
#tooltip .t-title { font-weight: 600; margin-bottom: 3px; }
#tooltip .t-row { color: var(--ink-2); font-variant-numeric: tabular-nums; }

/* ---- table ---- */

details.table-view { margin-top: 18px; }

details.table-view summary {
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--ink-2);
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--page);
  width: fit-content;
  list-style-position: inside;
}

details.table-view summary:hover { color: var(--ink-1); border-color: var(--axis); }

details.table-view[open] summary { margin-bottom: 4px; }

table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 12px;
  font-size: 0.83rem;
  font-variant-numeric: tabular-nums;
}

th, td {
  text-align: right;
  padding: 8px 9px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

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

/* These tables are read across a row — metro, then five figures — so the row is
   the unit that needs tracking, not the cell. */
tbody tr { transition: background 90ms ease; }
tbody tr:hover { background: var(--page); }

thead th {
  color: var(--ink-muted);
  font-weight: 600;
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* ---- footer ---- */

footer.notes {
  margin-top: 36px;
  font-size: 0.82rem;
  color: var(--ink-2);
}

footer.notes h2 { font-size: 0.95rem; margin-bottom: 8px; }
footer.notes li { margin-bottom: 7px; }
footer.notes a { color: var(--link); }

/* ---- site footer ---- */

/* The page ends on a full-width band a shade off the ground it has been sitting
   on all the way down. It reads as the bottom of the document rather than as one
   more panel, which is what a run of small links needs to stop looking like
   content that ran out.

   The element sits outside `.wrap` and re-establishes the measure itself. The
   usual `margin: 0 calc(50% - 50vw)` trick would have done it from the inside,
   but `100vw` counts the scrollbar and `.wrap` does not, so the footer would
   overhang by exactly the scrollbar's width — a horizontal scrollbar on every
   page, which is the one thing the layout tests check for by name. */
.site-footer {
  background: var(--surface-1);
  border-top: 1px solid var(--rule);
}

.footer-inner {
  max-width: 1080px;
  margin: 0 auto;
  padding: 0 24px;
}

/* Mobile first, and deliberately so. An `auto-fit` track list with three column
   minimums is the obvious way to write this and the wrong one: the minimums do
   not collapse, so on a 390px phone the footer alone forced the document 334px
   wider than the screen — a horizontal scrollbar on every page, from a block
   nobody scrolls to check. Each step up is an explicit breakpoint that the
   narrow case never has to survive. */
.footer-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 30px 40px;
  padding: 38px 0 30px;
}

@media (min-width: 660px) {
  .footer-grid { grid-template-columns: 1fr 1fr; }

  /* The blurb wants a measure of its own; two narrow columns would break it into
     a column of three-word lines. */
  .footer-brand { grid-column: 1 / -1; }
}

@media (min-width: 940px) {
  .footer-grid {
    grid-template-columns: minmax(230px, 1.25fr) minmax(130px, 0.62fr) minmax(260px, 1.5fr);
  }

  .footer-brand { grid-column: auto; }

  /* Seven sources stacked one per line make the footer taller than the section
     above it. Two columns turn the same list into a block the width of the space
     it was given, which is the shape the rest of the page is in. */
  .footer-sources ul {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 2px 22px;
    align-content: start;
  }
}

.footer-mark {
  font-family: var(--font-display);
  font-variation-settings: "opsz" 28;
  font-weight: 600;
  font-size: 1.12rem;
  letter-spacing: -0.01em;
  color: var(--ink-1);
  margin-bottom: 9px;
}

.footer-blurb {
  font-size: 0.82rem;
  line-height: 1.62;
  color: var(--ink-2);
  max-width: 42ch;
}

.footer-freshness {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 14px;
  font-size: 0.74rem;
  color: var(--ink-muted);
}

.footer-col h2 {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-muted);
  margin-bottom: 12px;
  font-weight: 600;
}

.footer-col ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.footer-col li {
  margin-bottom: 8px;
  font-size: 0.82rem;
  line-height: 1.45;
}

.site-footer a {
  color: var(--ink-2);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color 120ms ease, border-color 120ms ease;
}

.site-footer a:hover {
  color: var(--ink-1);
  border-bottom-color: var(--accent);
}

/* Each source names what it supplies. A bare list of seven institutions credits
   them without saying which number is whose, and this site's whole argument is
   that every figure on it is traceable. */
.footer-sources li span {
  display: block;
  font-size: 0.72rem;
  color: var(--ink-muted);
}

.footer-bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px 24px;
  padding: 15px 0 26px;
  border-top: 1px solid var(--border);
  font-size: 0.74rem;
  color: var(--ink-muted);
}

.footer-boast { max-width: 52ch; }

/* ---- method page ---- */

.prose { max-width: 780px; }

.method-head { padding: 30px 0 8px; }

.toc {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 26px 0 8px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-1);
  box-shadow: var(--shadow-1);
}

.toc a {
  font-size: 0.8rem;
  color: var(--ink-2);
  text-decoration: none;
  padding: 5px 11px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--page);
  transition: color 120ms ease, border-color 120ms ease;
}

.toc a:hover { color: var(--ink-1); border-color: var(--axis); }

.prose section {
  margin-top: 44px;
  scroll-margin-top: 70px;
}

.prose h2 {
  font-size: 1.86rem;
  margin-bottom: 14px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}

.prose h3 {
  font-size: 1.22rem;
  margin: 30px 0 8px;
}

.prose p {
  color: var(--ink-2);
  line-height: 1.68;
  margin: 0 0 14px;
}

.prose li { color: var(--ink-2); line-height: 1.68; margin-bottom: 8px; }

.prose a { color: var(--link); }

.prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.86em;
  background: var(--page);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 1px 5px;
}

.prose pre {
  background: var(--page);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px 16px;
  overflow-x: auto;
  font-size: 0.84rem;
  line-height: 1.6;
}

.prose pre code { background: none; border: none; padding: 0; }

table.facts {
  width: 100%;
  border-collapse: collapse;
  margin: 6px 0 18px;
  font-size: 0.85rem;
}

table.facts th,
table.facts td {
  text-align: left;
  vertical-align: top;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  white-space: normal;
  color: var(--ink-2);
}

table.facts th {
  color: var(--ink-1);
  font-weight: 600;
  width: 26%;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.85rem;
}

/* ---- equations ---- */

/* MathML, rendered natively. The site loads no CDN and carries no runtime
   dependency, which rules out KaTeX and MathJax; every current browser renders
   <math> without either. Only sizing and spacing are set here — the layout
   itself is the browser's job. */
math { font-size: 1.05em; }

.eq {
  margin: 18px 0 22px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--page);
  font-size: 1.15em;
  /* A long equation scrolls inside its own box rather than widening the page. */
  overflow-x: auto;
}

/* Symbols in the variable table are the row's subject, so they read as headings
   rather than as inline maths inside a sentence. */
table.facts th math { font-size: 1.1em; }

table.facts.wide th { width: auto; }
table.facts thead th { color: var(--ink-muted); font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.04em; }

.colophon { font-size: 0.78rem; color: var(--ink-muted); margin-top: 18px; }
footer.notes .colophon a { color: var(--ink-2); }

td .swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  margin-right: 8px;
  vertical-align: baseline;
  border: 1px solid var(--border);
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- narrow screens ---- */

/* Panel padding is the largest single source of wasted width on a phone: at 24px
   a side, a 390px screen spends a quarter of itself on gutters. Everything here
   buys horizontal room back; nothing changes the layout's structure. */
@media (max-width: 560px) {
  .wrap { padding: 20px 14px 56px; }

  section.panel { padding: 18px 15px; margin-top: 16px; }

  /* The folio is decoration bought with corner whitespace, and a phone has none
     to spend: the heading wraps into it and the numeral ends up struck through
     the title. It goes, and the "Step two" label that carries the actual
     meaning stays. */
  section.panel:has(> .eyebrow.step)::before { display: none; }
  .eyebrow.step { padding-right: 0; }

  .hero { padding: 18px 0 2px; }

  /* Two up rather than four down. At one per row the four tiles cost most of a
     screen of scrolling to say four numbers, and the pair-wise reading the row
     is for — rent against take-home, needs against what is left — is lost. */
  .stats {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
    margin-top: 18px;
  }
  .stat { padding: 13px 13px 13px 14px; }
  .stat .value { font-size: 1.32rem; }
  .stat .label { font-size: 0.7rem; }
  .stat .note { font-size: 0.74rem; }

  /* Beside the chip the sentence is left with about 150px and sets as a ragged
     column with a pool of empty space under the chip. Stacked, it gets the full
     width. */
  .verdict {
    display: block;
    padding: 13px 14px;
    font-size: 0.9rem;
  }
  .verdict-chip { margin-bottom: 9px; }

  .fields { gap: 12px; }

  .map-controls { top: 8px; right: 8px; }
  .map-status { left: 8px; bottom: 8px; }
}

/* ============================================================== touch + phone

   Two separate problems, deliberately behind two different queries.

   Hit areas are about the *pointer*, not the screen: a finger is imprecise on a
   1024px tablet too. Type size is about the *screen*, since a 0.71rem eyebrow
   is legible on a desktop and not on a phone held at arm's length.            */

/* An invisible target standing in for a mark too small to hit. `transparent`
   rather than `none`, because `fill: none` takes no pointer events at all and
   the rect would do nothing. */
.hit { fill: transparent; }

@media (pointer: coarse), (max-width: 620px) {
  /* WCAG 2.5.8 wants 24px in both directions. These all cleared it on width and
     failed on height, so the padding is vertical and the layout does not move. */
  .brand {
    display: inline-block;
    padding-block: 4px;
  }

  .site-footer li a,
  .footer-bar a {
    display: inline-block;
    padding-block: 5px;
  }

  /* The claim-to-file table on the method page: these links are the cell's
     whole point, not prose, so the inline-text exception does not cover them. */
  .prose td a {
    display: inline-block;
    padding-block: 4px;
  }
}

@media (max-width: 620px) {
  /* Nothing on a phone reads below 12px. Each of these was between 11.2 and
     11.9, close enough that raising it costs no line breaks, which is why the
     overflow tests still pass at 320px. */
  .eyebrow,
  .field .hint,
  .control-label,
  .stat .label,
  .stat .note,
  .multiple .meta,
  .footer-freshness,
  .footer-col h2,
  .footer-sources li span,
  .map-legend-caption,
  /* The whole status strip, not only the zoom reading: the gesture hint beside
     it inherits from `.map-status` and was left at 11.5px. It is hidden below
     560px, so only the narrow band above that ever showed it. */
  .map-status,
  .map-zoom-level {
    font-size: 0.75rem;
  }

  thead th,
  table.facts thead th { font-size: 0.78rem; }

  /* The colophon bar carries the licence link and the no-trackers line. Both
     are read, not skimmed past. */
  .footer-bar { font-size: 0.78rem; }

  /* Axis labels are the one place where more type can crowd the plot, so this
     is the smallest bump that clears the floor. */
  .axis-label { font-size: 12px; }

  /* The formula scales as a unit, so this moves the whole equation rather than
     its parts. It already lives in a scroller if it outgrows the column. */
  .prose code { font-size: 0.8rem; }
  math { font-size: 1.15em; }
}
