/* TokenDrift — "Instrument". A precision measuring device, not a landing page.
 *
 * Every element on this site is one of three things: a READING (a value), a
 * STAMP (when and where it was read), or a SCALE (what the value is relative
 * to). Nothing else earns space, and the styling is written to make that
 * distinction visible rather than to decorate the page.
 *
 * TYPE. Self-hosted IBM Plex, under the SIL Open Font License, whose text ships
 * beside the faces as `assets/OFL.txt` — never as a bare `OFL` or `LICENSE`,
 * which is an extensionless file the publisher's content-type map refuses by
 * design. Three families, one job each: Plex Sans CONDENSED for labels,
 * eyebrows and table headings (an engraved scale marking); Plex Sans for prose;
 * Plex MONO for every figure, source, timestamp and tier key. Plex was drawn
 * for an engineering company, which is why it fits a measuring instrument.
 *
 * Because every numeral on the site is in Mono, tabular alignment is STRUCTURAL
 * — a property of the typeface chosen for the column — rather than something
 * asked of a proportional face with `font-variant-numeric`. The body still
 * carries `tabular-nums` for the handful of digits that appear in prose.
 *
 * COLOUR. Paper, ink, and exactly one saturated accent (blueprint blue) doing
 * the work of every emphasis, plus two colours reserved for price DIRECTION and
 * nothing else. Saturation is spent in one place; boldness is spent on the tick
 * rule and nowhere else.
 *
 * Nothing here loads over the network. The faces are relative `url()`s, which
 * resolve against this stylesheet's own address — so the same declarations work
 * from `/assets/site.css` on the 404 page, from `../../assets/site.css` on a
 * model page, and over `file://`, with no root-relative path to get wrong.
 */

/* --- the faces ------------------------------------------------------------ */
/*
 * The Latin1 split of each face, not the complete one: 218 codepoints for about
 * 178KB across nine files, against roughly 900KB for the full set. `swap`, so a
 * slow font never blanks a price.
 *
 * `unicode-range` states the subset rather than leaving the browser to discover
 * it glyph by glyph. That matters here because three marks the site's own
 * chrome once used are NOT in this subset — U+2192 →, U+2191 ↑ and U+2193 ↓ —
 * and one more, U+25C6 ◆, was the brand mark. All four are now DRAWN (an inline
 * SVG symbol for the arrow, borders for the sort carets, an SVG for the mark)
 * rather than set as text, so no glyph on this site depends on a fallback face.
 *
 * What IS still outside the subset is content we do not author and must not
 * rewrite: a vendor model name containing an emoji, and the box-drawing
 * diagrams inside the normative documents, which are quoted verbatim. Those
 * fall through to the system stacks below, which is the correct outcome — the
 * alternative is editing a vendor's product name or a legal text to suit a
 * typeface.
 */
@font-face {
  font-family: "TD Plex Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(plex-sans-400-v1.woff2) format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA,
                 U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022,
                 U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212,
                 U+FB01-FB02;
}
/* Italic is a real requirement and not a completeness reflex: the ledes on
 * `changes.html` and the model page argue with `<em>`, and a synthesised
 * oblique of a face this carefully drawn is visibly wrong. */
@font-face {
  font-family: "TD Plex Sans";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url(plex-sans-400i-v1.woff2) format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA,
                 U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022,
                 U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212,
                 U+FB01-FB02;
}
@font-face {
  font-family: "TD Plex Sans";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(plex-sans-500-v1.woff2) format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA,
                 U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022,
                 U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212,
                 U+FB01-FB02;
}
@font-face {
  font-family: "TD Plex Sans";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(plex-sans-600-v1.woff2) format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA,
                 U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022,
                 U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212,
                 U+FB01-FB02;
}
@font-face {
  font-family: "TD Plex Cond";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(plex-cond-500-v1.woff2) format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA,
                 U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022,
                 U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212,
                 U+FB01-FB02;
}
@font-face {
  font-family: "TD Plex Cond";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(plex-cond-600-v1.woff2) format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA,
                 U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022,
                 U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212,
                 U+FB01-FB02;
}
@font-face {
  font-family: "TD Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(plex-mono-400-v1.woff2) format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA,
                 U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022,
                 U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212,
                 U+FB01-FB02;
}
@font-face {
  font-family: "TD Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(plex-mono-500-v1.woff2) format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA,
                 U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022,
                 U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212,
                 U+FB01-FB02;
}
@font-face {
  font-family: "TD Plex Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(plex-mono-600-v1.woff2) format("woff2");
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA,
                 U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022,
                 U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212,
                 U+FB01-FB02;
}

/* --- tokens: bare :root IS the light palette ------------------------------ */
/*
 * Every colour on the site is defined exactly once, here. The dark block below
 * and the `[data-theme]` overrides redefine TOKENS ONLY — no colour anywhere in
 * this file has its sole definition inside a media query, which is how a theme
 * ends up with a hole in it that only shows on somebody else's machine.
 *
 * Measured, both themes, against all four grounds a string can land on (page,
 * plate, striped row, hovered row). Every text pairing clears WCAG AA 4.5:1 and
 * every non-text boundary clears 3:1. The table is in the delivery note.
 */
:root {
  color-scheme: light;

  /* The four grounds, palest to deepest. This ladder is load-bearing: it is
   * what --text-3 below was measured against, and moving a step moves four
   * numbers. */
  --bg:            #f5f7f9;   /* paper — the page */
  --surface:       #ffffff;   /* the plate: a table, a tile, a notice */
  --surface-2:     #eff1f3;   /* the striped row */
  --surface-3:     #e8eaec;   /* the hovered row */

  /* A box edge, and a line that has to be SEEN. --border frames a plate the
   * reader already perceives from its fill, so it stays quiet. --rule is the
   * pinned heading's lower edge and the divider between two phone cards, both
   * of which carry meaning, so it clears 3:1 on all four grounds above
   * (3.44 / 3.69 / 3.26 / 3.06). The palette this replaces ran --rule at
   * 2.19:1 and said so in a comment; this is strictly better, and the row
   * hairline it was mostly spent on is gone — see `table.data td`. */
  --border:        #d5dae1;
  --border-strong: #7e868e;
  --rule:          #7e868e;

  --text:   #0f1419;   /* ink */
  --text-2: #39404b;
  /* The provenance grey: hostname, display name, timestamp — the three strings
   * this product exists to show, and the smallest type on the page. It has to
   * clear 4.5:1 on EVERY ground it lands on, not just on white. The first draft
   * of this palette used #6b7480, which fails on paper (4.41), on a striped row
   * (4.18) and on hover (3.92). This value measures 5.40 on paper, 5.80 on the
   * plate, 5.12 striped and 4.81 hovered. */
  --text-3: #5e6672;

  /* Blueprint. The one saturated colour that carries emphasis, and it is a
   * BLUE — a drawing-office blue — so that it can never be confused with the
   * two price-direction colours below. */
  --accent:        #1f3a93;
  --accent-hover:  #16296b;
  --accent-bg:     #e8ecf7;
  --accent-border: #6f88c6;
  /* The text colour that is legible ON the accent, which is a different question
   * from the accent itself and has to be answered once per ground. The accent is
   * a dark blueprint here and a light one in the dark palette, so a hardcoded
   * `#fff` on an accent-filled button is 10:1 on this ground and 1.9:1 on the
   * other. A colour needs a role per ground. */
  --accent-fg:     #ffffff;

  /* PRICE DIRECTION, and nothing else uses these two as a direction claim. They
   * are deliberately not the accent: the accent is emphasis, these are data. */
  --rise: #9c3025;
  --fall: #1f5f4a;

  /* One red on this site, and it means "adverse to the reader": a rate that
   * went up, a value that needs review, a reading too old to trust. A second
   * red a reader cannot tell from the first is worse than one red used
   * consistently, and Instrument spends saturation once per meaning. What
   * separates the three uses is never hue — the badges spell their word out and
   * the freshness bands each carry their own SHAPE. */
  --danger:        var(--rise);
  --danger-bg:     #fbeceb;
  --danger-border: #b4635c;

  /* Caution, one step below adverse: read twice, disagreed once. Ochre, which
   * is the only hue on the page that is neither blueprint nor a price
   * direction. */
  --warn:        #7a4c05;
  --warn-bg:     #fbf1dd;
  --warn-border: #ad8536;

  --up:   var(--rise);
  --down: var(--fall);

  --sans: "TD Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  /* Labels, eyebrows and column headings. Condensed is doing real work and not
   * a stylistic tic: a scale marking on an instrument is narrow because it has
   * to sit beside the reading without crowding it, and eight uppercase column
   * headings across 1000px of fixed table is exactly that problem. */
  --cond: "TD Plex Cond", "TD Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --mono: "TD Plex Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  /* Plex Sans sets a little smaller than the system UI stack at the same pixel
   * size, so the whole scale is nudged up rather than left to read as a
   * downgrade. */
  --s-xs: 11.5px;
  --s-sm: 13px;
  --s-md: 14.5px;
  --s-lg: 16px;
  --s-xl: 21px;
  --s-2xl: 30px;

  --gap: 16px;
  --radius: 4px;
  --max: 1180px;

  /* The alpha the tick rule's hairlines are drawn at, as ONE named token with
   * the argument recorded beside it — see `.rule-field` far below. It is an
   * argued exception to the contrast floor and it is allowed to be exactly
   * that: an isolated hairline at this alpha is about 2.2:1. Raising it turns a
   * thousand hairlines into a solid bar and destroys the only thing the element
   * says; everything a reader must RESOLVE — axis, gradations, labels,
   * extremes, the overlaid moves — is at full contrast. */
  --tick-alpha: 0.42;
}

/* Dark is DESIGNED, not inverted. `#1f3a93` on ink is 1.9:1 and unusable, so
 * the blueprint is redrawn as a light blue at 6.2–8.1:1, and every other
 * colour was derived and measured against the dark grounds rather than being
 * flipped. Only tokens appear in here. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --bg:            #0f1419;   /* the ink becomes the ground */
    --surface:       #161c23;
    --surface-2:     #1c232c;
    --surface-3:     #242c37;

    --border:        #2b333e;
    --border-strong: #6e767e;
    --rule:          #6e767e;   /* 4.02 / 3.72 / 3.44 / 3.06 */

    --text:   #e8ecf1;
    --text-2: #aab4c0;
    --text-3: #8d97a5;          /* 6.26 / 5.80 / 5.36 / 4.77 */

    --accent:        #86aaff;
    --accent-hover:  #a8c2ff;
    --accent-bg:     #151d33;
    --accent-border: #4a6cae;
    /* Inverted, because the accent is now the light colour. */
    --accent-fg:     #0f1419;

    --rise: #f0918a;
    --fall: #5fc7a6;

    --danger-bg:     #2a1513;
    --danger-border: #a8534b;

    --warn:        #e0aa4e;
    --warn-bg:     #251d0e;
    --warn-border: #8a7134;
  }
}

/* The same tokens under an explicit choice, so the two directions both win: a
 * host that stamps a theme onto the root element gets it whatever the OS says.
 * There is no `?theme=` affordance and no toggle — the site does not ask the
 * reader to configure an instrument. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --bg:            #0f1419;
  --surface:       #161c23;
  --surface-2:     #1c232c;
  --surface-3:     #242c37;

  --border:        #2b333e;
  --border-strong: #6e767e;
  --rule:          #6e767e;

  --text:   #e8ecf1;
  --text-2: #aab4c0;
  --text-3: #8d97a5;

  --accent:        #86aaff;
  --accent-hover:  #a8c2ff;
  --accent-bg:     #151d33;
  --accent-border: #4a6cae;
  --accent-fg:     #0f1419;

  --rise: #f0918a;
  --fall: #5fc7a6;

  --danger-bg:     #2a1513;
  --danger-border: #a8534b;

  --warn:        #e0aa4e;
  --warn-bg:     #251d0e;
  --warn-border: #8a7134;
}

/* --- base ---------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 400 var(--s-md)/1.55 var(--sans);
  font-variant-numeric: tabular-nums;
  overflow-x: hidden;
}

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-hover); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

code, .mono { font-family: var(--mono); font-size: 0.92em; }

.wrap { max-width: var(--max); margin: 0 auto; padding: 0 20px; }

/* --- the label voice ------------------------------------------------------- */
/*
 * One declaration for every engraved marking on the instrument — a stat tile's
 * `<dt>`, a filter's `<label>`, a column heading, a card label. They were six
 * near-identical blocks repeating the same four properties with three different
 * letter-spacings; a scale whose markings are set inconsistently is a scale a
 * reader stops trusting. Condensed at 600 rather than the body face, because
 * this is the one voice on the page that is neither a reading nor prose.
 *
 * Applied by each of those rules rather than by a shared class: the builder's
 * class names are fixed and this file may not invent markup to hang a style on.
 */
.eyebrow {
  font-family: var(--cond);
  font-weight: 600;
  font-size: var(--s-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* `clip` is deprecated and `clip-path` is the replacement; both are here because
 * the pair is what covers every engine still in service, and either one alone
 * hides the text on its own. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 20;
  background: var(--surface); color: var(--text);
  padding: 10px 14px; border: 1px solid var(--border-strong); border-radius: var(--radius);
}
.skip:focus { left: 8px; top: 8px; }

/* --- header -------------------------------------------------------------- */

/* Deliberately NOT sticky. A sticky masthead and a sticky table header have to
 * agree on a pixel offset, and they stop agreeing the moment the nav wraps —
 * which leaves table rows sliding through the gap. On a page whose whole point
 * is a long table, the column headings are the ones worth pinning. */
.site-head {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.head-inner {
  display: flex; align-items: center; gap: var(--gap);
  flex-wrap: wrap;
  min-height: 52px;
  padding-top: 8px; padding-bottom: 8px;
}

/* The nameplate. Mono, because the product's name is a string a machine reads
 * too — it is in every API response and every citation. */
.brand {
  font-family: var(--mono);
  font-size: var(--s-lg);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
  text-decoration: none;
  display: inline-flex; align-items: center; gap: 9px;
}
/* The mark is a miniature of the tick rule — four hairlines of graded height,
 * which is what this whole site draws at every scale. DRAWN, in an inline
 * `<svg>`, and not the `◆` it replaces: that glyph is outside the Latin1 subset
 * the faces are cut to, so it fell to a system font and arrived at a different
 * weight and baseline on every platform. It is a wordmark in markup, which is
 * expressly allowed; there is no image file anywhere on this site. */
.brand-mark {
  display: block;
  width: 13px; height: 15px;
  overflow: visible;
  color: var(--accent);
}
.brand-mark line { stroke: currentColor; stroke-width: 1.5; }

.site-head nav { display: flex; gap: 2px; margin-left: auto; flex-wrap: wrap; }
.site-head nav a {
  color: var(--text-2);
  text-decoration: none;
  font-family: var(--cond);
  font-weight: 500;
  font-size: var(--s-sm);
  letter-spacing: 0.025em;
  padding: 6px 10px;
  border-radius: var(--radius);
}
.site-head nav a:hover { color: var(--text); background: var(--surface-2); }
.site-head nav a[aria-current="page"] {
  color: var(--text); background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border);
}

/* --- page structure ------------------------------------------------------ */

main { padding-top: 34px; padding-bottom: 64px; }

.page-head { max-width: 68ch; margin-bottom: 26px; }

h1 {
  font-size: var(--s-2xl);
  line-height: 1.2;
  letter-spacing: -0.02em;
  font-weight: 600;
  margin: 0 0 10px;
}

h2 {
  font-size: var(--s-xl);
  line-height: 1.25;
  letter-spacing: -0.015em;
  font-weight: 600;
  margin: 0;
}

h3 {
  font-size: var(--s-lg);
  font-weight: 600;
  margin: 0 0 6px;
}

.lede { color: var(--text-2); font-size: var(--s-lg); margin: 0; }
.lede .badge { vertical-align: 1px; }

.prose { max-width: 72ch; color: var(--text-2); }

section { margin-top: 40px; }

.section-head { margin-bottom: 14px; }
.section-note { color: var(--text-3); font-size: var(--s-sm); margin: 6px 0 0; max-width: 78ch; }

.crumbs {
  font-family: var(--cond); font-weight: 500;
  font-size: var(--s-sm); letter-spacing: 0.04em;
  color: var(--text-3); margin-bottom: 14px;
}
.crumbs a { text-decoration: none; }
.crumbs a:hover { text-decoration: underline; }
.crumbs span[aria-hidden] { padding: 0 4px; color: var(--border-strong); }

/* A model's api_id is the key a caller types into an SDK, so it is set as one:
 * Mono, and allowed to break anywhere rather than pushed off the measure. */
.model-title { font-family: var(--mono); font-size: 25px; font-weight: 600; letter-spacing: -0.02em; word-break: break-word; }

.meta-list {
  list-style: none; display: flex; flex-wrap: wrap; gap: 6px 20px;
  padding: 0; margin: 14px 0 0;
  font-size: var(--s-sm); color: var(--text-3);
}
/* The label half of a `<b>value</b>` pair in a meta list is a marking, so it
 * takes the label voice; the value beside it stays in the reading voice. */
.meta-list b {
  color: var(--text-2);
  font-family: var(--cond);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  font-size: var(--s-xs);
}

/* --- the reading plate ---------------------------------------------------- */
/*
 * The instrument's readout: what was taken, and when. Formerly a "stat strip",
 * which is the template gesture this design exists to refuse — a row of big
 * numbers under small grey words, each in its own rounded card with a hairline
 * between.
 *
 * What makes it a plate rather than a strip: one recessed ground with the tiles
 * inset into it, gradations DRAWN between them as full-height rules instead of a
 * gap in a background colour, the label engraved in Condensed above the reading,
 * and the reading itself in Mono at a size that says it is the point of the box.
 * The markup is untouched — `<dl class="stats"><div class="stat"><dt>/<dd>` is
 * what the builder emits and four tests read those tags back — so every change
 * here is a change of styling, not of structure.
 */
/* The 2px ink edge along the top is the plate's DATUM — the line the readings
 * are taken against — which is why the top corners are square and the bottom
 * two are not. It is the one place on the site that spends the ink colour on a
 * rule, and it is what stops five numbers in a row from reading as a marketing
 * band. */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));
  /* The gradations between readings are the grid gap showing the plate's own
   * backing colour through it. Kept as a gap rather than moved onto each tile as
   * a border because the gap draws correctly when the tiles WRAP — at 390px they
   * are two abreast — where a `border-left` leaves a stray rule down the left of
   * every new row. What changed is the colour: --border was 1.27:1 on the old
   * palette and simply absent on the dark one. --rule clears 3:1 on all four
   * grounds, so a gradation on this instrument can actually be seen. */
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--border);
  border-top: 2px solid var(--text);
  border-radius: 0 0 var(--radius) var(--radius);
  overflow: hidden;
  margin-bottom: 30px;
}
.stat {
  background: var(--surface);
  padding: 14px 16px 15px;
}
.stat dt {
  font-family: var(--cond);
  font-weight: 600;
  font-size: var(--s-xs);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-3);
  margin: 0 0 6px;
}
.stat dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 21px;
  font-weight: 500;
  letter-spacing: -0.03em;
  color: var(--text);
  line-height: 1.15;
}
/* The qualifier under a reading. Prose, not a marking: it is a sentence about
 * what the number does and does not claim, and setting it in the label voice
 * made four tiles look like they carried two readings each. */
.stat dd small {
  display: block; font-family: var(--sans); font-size: var(--s-xs);
  font-weight: 400; color: var(--text-3); letter-spacing: 0;
  margin-top: 5px; line-height: 1.45; text-transform: none;
}

/* One row, however many tiles: an auto-fit track count that lands one short
 * leaves a conspicuous hole where a fifth statistic should be. */
@media (min-width: 900px) {
  .stats { grid-template-columns: repeat(var(--tiles, 4), minmax(0, 1fr)); }
}
@media (max-width: 899px) {
  /* An odd tile count otherwise leaves a blank half-cell at the end. */
  .stats .stat:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

/* --- the hero ------------------------------------------------------------- */
/*
 * Thesis left, reading plate right, calibrated tick rule across the foot of
 * both. The h1 gets a MEASURE and not a column: a 30px headline run to 1123px is
 * eleven words a reader's eye has to track back across, and the space to its
 * right is where the readout belongs — which is also why there is no gradient
 * and no oversized number anywhere in here.
 */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: 26px 42px;
  align-items: start;
  margin-bottom: 34px;
}
.hero-thesis { max-width: 40ch; }
.hero-thesis h1 { margin-bottom: 12px; }
/* The plate sits on the h1's own baseline rather than at the top of the grid
 * cell, so the two halves of the hero read as one instrument face. */
.hero-plate { padding-top: 6px; }
/* Two by two, not four across. `.stats` is `auto-fit minmax(155px, 1fr)` and the
 * `--tiles` override further up puts every tile on one row at 900px, which in
 * half a hero is four 130px columns — narrower than the widest reading in them
 * ("2026-08-16 18:02"). A 2×2 readout is also what the plate is: two rows of two
 * gradations, which is how the physical thing is laid out. */
.hero-plate .stats { margin-bottom: 0; }
@media (min-width: 900px) {
  .hero-plate .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --- the calibrated tick rule --------------------------------------------- */
/*
 * The signature. It is the one element on this site allowed to be bold, and
 * everything else stays quiet so that it can be.
 *
 * Contrast, stated once because the field is an argued exception: the axis, the
 * gradations, the decade labels, the two extremes and the overlaid repricings are
 * all at FULL contrast, because each of them is something a reader is asked to
 * resolve. The hairline FIELD is not — see `.rule-ticks`.
 */
.rule {
  grid-column: 1 / -1;
  border-top: 1px solid var(--border);
  padding-top: 18px;
}
.rule-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 4px 14px; margin-bottom: 14px;
}
.rule-eyebrow {
  font-family: var(--cond); font-weight: 600;
  font-size: var(--s-xs); letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--text-2);
}
/* The density reading in words, on the face of the element and not only in the
 * `aria-label`: it is the one fact the field conveys that no reader — sighted or
 * not — should have to resolve a hairline to get. */
.rule-density { font-family: var(--mono); font-size: var(--s-xs); color: var(--text-3); }

/* The field's height lives in a token because the zero lane has to line its own
 * bottom edge up with the AXIS inside that field, and the label under the lane
 * has to sit in the same row as the decade labels. Two elements agreeing on a
 * measurement is exactly the thing to name once. 0.81 is the axis's position in
 * the SVG's own coordinate space (y=60 of 74). */
.rule { --rule-field-h: 96px; --rule-axis-frac: 0.81; }
.rule-body { display: flex; align-items: start; gap: 14px; }

/* The values that have no position on a log axis, off the scale and visibly
 * empty. 23 published rates are genuinely $0.00; this lane is where they are, and
 * it holds no tick because none of them has a place on the axis. The gap to its
 * right is as load-bearing as the lane — it is what says "not part of the
 * scale". */
.rule-zero {
  flex: 0 0 auto;
  width: 46px;
  /* Two rows, mirroring `.rule-scale`: the field, then the label row. This is
   * what puts `$0.00` on the same line as `$0.001` — it was 22px above it, which
   * made the lane look like a legend for the scale instead of a lane beside it. */
  display: grid;
  grid-template-rows: var(--rule-field-h) auto;
  justify-items: center;
}
/* Dashed, empty, and stopping exactly at the axis's own height. Nothing is ever
 * drawn inside it: that is the point. */
.rule-zero-lane {
  display: block; width: 100%;
  height: calc(var(--rule-field-h) * var(--rule-axis-frac));
  align-self: start;
  border: 1px dashed var(--rule);
  border-radius: 2px;
  background: none;
}
.rule-zero-label {
  font-family: var(--mono); font-size: var(--s-xs); color: var(--text-3);
  margin-top: 2px; white-space: nowrap;
}

.rule-scale { flex: 1 1 auto; min-width: 0; }

/* `preserveAspectRatio="none"`, so the field fills the column it is given and
 * every x is a true fraction of the axis. Only axis-ALIGNED geometry is drawn
 * inside it — vertical hairlines and horizontal spans — so the aspect distortion
 * has nothing to distort, and `vector-effect` keeps every stroke exactly one
 * device-independent pixel wide however the box is scaled. The labels are HTML
 * below, because a glyph in here would be stretched. */
.rule-field {
  display: block;
  width: 100%;
  height: var(--rule-field-h);
}
.rule-field path { vector-effect: non-scaling-stroke; fill: none; }

/* THE ARGUED EXCEPTION, and the only one in this stylesheet.
 *
 * 1,141 hairlines at full strength are not 1,141 hairlines, they are a solid
 * black bar — which destroys the single thing this element says, namely WHERE in
 * five orders of magnitude the prices actually sit. So the field is drawn at
 * --tick-alpha, and an isolated hairline is then about 2.2:1 against the page,
 * under the 3:1 non-text floor.
 *
 * It is permitted here on two conditions, both met:
 *   - the reading the field conveys is stated in WORDS, in the `<svg>`'s own
 *     `aria-label` and again visibly in `.rule-density`, so no one has to resolve
 *     an individual hairline to learn anything;
 *   - the alpha is a SINGLE named token, `--tick-alpha`, declared once in the
 *     palette with this argument recorded beside it, so it cannot be nudged in
 *     one place and not another.
 *
 * Everything a reader must actually resolve is below at full contrast. */
.rule-ticks {
  stroke: var(--text);
  stroke-width: 1;
  stroke-opacity: var(--tick-alpha);
}

/* The per-model lane. The SAME viewBox and the same `scale_x`, rendered shorter —
 * a compact instance of one scale, never a second scale. Its `--rule-field-h` is
 * what changes, so the zero lane beside it and the label row below it follow
 * automatically. */
.rule-axis-line { stroke: var(--text); stroke-width: 1.25; }
.rule-decade    { stroke: var(--text); stroke-width: 1.25; }
.rule-minor     { stroke: var(--text-3); stroke-width: 1; }

/* The cheapest and the dearest value in the run. Full height and in the accent,
 * because the two callouts underneath name them and a callout pointing at
 * something invisible names nothing. */
.rule-extreme { stroke: var(--accent); stroke-width: 1.75; }

/* This run's vendor repricings: a span from the old figure to the new one, with a
 * drop line at each end into the field so the reader can see WHICH part of the
 * distribution moved. Rise and fall, which are the only two colours on this site
 * that mean price direction. */
.rule-move      { stroke-width: 2; stroke-linecap: butt; }
.rule-move-up   { stroke: var(--rise); }
.rule-move-down { stroke: var(--fall); }

/* The decade labels, positioned as a fraction of the axis they belong to. */
.rule-labels { position: relative; height: 1.5em; margin-top: 2px; }
.rule-mark {
  position: absolute; top: 0;
  font-family: var(--mono); font-size: var(--s-xs);
  color: var(--text-2); white-space: nowrap;
}
/* The end labels are pulled inside the box; without this the first overhangs the
 * page gutter and the last is clipped by the column. */
.rule-mark-mid   { transform: translateX(-50%); }
.rule-mark-end   { transform: translateX(-100%); }
.rule-mark-start { transform: none; }

.rule-ends {
  display: flex; flex-wrap: wrap; justify-content: space-between;
  gap: 10px 24px; margin-top: 12px;
}
.rule-callout { margin: 0; display: flex; flex-direction: column; gap: 1px; }
/* The dearest end is right-aligned, because that is the end of the scale it
 * names — the callouts are at the ends of the axis, so they sit at the ends of
 * the row. */
.rule-callout:last-child { text-align: right; }
.rule-callout-label {
  font-family: var(--cond); font-weight: 600;
  font-size: var(--s-xs); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--text-3);
}
.rule-callout-value {
  font-family: var(--mono); font-size: var(--s-lg); font-weight: 500;
  color: var(--accent); letter-spacing: -0.02em;
}
.rule-callout-what { font-size: var(--s-sm); color: var(--text-2); }
.rule-callout-tier { font-family: var(--mono); color: var(--text-3); }

/* Three arguments, three paragraphs. They were one 92ch slab six lines deep —
 * the largest and the quietest thing in the hero, which is the wrong way round
 * for the only prose that explains the signature. A measure and a paragraph
 * break are what let a reader take one of them and stop. */
.rule-note {
  margin: 12px 0 0;
  font-size: var(--s-sm); color: var(--text-3);
  max-width: 74ch; line-height: 1.55;
}
/* No `.rule-note + .rule-note` any more: the field's legend is ONE paragraph,
 * and a rule for the gap between stacked notes would say that stacking them is
 * still expected. Both emitters — the hero and the decade note — write exactly
 * one. */
.rule-note strong { color: var(--text-2); }

/* The per-model lane. The SAME viewBox and the same `scale_x` as the hero,
 * rendered shorter — a compact instance of ONE scale, never a second scale. Only
 * `--rule-field-h` changes, so the empty zero lane beside it and the decade
 * labels below it follow without being told. */
.rule-compact {
  /* Shorter than the hero's 96px because the viewBox is cropped to match: the
   * overlay band is gone, so the box is 46 units tall instead of 74 and the
   * hairlines keep the same proportion of it. */
  --rule-field-h: 46px;
  margin-top: 22px;
  border-top: none;
  padding-top: 0;
}
.rule-compact .rule-note { margin-top: 9px; }
/* The axis sits at y=60 of a box that now runs 22..74, i.e. 38/52 of the way
 * down, so the empty zero lane beside it has to be told the new fraction or its
 * bottom edge stops lining up with the axis. */
.rule-compact { --rule-axis-frac: 0.73; }

@media (max-width: 899px) {
  .hero { grid-template-columns: minmax(0, 1fr); gap: 22px; }
  .hero-thesis { max-width: none; }
  .hero-plate { padding-top: 0; }
  /* A six-decade axis in 362px of viewport is 60px a decade, which no label
   * fits into. The field keeps its full domain — it is the same scale, and
   * rescaling it per breakpoint would make it a different one — and the labels
   * thin out to the three a reader needs to orient. */
  .rule { --rule-field-h: 72px; }
  .rule-mark { font-size: 10.5px; }
  .rule-labels .rule-mark:nth-child(2),
  .rule-labels .rule-mark:nth-child(4),
  .rule-labels .rule-mark:nth-child(6) { display: none; }
  /* The lane narrows but its label does not, so `$0.00` centred on a 34px lane
   * overhung both sides and came within a few pixels of `$0.001` — two different
   * scales' labels touching. Anchored left, it sits over its own lane and the gap
   * to the axis stays a gap. */
  .rule-zero { width: 38px; justify-items: start; }
  .rule-zero-label { font-size: 10.5px; }
  .rule-ends { flex-direction: column; }
  .rule-callout:last-child { text-align: left; }
}

/* --- notices ------------------------------------------------------------- */

.notice {
  border: 1px solid var(--border);
  border-left: 3px solid var(--text-3);
  background: var(--surface);
  border-radius: var(--radius);
  padding: 12px 15px;
  font-size: var(--s-sm);
  color: var(--text-2);
  margin-bottom: 26px;
  max-width: 86ch;
}
.notice strong { color: var(--text); }
.notice-warn { border-left-color: var(--warn); background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn); }
.notice-warn strong { color: var(--warn); }
.notice-accent { border-left-color: var(--accent); }

.empty {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  padding: 18px;
  color: var(--text-3);
  font-size: var(--s-sm);
  max-width: 78ch;
}

/* --- what this site is, above the first table ----------------------------- */
/*
 * Three lines, each ending in the page that proves it. Set as a list rather than
 * a paragraph because a reader in the first screen of a reference site is
 * scanning, not reading, and as text rather than cards because three cards is a
 * marketing gesture on a page whose product is a table.
 */
.claims {
  list-style: none;
  padding: 0;
  margin: 0 0 26px;
  max-width: 82ch;
  display: grid;
  gap: 8px;
  font-size: var(--s-sm);
  color: var(--text-2);
}
.claims li {
  padding-left: 15px;
  border-left: 2px solid var(--accent-border);
  line-height: 1.55;
}
.claims strong { color: var(--text); }

/* --- the standing disclosures --------------------------------------------- */
/*
 * The three notices this replaces were stacked between the stat strip and the
 * first price, so the page apologised three times before it sold anything. Every
 * sentence is still here, verbatim, one click away and beside the table it
 * qualifies. `<details>` and not a script: with scripting off the browser opens
 * it, so the failure mode is MORE disclosure rather than less.
 */
.disclosure {
  border: 1px solid var(--border);
  border-left: 3px solid var(--text-3);
  border-radius: var(--radius);
  background: var(--surface);
  max-width: 86ch;
  margin-bottom: 18px;
  font-size: var(--s-sm);
  color: var(--text-2);
}
.disclosure > summary {
  padding: 10px 15px;
  cursor: pointer;
  line-height: 1.5;
}
/* The marker is the only affordance saying this opens, so it is kept and given
 * the row's own colour rather than being replaced with a glyph of our own. */
.disclosure > summary::marker { color: var(--text-3); }
.disclosure > summary:hover { color: var(--text); background: var(--surface-2); }
.disclosure > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: var(--radius);
}
.disclosure-more { color: var(--accent); margin-left: 6px; }
.disclosure[open] > summary {
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.disclosure-body { padding: 15px 15px 1px; }
/* Inside the disclosure the notices are already framed by it, so they lose the
 * outer box and keep only the rule that distinguishes a warning from a note. */
.disclosure-body .notice { border: none; border-left: 3px solid var(--text-3); border-radius: 0; }
.disclosure-body .notice-warn { border-left-color: var(--warn); }
.disclosure-body > h3 { margin-top: 26px; }
.disclosure-body > h3:first-child { margin-top: 0; }

/* --- the 404's search ----------------------------------------------------- */

.find { margin: 0 0 34px; max-width: 46ch; }
.find label {
  display: block;
  font-family: var(--cond); font-weight: 600;
  font-size: var(--s-xs); text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--text-3); margin-bottom: 7px;
}
.find-row { display: flex; gap: 8px; }
.find input[type="search"] {
  flex: 1 1 auto; min-width: 0;
  font: inherit; font-size: var(--s-sm);
  padding: 7px 10px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}
.find input[type="search"]:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }

/* --- filters ------------------------------------------------------------- */

.filters {
  display: flex; flex-wrap: wrap; gap: 12px 18px; align-items: flex-end;
  padding: 14px 15px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  border-bottom: none;
}
.filters[hidden] { display: none; }

.field { display: flex; flex-direction: column; gap: 5px; }
.field label {
  font-family: var(--cond);
  font-weight: 600;
  font-size: var(--s-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}
.field input[type="search"], .field select {
  font: inherit;
  font-size: var(--s-sm);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 6px 9px;
  min-width: 150px;
}
.field input[type="search"] { min-width: 210px; }
/* Nothing was setting this, so the placeholder fell to the UA's #757575 — 4.46:1
 * on --bg, the one string on the page failing 4.5 by a hair while every token
 * around it passed. --text-3 is 5.20 and is already the colour of every other
 * subordinate string here. */
.field input[type="search"]::placeholder { color: var(--text-3); }

.field-checks { flex-direction: column; gap: 6px; }
.field-checks .check {
  display: flex; align-items: center; gap: 7px;
  font-size: var(--s-sm); text-transform: none; letter-spacing: 0; color: var(--text-2);
}
.field-checks input { accent-color: var(--accent); }

.field-status { margin-left: auto; flex-direction: row; align-items: center; gap: 12px; }
/* A reading, so it is set as one: how many rows the filter left standing. */
.filter-status { font-family: var(--mono); font-size: var(--s-sm); color: var(--text-3); margin: 0; }

.btn {
  font: inherit;
  font-family: var(--cond); font-weight: 600;
  font-size: var(--s-sm); letter-spacing: 0.04em;
  color: var(--text-2);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 6px 12px;
  cursor: pointer;
}
.btn:hover { color: var(--text); background: var(--surface-3); }

/* The filled variant, for the one button on the site that is the page's whole
 * purpose: the 404's search. It has to live AFTER `.btn` — both selectors weigh
 * (0,1,0), so source order is the only thing separating them, and defined above
 * `.btn` it lost every declaration it shares with it and rendered as a plain
 * grey button.
 *
 * `--accent-fg` and never `#fff`: the accent is a dark teal on the light ground
 * and a light one on the dark, so a fixed white foreground is 5.3:1 on one and
 * 1.7:1 on the other. A colour needs a role per ground. */
.btn-primary {
  color: var(--accent-fg); background: var(--accent); border-color: var(--accent);
}
.btn-primary:hover {
  color: var(--accent-fg);
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

/* --- tables -------------------------------------------------------------- */

/* `overflow-x: auto` already made this the nearest scrolling ancestor of the
 * sticky <th> — an overflow-y of `visible` computes to `auto` the moment the
 * other axis is not visible — but with no height its vertical scrollport had no
 * bottom to scroll to, so the header pinned to the top of the TABLE and slid
 * away with it. The max-height gives that scrollport a real end. It only binds
 * on a table taller than the viewport, so the short tables elsewhere on the
 * site render exactly as before; the 80px is the room the box needs to sit
 * fully on screen once the reader has scrolled the page down to it. */
.table-wrap {
  overflow: auto;
  max-height: calc(100vh - 80px);
  /* NOT redundant, and deleting it restores a 27,000-pixel void. Unpositioned,
   * this box let the 28,073px table's layout overflow leak into the VIEWPORT's
   * scroll region: at 1440px the document measured 28,042px tall against 1,999px
   * of content — scroll to 10,000 and the screen is blank white — and at 728px
   * the page scrolled 166px sideways despite `body { overflow-x: hidden }`.
   * Positioning it makes the box the containing block its own overflow is
   * measured against. Measured after: 1,312px tall, 0px horizontal. The bug
   * arrived with the max-height above and belongs to it. */
  position: relative;
  /* No `overscroll-behavior: contain` here. It was added when this box was not
   * yet a real scrollport; now that it is, containment means a wheel over the
   * table never chains to the page once the table hits its end, and below
   * 1180px the only ground left to scroll the page with is a 20px gutter. */
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  /* THE ACCESSORY THAT WAS REMOVED. This box used to carry a two-layer drop
   * shadow, and the `--shadow` token that fed it is gone from the palette with
   * it. A plate on a measuring instrument is bedded flush into its face; it does
   * not float above it, and a shadow was the one thing in this stylesheet
   * pretending the page had depth it has no reason to have. It also cost a token
   * that had to be redefined per theme (`none` on the dark one) to say nothing
   * at all. */
}
.filters + .table-wrap { border-radius: 0 0 var(--radius) var(--radius); }

table.data {
  width: 100%;
  min-width: 720px;
  border-collapse: collapse;
  font-size: var(--s-sm);
}
table.data.narrow { min-width: 520px; }
table.data.kv { min-width: 0; }

/* Two tables carry these eight columns: the front page's 152 model rows and the
 * 820 rows of every published value. Left to content-based sizing the table
 * grows past the viewport and pushes the two columns that ARE the product — when
 * it was verified, and what it was read from — off the right edge, where a
 * reader has to go looking for them. Fixed layout keeps them on screen and
 * truncates the long model ids instead, which carry a title. */
/* 1120px, re-measured for the Instrument type and not inherited.
 *
 * It was 1000, fitted to the system UI and system mono stacks. Plex Sans
 * Condensed and Plex Mono are not those widths, and at 1000px three columns
 * clipped as soon as the faces landed: both price headings lost the "USD/1M"
 * this number exists to protect, and the Flags column cut "no longer the
 * vendor's" off mid-word — a badge whose whole job is to warn.
 *
 * MEASURED IN A BROWSER, at a genuine 1440px layout viewport, and not estimated:
 * a previous estimate on this same table was 18px short. `build.PRICE_COLUMNS`
 * carries the per-column arithmetic. The eight hard minimums total 974px; 1120
 * clears every one of them and leaves the surplus to Model, the only column that
 * truncates. At the 1180px `--max` the table gets 1123px, so nothing below has
 * changed for a desktop reader — the extra 120px is spent inside a band that
 * already scrolls under that width.
 *
 * This rule must remain the FIRST `table.data.fixed {` block in the file: a test
 * reads the minimum back out of the stylesheet with a non-greedy match and would
 * otherwise find the narrow-screen override's `min-width: 0`. */
table.data.fixed { table-layout: fixed; min-width: 1120px; }
table.data.fixed td { overflow: hidden; text-overflow: ellipsis; }

/* The scale markings. Condensed 600 uppercase — the label voice — which is what
 * buys back the room "USD/1M" needs on a heading that must not wrap. */
table.data thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--surface-2);
  text-align: left;
  font-family: var(--cond);
  font-weight: 600;
  font-size: var(--s-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
  padding: 0;
  /* The heading now has striped rows sliding under it, so its lower edge does
   * the work of telling the pinned row from the first data row. It is one of
   * the two places --rule survived the removal of the row hairline, and the
   * reason it was raised to 3:1. */
  border-bottom: 1px solid var(--rule);
}
table.data thead th:not(:has(button)) { padding: 9px 12px; }
/* No `opacity` here. It read as a harmless de-emphasis and was a contrast
 * failure the token could not show: 0.7 composited --text-3 down to #8f949e,
 * 2.78:1 on the light ground and 3.40:1 on the dark one. Inherited whole,
 * --text-3 is 4.93 and 5.51. The unit is already subordinate by case and
 * letter-spacing; it does not need to be faint as well. */
table.data thead th .unit { text-transform: none; letter-spacing: 0; }

table.data thead th button {
  all: unset;
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: 9px 12px;
  cursor: pointer;
  color: inherit;
  font: inherit;
  white-space: nowrap;
}
/* The sort indicator, DRAWN as a border triangle rather than set as `\2191` /
 * `\2193`. Two independent reasons, and either alone would be enough:
 *
 *   - U+2191 and U+2193 are not in the Latin1 subset these faces are cut to, so
 *     as text they fell to a system font — a different weight, a different
 *     width, and a heading that shifted when it was sorted.
 *   - A BORDER survives `forced-colors: active`, which repaints every
 *     `background` as Canvas. The same argument the freshness bands are drawn
 *     under, further down this file.
 *
 * The box keeps its width in the unsorted state so that clicking a heading never
 * relayouts the row it is in. `currentColor`, so the caret follows the heading
 * from --text-3 to --text when it becomes the sorted one. */
table.data thead th button::after {
  content: "";
  display: inline-block;
  width: 0; height: 0;
  margin-left: 6px;
  vertical-align: 0.08em;
  border: 4px solid transparent;
  border-top-width: 5px;
  border-bottom-width: 5px;
}
table.data thead th[aria-sort="ascending"] button::after { border-bottom-color: currentColor; margin-bottom: 5px; }
table.data thead th[aria-sort="descending"] button::after { border-top-color: currentColor; margin-top: 5px; }
table.data thead th[aria-sort]:not([aria-sort="none"]) { color: var(--text); }
table.data thead th button:hover { background: var(--surface-3); }
/* `all: unset` throws `outline` away with everything else, and it does so at
 * (0,1,4) — the global `:focus-visible` rule is (0,1,0) and never reached these
 * seven controls, so Tab landed on every column heading with no ring in either
 * theme. This selector is (0,2,4). The offset is NEGATIVE because the button
 * fills its sticky cell edge to edge: a ring drawn outside the box is clipped
 * against the header's own boundary, which is a ring the reader cannot see. */
table.data thead th button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -3px;
}

/* NO row separator. A rule quiet enough to sit under 820 of them cannot clear
 * 3:1 — the palette this replaces ran it at 2.19:1 and documented the
 * compromise — and a rule loud enough to clear 3:1, repeated 820 times, is a
 * grid the reader has to look past to reach the numbers. Alternating ground does
 * the same work with no line at all, and it is the signal that survives on a
 * phone, where there is no hover. The stripe is --surface-2, one measured step
 * off the plate.
 *
 * Where a separator IS still drawn: the pinned heading's lower edge, above, and
 * the divider between two phone CARDS, which are blocks and not rows. */
table.data td { padding: 8px 12px; vertical-align: top; line-height: 1.45; }

/* Two signals, because one of them is unavailable on the device that needs it
 * most: the stripe is what tells row from row on a phone, where there is no
 * hover. Hover therefore has to move up to --surface-3 or it would land on the
 * stripe's own colour and read as nothing at all on every other row.
 *
 * `nth-child` counts the rows the DOM has, not the ones the reader can see, and
 * site.js filters by hiding rows — so past the first hidden row the alternation
 * doubles up, exactly while a narrowed list is being scanned. With scripting
 * off nothing is ever hidden and the count is exact, so it stays the default
 * and site.js opts out by putting `striped` on the table, taking the count over
 * itself with `stripe` on every second VISIBLE row.
 *
 * The three stripe rules and the hover rule all weigh (0,2,3) — hence the
 * `:where()`, which contributes nothing — so source order alone separates them
 * and hover, being last, still wins on a striped row. Anything inserted between
 * them inherits that ordering; anything appended after hover would beat it. */
table.data tbody tr:nth-child(even) { background: var(--surface-2); }
table.data:where(.striped) tbody tr:nth-child(even) { background: transparent; }
table.data tbody tr.stripe { background: var(--surface-2); }
table.data tbody tr:hover { background: var(--surface-3); }

table.data th.num, table.data td.num { text-align: right; font-family: var(--mono); white-space: nowrap; }
table.data th.num button { text-align: right; }
table.data td.nowrap { white-space: nowrap; }

table.data.kv th { text-align: left; font-weight: 500; color: var(--text-2); white-space: nowrap; width: 1%; padding-right: 28px; }
table.data.kv td { font-family: var(--mono); }
table.data.kv td.plain { font-family: var(--sans); color: var(--text-3); }

td .model-link {
  font-family: var(--mono); text-decoration: none;
  display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
td .model-link:hover { text-decoration: underline; }
td .display-name {
  display: block; color: var(--text-3); font-size: var(--s-xs); line-height: 1.4;
  margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

td .tier { font-family: var(--mono); white-space: nowrap; }
td .variant { color: var(--text-3); font-family: var(--mono); }

/* The front page shows one tier per model and this is the way to the rest of
 * them. Styled like `.display-name` — a muted second line under the cell's own
 * value — because that is what it is: a note about the row, not a second
 * value in it. Underlined rather than left to colour alone, since it sits in a
 * column whose first line is not a link and there is nothing to contrast with. */
td .more-tiers {
  display: block; font-size: var(--s-xs); line-height: 1.4; margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
td .more-tiers a { color: var(--text-3); text-decoration: underline; }
td .more-tiers a:hover { color: var(--text); }

td .null { color: var(--text-3); }

/* Three reasons a price cell is empty, told apart on sight as well as by the
   `.sr-only` explanation each one carries. An em dash for all three let a
   sighted reader look at "1.5 / —" on a cache row and conclude that output was
   free. */
td .null-na,
td .null-none { font-family: var(--mono); font-size: var(--s-xs); font-style: normal; }
td .null-none { color: var(--warn); }

.stamp { font-family: var(--mono); font-size: var(--s-xs); color: var(--text-2); }

/* The source name WRAPS rather than ellipsising, and the reason is the same one
 * `.fresh` gives below: truncation was eating the qualifier. Every name in the
 * registry runs 21-58 characters against a column of about 110px, so all eight
 * of them cut to roughly thirteen — "OpenRouter …", "Claude API …", "OpenAI API
 * …" on every row of every price table. That is a receipt a reader cannot read:
 * "Claude API pricing (docs)" and a hypothetical "Claude API pricing (API)"
 * truncate to the same string, and which one a number came from is exactly what
 * this column exists to say. The `title` still carries the full URL. */
.src { text-decoration: none; display: block; white-space: normal; overflow-wrap: break-word; }
.src:hover { text-decoration: underline; }
.src-host { display: block; font-family: var(--mono); font-size: var(--s-xs); line-height: 1.4; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.src-name { display: block; max-width: 24ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.table-foot { font-size: var(--s-sm); color: var(--text-3); margin: 10px 0 0; max-width: 82ch; }

/* --- freshness ----------------------------------------------------------- */

/* The cell was clipping, not ellipsising: `table.data.fixed td` sets
 * text-overflow, but the overflowing line boxes belong to this block, which had
 * none of its own, so "4 hours ago, unconfirmed" was cut mid-glyph to "…unconfi"
 * — amputating the one word that tells the reader not to trust the number.
 * Wrapping keeps the qualifier whole; only the stamp, whose width is fixed and
 * whose meaning survives truncation, still ellipsises. */
.fresh { display: block; }

/* Drawn as a pseudo-element so the marker does not depend on the empty
 * <span class="dot"> the builder ships 820 of; when that span goes, nothing
 * here changes. Each band gets a SHAPE as well as a colour — this dot was the
 * one marker on the page carrying its meaning in hue alone (the badges spell
 * "unconfirmed" out), which is no marker at all to a deuteranope or to anyone
 * reading in sunlight. */
.fresh .dot { display: none; }
.fresh::before {
  content: "";
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 7px; vertical-align: 0;
  /* No band. `band()` returns one of the four below for every row it renders,
   * so nothing on the site reaches this — and a fifth marker that looked like
   * `recent` in a different grey would be the hue-only marker this whole block
   * exists to abolish. A hollow dashed ring says "no band" and cannot be read
   * as any of them. */
  background: none;
  border: 1px dashed var(--text-3);
}
.fresh[data-age="recent"]::before { background: var(--accent); border: 0; }
.fresh[data-age="aging"]::before {
  background: var(--warn);
  border: 0;
  /* A clipped square, not a rotated one: rotation would give this marker an
   * 11.3px diagonal box and shift it off the others' baseline. */
  border-radius: 0;
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
.fresh[data-age="stale"]::before { background: var(--danger); border: 0; border-radius: 1px; }
.fresh[data-age="never"]::before { background: none; border: 2px solid var(--danger); }

/* Forced colours repaint every `background` as Canvas, so three of the four
 * bands disappeared into the page and only `never` came through — it is the one
 * drawn as a BORDER, and a border is repainted as CanvasText. Redrawn rather
 * than opted out of with `forced-color-adjust`: a reader in this mode chose that
 * palette, and the shape is what carries the band here anyway. A 4px border on
 * an 8px box under `border-box` sizing is a solid mark, which the radius and the
 * clip-path shape exactly as they shape a background; `never` stays hollow at
 * 2px. The no-band default needs nothing here — it is already a dashed border,
 * which this mode repaints rather than erases.
 *
 * `[data-age]` rather than a bare class so these weigh (0,2,1) — the same as the
 * band rules above — and settle against them on source order. */
@media (forced-colors: active) {
  .fresh[data-age]::before { background: none; border: 4px solid; }
  .fresh[data-age="never"]::before { border-width: 2px; }
}
.fresh .stamp {
  display: block; font-family: var(--mono); font-size: var(--s-xs); line-height: 1.4;
  color: var(--text-3); margin-left: 15px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* --- badges -------------------------------------------------------------- */

.badge {
  display: inline-block;
  font-family: var(--mono);
  font-weight: 500;
  font-size: 10.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.5;
  padding: 1px 6px;
  border-radius: 2px;
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text-2);
  white-space: nowrap;
}
.badge + .badge { margin-left: 4px; }

.badge-warn    { color: var(--warn);   background: var(--warn-bg);   border-color: var(--warn-border); }
.badge-danger  { color: var(--danger); background: var(--danger-bg); border-color: var(--danger-border); }
.badge-ok      { color: var(--accent); background: var(--accent-bg); border-color: var(--accent-border); }
.badge-muted   { color: var(--text-3); }
.badge-vendor  { color: var(--accent); background: var(--accent-bg); border-color: var(--accent-border); }
.badge-reading { color: var(--warn);   background: var(--warn-bg);   border-color: var(--warn-border); }
.badge-unknown { color: var(--text-3); background: var(--surface-2); border-style: dashed; }

/* --- changes ------------------------------------------------------------- */

.cause-key {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 34px;
}
.cause-key-item { background: var(--surface); padding: 13px 15px; }
.cause-key-item p { margin: 8px 0 0; font-size: var(--s-sm); color: var(--text-2); }

.delta { font-family: var(--mono); white-space: nowrap; }
.delta-up   { color: var(--up); }
.delta-down { color: var(--down); }
.arrow { color: var(--text-3); padding: 0 5px; }

/* --- the magnitude lane ---------------------------------------------------- */
/*
 * The tick rule's grammar again, at row scale: a centre-zero lane whose fill
 * grows right for a rise and left for a fall, log-scaled over 1%–1000%.
 *
 * A MARK ONLY WHERE A MAGNITUDE EXISTS. `build._magnitude` is reached from
 * exactly one branch of the delta cell — the one where both an old and a new
 * figure exist. A withdrawal gets `.mag-open` below instead of a length, and a
 * first publication and a source handover get NOTHING, because neither has a
 * magnitude for a length to be a length of. An empty track under every one of
 * them would read as "moved 0%", which is a measurement none of them made.
 */
.mag {
  display: block;
  position: relative;
  width: 78px;
  height: 6px;
  margin: 3px 0 0 auto;      /* right-aligned: it lives in a `td.num` */
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
/* Zero. The lane is centre-origin, so the middle of it has to be drawn or a fill
 * growing leftwards has no origin to be growing from. */
/* Zero, and it has to be the most legible thing in the lane: the fill grows out
 * of it in either direction, so a reader who cannot see the origin sees a bar that
 * starts at the left and reads every fall as a rise. --text-3 rather than --rule
 * for that reason — this 1px line is doing the work of an axis. */
.mag::before {
  content: "";
  position: absolute; left: 50%; top: -3px; bottom: -3px;
  border-left: 1px solid var(--text-3);
}
.mag-fill { position: absolute; top: 0; bottom: 0; }
.mag-up   { background: var(--rise); }
.mag-down { background: var(--fall); }

/* A withdrawal: closed on the side we have a reading for, open on the side we do
 * not. Deliberately not a bar — there is no percentage between a number and an
 * absence, and any length here would be invented. */
/* Inline with the word it qualifies, not on a line of its own: the lane below a
 * percentage is a second reading of the same fact, but a withdrawal has ONE fact
 * and the mark belongs beside it. Stacked, it hung under the badge's right edge
 * and read as a stray rule rather than a deliberate mark. */
.mag-open {
  display: inline-block;
  margin: 0 0 0 7px;
  vertical-align: 1px;
  border: 1px solid var(--rule);
  border-right: none;
  height: 9px;
  width: 12px;
}
.mag-open::before { content: none; }

/* --- the from-to arrow ----------------------------------------------------- */
/*
 * The sprite that holds the arrow's geometry has to be in the document but must
 * never occupy space or be spoken. `position: absolute` with no size rather than
 * `display: none`, which some engines have historically treated as a reason to
 * skip resolving the `<use>` inside it.
 */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* The arrow itself: sized in `em` so it tracks whatever type it sits in — it
 * appears in a Mono price cell, in a Condensed table heading and in the middle
 * of a prose sentence — and coloured --text-3, because the arrow is the quietest
 * part of "0.30 → 0.45". The two FIGURES are the reading; the arrow is only the
 * relation between them, and drawing it at full ink strength made the transition
 * shout louder than either number in it. */
.arw {
  display: inline-block;
  width: 1.15em;
  height: 0.62em;
  vertical-align: -0.01em;
  margin: 0 0.16em;
  color: var(--text-3);
}
/* Inside a coloured delta the arrow joins the direction it belongs to; a grey
 * hinge between two red figures reads as a break in the value. */
.delta .arw, .move .arw { color: currentColor; }

/* "0.003625 → 0.022" and "+506.9%" are two separate facts and were rendering as
 * one string, `0.022+506.9%`, with the sign of the percentage doing double duty
 * as a separator. The move is the reading; the percentage is the magnitude of
 * it, and it needs room to be a second thing. */
.move + .delta { margin-left: 0.6em; }

/* --- history ------------------------------------------------------------- */

.history-block { margin-top: 22px; }
.history-head {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  margin-bottom: 8px;
}
.history-head h3 { font-family: var(--mono); font-weight: 600; margin: 0; }
.history-head .count { font-family: var(--mono); font-size: var(--s-sm); color: var(--text-3); }

/* --- the price history chart ----------------------------------------------- */
/*
 * A STAIRCASE, not a line graph, and the difference is a claim about the data:
 * a vendor publishes a number and it holds until they publish a different one, so
 * a diagonal between two observations would draw a glide that never happened.
 * `build._spark` composes the geometry; this file decides what each part of it
 * looks like.
 *
 * `preserveAspectRatio="none"` again, so the chart fills its column — hence
 * `non-scaling-stroke`, without which the horizontal stretch would thicken every
 * riser and pull the dash pattern along the flats into long smears.
 */
.spark {
  display: block;
  width: 100%;
  max-width: 620px;
  height: 76px;
  margin: 2px 0 10px;
  overflow: visible;
}
.spark path { fill: none; vector-effect: non-scaling-stroke; stroke-linecap: butt; }

/* No `.grid` rule. `_spark` emits the stairs, the risers and the brackets and
 * nothing else — the gridlines it once styled at --border were 1.27:1 against the
 * page, which is decoration nobody could see. Restoring them means restoring the
 * contrast too: --rule, now at 3:1, is the floor a non-text line on this palette
 * is allowed to be. */
.spark .line-in  { stroke: var(--accent); stroke-width: 1.75; }
.spark .line-out { stroke: var(--text-2); stroke-width: 1.75; }
/* Output is told from input by a DASH as well as by colour, because on the dark
 * palette the two are a blue and a light grey and hue alone is not a legend. */
.spark .line-out:not(.line-gap) { stroke-dasharray: 5 3; }

/* The windows nobody looked in. Faint and finely dashed: the value is drawn
 * across them because it is our best reading, and drawn differently because we
 * did not watch it hold. */
/* A finer dot than the output line's dash, and lighter: at `2 3` against output's
 * `5 3` the two patterns were the same texture at a glance, and a reader could not
 * tell "we watched this" from "we did not look". */
.spark .line-gap { stroke-width: 1.25; stroke-dasharray: 1 3; opacity: 0.7; }

/* The bracket over each such window. --rule, so it clears 3:1 — this is the mark
 * that tells a reader a stretch of the chart is inferred, which makes it
 * something they have to be able to resolve. */
.spark .line-bracket { stroke: var(--rule); stroke-width: 1; }

.spark-key {
  display: flex; flex-wrap: wrap; gap: 2px 16px;
  font-size: var(--s-xs); color: var(--text-3); margin: -6px 0 12px;
}
.spark-key span::before {
  content: ""; display: inline-block; width: 13px; height: 0;
  margin-right: 6px; vertical-align: 4px;
  border-top: 2px solid currentColor;
}
.spark-key .k-in::before  { border-top-color: var(--accent); }
.spark-key .k-out::before { border-top-color: var(--text-2); border-top-style: dashed; }
.spark-key .k-gap::before { border-top-color: var(--rule); border-top-style: dotted; }
/* The axis note is prose, not a key entry, so it carries no swatch. */
.spark-key .k-axis::before { content: none; margin: 0; }

/* --- "held steady" -------------------------------------------------------- */
/*
 * The tick rule's grammar, one dimension down: a length that means something
 * because it is measured against a stated maximum — here, the longest observed run
 * among the tiers on this page. A bar normalised against itself would be full
 * length on every block and would encode nothing.
 */
.held {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 4px 10px; margin: 0 0 12px; max-width: 620px;
}
.held-label {
  font-family: var(--cond); font-weight: 600;
  font-size: var(--s-xs); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--text-3);
  flex: 0 0 auto;
}
.held-track {
  flex: 1 1 140px; min-width: 90px;
  height: 8px;
  border: 1px solid var(--rule);
  border-radius: 1px;
  /* A track and a fill, so the bar states its own maximum. A bare fill with no
   * surrounding track is a length with nothing to be a fraction OF. */
  background: none;
}
.held-fill {
  display: block; height: 100%;
  background: var(--accent);
  /* A run that is genuinely tiny against the page's longest — 19 hours beside 308
   * days is 0.26%, a fifth of a pixel — must still be a MARK. Rendered as
   * nothing it reads as an empty track, i.e. "this has not held at all", which is
   * the opposite of what a short run says. */
  min-width: 2px;
}
.held-value {
  font-family: var(--mono); font-size: var(--s-xs); color: var(--text-2);
  flex: 0 0 auto;
}

/* --- lists --------------------------------------------------------------- */

.doc-list { list-style: none; padding: 0; margin: 0; max-width: 80ch; }
.doc-list li {
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
  font-size: var(--s-sm);
  color: var(--text-2);
}
.doc-list li:last-child { border-bottom: none; }
.doc-list a.raw { font-size: var(--s-xs); color: var(--text-3); }

.limits { max-width: 80ch; padding-left: 20px; margin: 0; color: var(--text-2); font-size: var(--s-sm); }
.limits li { margin-bottom: 10px; }
.limits strong { color: var(--text); }

/* --- the normative documents --------------------------------------------- */
/*
 * These three pages are the only ones whose body comes out of `site.markdown`
 * rather than out of `build.py`, so this is the one block on the site written
 * against a document instead of against a table. It replaces `pre.doc`, which
 * shipped 850 lines of Markdown as monospace source: correct byte for byte and
 * unreadable, with 370 literal `**` on the methodology page alone.
 *
 * A measure, not the full page width. `.doc-body` is 30,000 words of prose and
 * an unbounded line of it at 1440px is roughly 190 characters, which a reader
 * loses their place in between one line and the next. The TABLES are the
 * exception and are allowed the whole width, because a table cut off at 78ch is
 * a table with a column missing.
 */

.doc-body { max-width: 78ch; color: var(--text-2); font-size: var(--s-md); }
.doc-body > p, .doc-body > ul, .doc-body > ol, .doc-body > blockquote { line-height: 1.65; }
.doc-body p { margin: 0 0 14px; }

/* The document states its own title and its version line, and the page head
 * above states which of the three documents this is. Both are wanted — the
 * masthead carries the version — so the document's own h1 is scaled to sit
 * under the page's rather than competing with it. */
.doc-body h1 { font-size: var(--s-xl); margin: 32px 0 4px; }
.doc-body h2 {
  font-size: var(--s-lg);
  margin: 38px 0 10px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.doc-body h3 { font-size: var(--s-md); margin: 24px 0 6px; color: var(--text); }
.doc-body h1, .doc-body h2, .doc-body h3 { color: var(--text); }

.doc-body ul, .doc-body ol { margin: 0 0 14px; padding-left: 22px; }
.doc-body li { margin-bottom: 7px; }
.doc-body strong { color: var(--text); }
.doc-body code {
  font-family: var(--mono); font-size: 0.92em;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 3px; padding: 0 3px;
}
.doc-body pre {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px;
  overflow-x: auto; font-size: var(--s-sm); line-height: 1.55;
  margin: 0 0 16px;
}
.doc-body pre code { background: none; border: none; padding: 0; }
.doc-body hr { border: none; border-top: 1px solid var(--border); margin: 26px 0; }
.doc-body blockquote {
  margin: 0 0 16px;
  padding: 12px 16px;
  border-left: 3px solid var(--accent-border);
  background: var(--surface-2);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.doc-body blockquote > :last-child { margin-bottom: 0; }
.doc-body blockquote h3 { margin-top: 0; }

/* Every table in these documents comes wrapped in this by `markdown._table`.
 * TERMS and METHODOLOGY both carry tables wider than the measure — one of them
 * is the field contract — and `overflow-x` on the wrapper is what keeps the
 * PAGE from scrolling sideways instead. */
.doc-body .table-scroll {
  overflow-x: auto;
  max-width: 100%;
  margin: 0 0 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.doc-body .table-scroll table { border-collapse: collapse; width: 100%; font-size: var(--s-sm); }
.doc-body .table-scroll th, .doc-body .table-scroll td {
  padding: 7px 12px; border-bottom: 1px solid var(--rule);
  text-align: left; vertical-align: top; line-height: 1.5;
}
.doc-body .table-scroll th { color: var(--text); font-weight: 600; background: var(--surface-2); }
.doc-body .table-scroll tbody tr:last-child td { border-bottom: none; }

/* The section link. Visible on hover and on keyboard focus, and never the only
 * route to the anchor: the heading carries the `id`, so a citation resolves
 * whether or not anybody ever sees this glyph. `aria-hidden` is deliberately
 * NOT set — it is a real link with a real accessible name. */
.doc-body .anchor {
  margin-left: 8px;
  font-size: 0.8em;
  text-decoration: none;
  color: var(--text-3);
  opacity: 0;
  transition: opacity 90ms ease-out;
}
.doc-body h1:hover .anchor, .doc-body h2:hover .anchor, .doc-body h3:hover .anchor,
.doc-body .anchor:focus-visible { opacity: 1; }

/* A cited section arrived flush against the top of the viewport, with the
 * horizontal rule above the heading scrolled just out of sight, so §9 looked
 * like the tail of §8. The masthead does not scroll with the page, so this only
 * has to buy back that rule and a line of air. */
.doc-body :target { scroll-margin-top: 24px; }

.doc-toc {
  max-width: 78ch;
  margin: 0 0 32px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}
.doc-toc ul {
  list-style: none; padding: 0; margin: 0;
  columns: 2; column-gap: 28px;
  font-size: var(--s-sm);
}
.doc-toc li { margin-bottom: 5px; break-inside: avoid; }
.doc-toc a { text-decoration: none; }
.doc-toc a:hover { text-decoration: underline; }

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

.site-foot {
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  padding: 26px 0 44px;
  color: var(--text-3);
  font-size: var(--s-sm);
}
.foot-lead { margin: 0 0 14px; max-width: 78ch; color: var(--text-2); }
.foot-links {
  list-style: none; display: flex; flex-wrap: wrap; gap: 6px 20px;
  padding: 0; margin: 0 0 16px;
  font-family: var(--cond); font-weight: 500; letter-spacing: 0.04em;
}
.foot-meta { margin: 0 0 8px; max-width: 86ch; line-height: 1.5; }

/* --- responsive ---------------------------------------------------------- */

@media (max-width: 720px) {
  :root { --s-2xl: 23px; --s-xl: 18px; }
  main { padding-top: 24px; }
  .wrap { padding: 0 14px; }
  .field-status { margin-left: 0; width: 100%; }
  .field input[type="search"] { min-width: 0; width: 100%; }
  .field { flex: 1 1 160px; }

  /* A scrollport inside a scrollport is not worth having on a screen this
   * short; the page scrolls instead. */
  .table-wrap { max-height: none; }

  /* Two columns of section links inside 390px of device width is four words a
   * line. The jump list is only worth having if a section name is readable. */
  .doc-toc ul { columns: 1; }

  /* The price table is 1000px of fixed columns in 362px of viewport — 2.8
   * screens of horizontal scroll. Scrolling right far enough to read "Read
   * from" takes Provider and Model off the left edge, so "whose price is this?"
   * and "where did it come from?" cannot be answered at the same time. The
   * fixed-layout reasoning above is a WIDE-screen argument; here each row
   * becomes a card instead, and every field of one row is on screen at once
   * with no horizontal scroll at all.
   *
   * Both axes go back to `visible` together: any other value on either one
   * computes the other to `auto`, which would make the wrapper a scroll
   * container again and strand the sticky heading inside a box with no bottom. */
  .table-wrap:has(> table.data.fixed) { overflow: visible; }

  table.data.fixed { display: block; table-layout: auto; min-width: 0; }
  table.data.fixed colgroup { display: none; }

  /* The column headings are also the sort controls, so they cannot simply be
   * dropped when the columns go: they become a one-line rail of sort buttons
   * that scrolls sideways on its own and stays pinned to the top of the
   * viewport. It can pin to the viewport because the wrapper above stopped
   * being a scroll container, and it can pin to 0 because the masthead is
   * deliberately not sticky — there is no second offset to agree with. */
  table.data.fixed thead {
    display: block;
    position: sticky; top: 0; z-index: 3;
    background: var(--bg);
    border-bottom: 1px solid var(--rule);
  }
  table.data.fixed thead tr {
    display: flex;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    /* A classic scrollbar would take a quarter of the pinned bar's height on
     * the desktop browsers that draw one; touch browsers overlay it anyway. */
    scrollbar-width: thin;
  }
  table.data.fixed thead th { position: static; border-bottom: none; flex: 0 0 auto; }
  /* Flags has no button, so as a chip it would be an unpressable one. */
  table.data.fixed thead th:not(:has(button)) { display: none; }

  table.data.fixed tbody { display: block; }
  table.data.fixed tbody tr {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 7px 12px;
    padding: 12px 0 14px;
    border-bottom: 1px solid var(--rule);
  }
  /* site.js filters by setting the `hidden` attribute, and an author `display`
   * beats the UA sheet that honours it: without this, filtering out a row on a
   * phone would leave its card on screen. */
  table.data.fixed tbody tr[hidden] { display: none; }
  table.data.fixed tbody tr:last-child { border-bottom: none; }

  table.data.fixed td {
    display: block;
    grid-column: 1 / -1;
    padding: 0 12px;
    border-bottom: none;
    /* The desktop ellipsis has no fixed column to clip against here, and a card
     * has the room to say the whole thing. Half a card is 167px at 390px of
     * device width, which a tier variant like ":priority_short_context" — one
     * unbreakable 173px token — runs straight out of, and body's overflow-x
     * would hide the tail rather than scroll to it. */
    overflow: visible;
    white-space: normal;
    overflow-wrap: anywhere;
  }

  /* Model is the card's title; provider and tier pair off under it, and the two
   * numbers a shopper came for sit side by side directly below, so identity and
   * price are never more than a glance apart. */
  table.data.fixed tbody td:nth-child(2) { order: 1; }
  table.data.fixed tbody td:nth-child(1) { order: 2; grid-column: auto; }
  table.data.fixed tbody td:nth-child(3) { order: 3; grid-column: auto; }
  table.data.fixed tbody td:nth-child(4) { order: 4; grid-column: auto; }
  table.data.fixed tbody td:nth-child(5) { order: 5; grid-column: auto; }
  table.data.fixed tbody td:nth-child(6) { order: 6; }
  table.data.fixed tbody td:nth-child(7) { order: 7; }
  table.data.fixed tbody td:nth-child(8) { order: 8; }

  /* With the heading row gone from above the values, each value has to carry
   * its own label or a bare "25" says nothing about which side of the meter it
   * is. The cells ship no data-label attribute — that is the builder's file,
   * not this one — so the headings are repeated here verbatim and this block
   * has to be renamed whenever a column is. It already went stale once, saying
   * "Verified" for a column renamed to "Last read", so a test now reads these
   * strings back out of this file and compares them with the built page's own
   * `<th>` text: the duplication stays, and it can no longer drift in silence.
   *
   * They are bound to `table.data.fixed`'s column ORDER, which is why the front
   * page and the full value table were deliberately given the SAME eight
   * columns and the A–Z model index — four columns, a different set — was not
   * given this class. `build.PRICE_COLUMNS` is the one definition of that
   * order; these strings are its narrow-screen half. */
  table.data.fixed tbody td::before {
    display: block;
    font-family: var(--sans);
    font-size: var(--s-xs);
    font-weight: 400;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-3);
  }
  table.data.fixed tbody td:nth-child(1)::before { content: "Provider"; }
  table.data.fixed tbody td:nth-child(3)::before { content: "Tier"; }
  table.data.fixed tbody td:nth-child(4)::before { content: "Input USD/1M"; }
  table.data.fixed tbody td:nth-child(5)::before { content: "Output USD/1M"; }
  table.data.fixed tbody td:nth-child(6)::before { content: "Last read"; }
  table.data.fixed tbody td:nth-child(7)::before { content: "Read from"; }
  table.data.fixed tbody td:nth-child(8)::before { content: "Flags"; }

  /* The empty-state row is one colspan cell, not a card. */
  table.data.fixed tbody td[colspan] { grid-column: 1 / -1; order: 0; }
  table.data.fixed tbody td[colspan]::before { content: none; }

  /* The prices are the reason the page exists, so they are the largest thing on
   * the card, and left-aligned because there is no longer a column of digits
   * for them to line up against. */
  table.data.fixed tbody td.num { text-align: left; font-size: var(--s-lg); }

  /* Nothing on a card truncates: the id, the display name, the source and the
   * hostname all wrap instead. A long model id is the one string that cannot
   * break at a space, so it is allowed to break anywhere. */
  table.data.fixed tbody td .model-link {
    white-space: normal; overflow: visible; overflow-wrap: anywhere; font-size: var(--s-md);
  }
  table.data.fixed tbody td .display-name,
  table.data.fixed tbody td .src,
  table.data.fixed tbody td .src-host,
  table.data.fixed tbody td .fresh .stamp,
  table.data.fixed tbody td .tier,
  table.data.fixed tbody td .variant {
    white-space: normal; overflow: visible; max-width: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}
