/* identity.css — AUTHORED. Never generated, never overwritten by a build.
 *
 * Layer 1 of the visual identity: palette, type scale, weight, radii and
 * elevation, as custom properties, plus the base element treatment that makes
 * them visible. structure.css (generated) carries layout and reaches for these
 * names with neutral fallbacks, so this file can take over completely without
 * fighting a single declaration in it.
 *
 * What this file deliberately does NOT do:
 *   - style any `[data-block]` by name, or anything that exists only inside
 *     one. That is layer 3 and it lives in blocks.css. The card recipe below
 *     is defined here as tokens and applied there.
 *   - style the inline marks. Those are layer 2, in marks.css.
 *
 * AUTHORED-PROSE DEVICES DO LIVE HERE, and they are the exception that keeps
 * the rule legible: a thing an AUTHOR put in the body of a page, carrying no
 * `data-block`, has no layer-3 home because layer 3 is keyed on `data-block`
 * and its vocabulary is tables. `img.shot` is the first (the screenshot an
 * author embedded); the card fence (`:::card{…}`,
 * project/plans/done/build-the-card-fence.md) is the second, and it is the first
 * consumer of `--ds-surface-card` anywhere.
 *
 * Derived from the S4 specimen return, 2026-09-10, adopted piecemeal: colour,
 * core typography and the elevation recipe were taken; its spacing, radius and
 * half-pixel type ramps were not, and are replaced by scales here.
 *
 * DARK ONLY, deliberately — it is what the product is. A light half is not
 * derived, not stubbed, and would be a fresh design rather than an inversion:
 * the look rests on a top-lit gradient over a dark ground, which has no
 * meaningful light-mode counterpart. Add one when developers ask for it.
 */

/* Redistributed under the SIL Open Font License 1.1. The notice the licence
 * requires travels with them, at fonts/OFL.txt, and the colophon credits both.
 * `font-display: swap` because a docs site that withholds its text while a
 * font loads is worse than one that reflows. */
@font-face {
  font-family: Roboto;
  src: url(fonts/roboto.woff2) format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Roboto Mono";
  src: url(fonts/roboto-mono.woff2) format("woff2");
  font-weight: 100 700;
  font-style: normal;
  font-display: swap;
}

:root {
  color-scheme: dark;

  /* ---- surfaces, back to front ---------------------------------------- */
  --ds-ground:       #08090b;  /* the page itself */
  --ds-paper:        #0e1116;  /* a recessed panel */
  --ds-raised-top:   #13161c;  /* card gradient, lit edge */
  --ds-edge:         #212630;  /* a card's own border — brighter than a rule */

  /* Two rule weights, and the distinction is load-bearing: `rule` separates
   * rows inside a table, `rule-strong` separates a header from its body and
   * one section from the next. Never use one for the other's job. */
  --ds-rule:         #1e232b;  /* between rows in a table */
  --ds-rule-strong:  #2c333d;  /* header from body, chrome from content */

  /* ---- content: the product's names, this site's values --------------- */
  /* B9. `appsignal-server` already ships a semantic content scale in
   * theme-tokens.css — `--content-default` / `--content-secondary` /
   * `--content-muted` / `--content-subtle` — so inventing a parallel
   * ink/muted/faint vocabulary was two names for one idea and the start of a
   * drift -- and `ink` is gone from this file entirely for the same reason:
   * a word that lost a naming argument and stayed in the comments is a second
   * vocabulary with no tokens behind it. The scale is `content-*`, here and in
   * the product, and nothing else names it.
   * The NAMES are adopted exactly, so a future "do our tokens match the
   * product's?" check is string equality rather than a translation table.
   *
   * ⚠️ THE VALUES DELIBERATELY DIVERGE, AND THAT DIVERGENCE IS A FINDING, NOT
   * AN OVERSIGHT. The product's dark `--content-subtle` is #787878, which
   * fails WCAG AA on BOTH of its own dark surfaces — 3.78 on
   * `--surface-default` and 4.17 on `--surface-canvas`. Adopting the hex along
   * with the name would import a live accessibility defect into a site that
   * currently passes. Measured and written up in
   * learnings/product-content-subtle-fails-aa-in-dark.md; P1 says report it
   * upstream rather than quietly correct it, so the name is shared, the value
   * is ours, and an equality check SHOULD flag these three rows. */
  --content-default:   #e9edf3;  /* prose, headings, values that matter */
  --content-secondary: #98a2b0;  /* table cells — most text on the site */
  /* ⚠️ #68717e until 2026-09-10, where it was the ONLY failing foreground on
   * the site: 4.03 on the page ground, against AA's 4.50. #7b8593 is 5.33.
   *
   * Not the 7.05 candidate that was on the table, and the reason is B3 rather
   * than B1: at 7.05 this sits within a hair of --ds-muted's 7.71, which would
   * have fixed the contrast by collapsing the three-rung content ladder into two
   * and made the wall-of-text complaint worse. The margin over AA only has to
   * be real, not maximal -- and with the wash gone (B4) the ground is #08090b
   * everywhere, so 5.33 is the number at every point on the page rather than
   * the best case. check-contrast.py enforces both halves. */
  --content-subtle:    #7b8593;  /* provenance, chrome, anything deferrable */


  /* ---- interaction and state ------------------------------------------ */
  /* ⚠️ `--ds-accent` #35d6f0 was DELETED here (B10), not re-tinted. It and
   * `--ds-link` measured **1.03** against each other — 1.00 is identical —
   * differing by 24 in the red channel and 2 in the others. Two tokens 1.03
   * apart are one token with two names, and they were being used on two
   * different axes: accent painted *selected* (a menu state) and link painted
   * *hover* (a link state). Hovering the page you were already on therefore
   * changed nothing, which is precisely the pair a reader most needs to tell
   * apart.
   *
   * The fix is not four distinguishable cyans — at four nobody can rank them,
   * and it spends the palette on chrome. The states are separated onto
   * DIFFERENT CHANNELS instead: interaction owns the hue, position owns a
   * structural mark plus brightness, pressed owns a non-colour change. So
   * there is exactly one interaction colour on this site, and it is this one. */
  /* ---- the action dialect ---------------------------------------------
   * SIX STATES. The rule used to be sayable in one line -- *hue is what you can
   * do, brightness is where you are* -- and as of 2026-09-11 it takes two,
   * because position moved onto a hue of its own (see `--action-selected`
   * below): **a hue says which question you are being answered; cyan is what
   * you can do, violet is where you are, and the three verdict hues are what we
   * found.** Brightness still ranks within a question, it just no longer
   * distinguishes between them. Declared in plans/done/block-vocabulary.md, checked
   * by check-block-vocabulary.py.
   *
   *   rest             you can click this        --action-rest, or unmarked
   *   hover            you are pointing at one   --action-hover, always the hue
   *   pressed          the click registered      --action-pressed-opacity
   *   selected         you are here, stickily    --action-selected + 500 + mark
   *   selected-within  you are inside this part  --action-selected, no mark
   *   focus            your keyboard is here     --content-default outline
   *
   * ⚠️ `--action-selected` IS NEW (2026-09-11) AND IT REVERSES WHAT THIS BLOCK
   * USED TO SAY. The text here argued that minting it would be "three names for
   * one value", because `selected`, `selected-within` and `focus` all took
   * `--content-default`. That was true while it was true, and it stopped being
   * true when the `eyebrow` role took `--content-default` as well: a group
   * heading, the page you are on and the section you are inside were then the
   * same colour, two lines apart, and the position cue was competing with a
   * label for the one step that was supposed to mean position.
   *
   * So position gets its own hue and gives `--content-default` back to content.
   * `focus` deliberately stays behind -- your keyboard being somewhere is a
   * different question from where you are in the site, and a focus ring that
   * matched `selected` would be the B10 defect again in a new place.
   *
   * ⚠️ THIS IS NOT B10 COMING BACK, and the numbers are the argument. B10
   * deleted `--ds-accent` #35d6f0 because it sat **24.2** from `--action-hover`
   * -- two cyans, one of them painting *selected* and the other *hover*, so the
   * reader could not tell the two axes apart. This token's nearest neighbour in
   * the whole palette is `--content-default` at **51.0**, and it is 134.3 from
   * `--action-hover`. The failure B10 names is two tokens too close to rank on
   * two different axes; this is one token, far from everything, on one axis.
   *
   * ⚠️ WHAT IT COSTS, PLAINLY. The old cue was `--content-default` against
   * `--content-secondary` links -- a **2.20:1** brightness step. This is
   * **1.54:1** plus a hue. For a reader who cannot separate violet from grey
   * the step is the smaller number alone, so the structural mark below is now
   * carrying more than it was. `selected` has one and is fine.
   * `selected-within` has none, and is the state this trade makes weaker --
   * see the note on its rule, and the open item in todo.md that wants it moved
   * onto the shape axis. That item was already open; this makes its case.
   *
   * ⚠️ TWO CYANS, AND THEY ARE THE TIGHTEST PAIR IN THE PALETTE -- 50 apart
   * against MIN_DISTANCE's 40. That cost buys the one thing the single
   * `--ds-link` could not do: it meant REST in prose (`a { color: … }`) and
   * HOVER in the nav (`nav a:hover { color: … }`), so hovering a prose link
   * made it less cyan and hovering a nav link made it more. Both now move the
   * same way. The alternative -- hover as `--content-default`, no new colour --
   * collides with `selected`, and then the page you are on is the one link that
   * cannot visibly be hovered, which is the defect B10 deleted `--ds-accent`
   * to fix. */
  --action-rest:     #3fb6cc;   /* 8.32:1 on ground — a link, where links are the exception */
  --action-hover:    #4dd8ee;   /* 11.74:1 — pointing at anything clickable */
  /* ⚠️ WAS `#d8bfe5`, the product's `--color-purple-200`, for one day
   * (2026-09-11). The violet was defensible and answerable to something, and it
   * was still a guess: it entered the palette because purple was the family
   * nothing else had spent, which is a reason about the palette rather than
   * about this site. This is the wafel's own warm, so the position hue is now
   * answerable to the mark at the head of the rail instead.
   *
   * ⚠️ IT IS NOT THE MARK'S GOLD, AND IT CANNOT BE. `#fdba47` — the lattice in
   * `site-art/wafel-lockup.svg` — sits **35.4** from `--ds-warn` #fbbf24,
   * under check-contrast.py's MIN_DISTANCE of 40: two colours 7° of hue and one
   * point of lightness apart. So the identity's own gold and the verdict for
   * *drifts, a gap* are one colour with two names, and the palette could not
   * hold both. This value is pulled redder (H 60 against the gold's 77) and
   * quieter (C 0.10 against 0.148), which puts it **96** from `warn` and
   * **61** from the gold: near enough to read as the mark's family, far enough
   * that a nav item is never mistaken for a warning or for the logo.
   *
   * Two hue families now, not three: **cyan is what you can do, warm is where
   * you are and who this is**. The verdict marks keep green/amber/red, which is
   * a vocabulary rather than a family — they name findings in a table, they
   * never say anything about the site itself.
   *
   * Lightness is deliberately unchanged from the violet (11.86 -> 11.55, both
   * L 0.83): position had the right brightness already, and this swap is meant
   * to move the hue and nothing else, so nothing downstream has to be re-judged.
   *
   * ⚠️ The 1.54:1 step over a `--content-secondary` link is UNCHANGED and so is
   * its cost. A reader who cannot separate warm from grey still gets the
   * smaller number alone — see `selected-within` below and the open item in
   * todo.md. Swapping violet for orange does not answer that; it was never a
   * hue problem. */
  --action-selected: #f8b884;   /* 11.55:1 — where you are, and only there */
  --action-pressed-opacity: 0.6;
  --ds-good:         #34d399;  /* verdict marks only — agrees, house */
  --ds-warn:         #fbbf24;  /* verdict marks only — drifts, a gap */
  --ds-bad:          #fb7185;  /* verdict marks only — inert, absent */

  /* ---- type ----------------------------------------------------------- */
  /* ⚠️ APPLE GETS SF. EVERYONE ELSE GETS A FONT WE CHOSE.
   *
   * The stacks here used to be Apple-first and then Apple-only-in-spirit:
   * seven names, six of which are Apple or Microsoft faces. Measured with
   * `fc-match` on Linux, 2026-09-11, what a reader actually got was
   *
   *   -apple-system / BlinkMacSystemFont / "SF Pro Text" / "Helvetica Neue"
   *                                                        -> Noto Sans
   *   Helvetica / Arial                                    -> Liberation Sans
   *   ui-monospace / Menlo                    -> Noto Sans, A PROPORTIONAL FACE
   *
   * Three problems, and the third is not cosmetic. Nobody chose Liberation
   * Sans. WHICH font you land on is decided by the distro's fontconfig
   * substitution table rather than by this file, so the same stylesheet
   * resolves differently on two Linux boxes and neither outcome is visible
   * from reading it. And a mono stack that can alias to a proportional face
   * lands that on inline code and every `identifier` mark -- the most repeated
   * element in the whole design.
   *
   * ROBOTO, and the reason is not taste: the product names it as its own
   * non-Apple preference in all five of its stacks (`--font-mona`,
   * `--font-body`, `--font-family-design`, `--font-family-app`, and
   * `"Roboto Mono"` in `--font-mono`). Picking Inter would be this site
   * inventing a typeface opinion the product does not hold. The brand faces --
   * MuseoSansRounded, Mona Sans -- are licensed product assets and are not
   * here; the fallback preference is public and unambiguous.
   *
   * SELF-HOSTED, not Google Fonts. The built site's only external host is
   * components.appsignal.design, which is ours; fonts.googleapis.com would be
   * its first third-party dependency, on a public page. 75 KB for both.
   *
   * ⚠️ `Menlo` is DELETED rather than demoted -- it is the entry that aliases
   * to a proportional face, and leaving it after "Roboto Mono" is only
   * harmless for as long as the webfont loads. `SFMono-Regular` stays because
   * it resolves correctly or not at all.
   *
   * Symbols outside the Latin subset -- ✓ U+2713, → U+2192, └ U+2514 -- are
   * not in these files and fall back, as they did before and as they do in
   * every webfont. That is expected, not a gap. */
  --ds-font-body: -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
  --ds-font-mono: ui-monospace, SFMono-Regular, "Roboto Mono", monospace;

  /* Mono is the default for anything the machine derived — identifiers, hex
   * codes, counts, every table cell. Sans is reserved for prose written by a
   * person. That split is the strongest signal on the site: it tells a reader
   * which half of a page was measured. */

  /* ⚠️ SIX SIZES, AND THERE IS NO SEVENTH. plans/done/type-scale.md.
   *
   * Joseph, 2026-09-10: a mini scale — H1–H4, Body, Caption — with **"this is
   * Body size" and no affordances for "body but slightly smaller" or "body but
   * extra-large"**. The seven t-shirt steps this replaces were DELETED rather
   * than aliased onto these numbers, because an alias preserves the exact
   * affordance the closed set exists to remove.
   *
   * What the old ramp actually did: 25 of its 31 use sites sat BELOW body, all
   * of them within 4px of each other. Four-fifths of the styled text on the
   * site was reaching for "body, but a bit quieter" — which is how a hierarchy
   * becomes a gradient, and a gradient is a wall of text. `--ds-text-xl` was
   * unused across two separate calibrations: a step nobody reaches for in
   * either is not a size the system needs, it is a size the system has.
   *
   * 13 · 16 · 20 · 25 · 32 · 40 — base 16, ratio ~1.25 either way.
   *
   * ⚠️ THE RULE FOR CHOOSING BETWEEN BODY AND CAPTION, and it is the whole
   * discipline: Caption is for labels ABOUT content. It is never for content.
   * A column header is a caption; the cell under it is Body. A provenance
   * stamp is a caption; a gap notice is Body, because a gap is a finding. The
   * stylesheets broke this 12 times before this change — table cells,
   * member-list items, gap-notice items and the route table were all findings
   * rendered as fine print because tables felt like they wanted to be dense. */
  --ds-h1:      40px;   --ds-lh-h1:      1.15;
  --ds-h2:      32px;   --ds-lh-h2:      1.20;
  --ds-h3:      25px;   --ds-lh-h3:      1.25;
  --ds-h4:      20px;   --ds-lh-h4:      1.30;
  --ds-body:    16px;   --ds-line-body:  1.55;
  --ds-caption: 13px;   --ds-lh-caption: 1.45;

  --ds-track-h1: -0.02em;
  --ds-track-h2: -0.015em;
  --ds-track-h3: -0.01em;

  --ds-weight-normal: 400;
  --ds-weight-medium: 500;  /* labels, table headers, pills */
  --ds-weight-bold:   600;  /* headings, and the odd lead sentence */

  /* ONE tracking value for uppercase mono, and it took a deletion rather than a
   * decision to get there. There were two — `--ds-track-caps` at 0.1em and this
   * at 0.08em — for two treatments the file itself called "otherwise
   * identical", and choosing between them was logged here as a values decision
   * nobody wanted to make. It resolved itself on 2026-09-11: the orientation
   * block was folded into the class templates, and `--ds-track-caps`'s only
   * consumer went with it. The second value is gone, not picked.
   *
   * ⚠️ RENAMED FROM `--ds-track-eyebrow`, because the eyebrow role stopped
   * setting tracking at all on 2026-09-11 -- it was sans and sentence case for
   * three days. A token named after its only-ever consumer and still wearing
   * that name after the consumer stopped using it is exactly the drift this
   * file exists to stop, and the rename is kept now that the consumer is back:
   * the name describes the job, not the wearer.
   *
   * ⚠️ IT IS NO LONGER "uppercase MONO". The eyebrow role took uppercase again
   * on 2026-09-14 and it is SANS, so this is the first proportional wearer.
   * One value still, deliberately -- 0.08em was chosen when the argument was
   * between two near-identical mono treatments, and nothing has measured it
   * against a sans face. If the rail's labels ever look loose, this is the
   * number, and splitting it is a values decision rather than a bug fix. */
  --ds-track-head:    0.08em;  /* table headers, and every uppercase label */

  /* ---- shape ----------------------------------------------------------- */
  --ds-radius-chip: 3px;   /* inline code, pills */
  --ds-radius-card: 16px;  /* a block-level surface */

  /* ---- elevation -------------------------------------------------------- */
  /* One recipe, defined here and applied by the block layer. The inset hairline
   * is the whole trick: it reads as a light source above the page, which is
   * what stops a dark surface looking like a hole. */
  /* The foot of the gradient is `--ds-paper` itself. It was a separate token,
   * `--ds-raised-bottom`, whose value was byte-identical to `--ds-paper` --
   * two names for #0e1116, so the card could drift from the panel it is
   * supposed to settle into and nothing would say so. */
  --ds-surface-card: linear-gradient(180deg,
                       var(--ds-raised-top) 0%, var(--ds-paper) 100%);
  --ds-shadow-card:  inset 0 1px 0 rgba(255, 255, 255, .045),
                     0 24px 48px -28px rgba(0, 0, 0, .9);

  /* Chips and pills sit on the surface rather than above it. */
  --ds-fill-subtle:  rgba(255, 255, 255, .06);

  /* ---- measure ---------------------------------------------------------- */
  --ds-measure:    70rem;
  --ds-space-page: 2rem 1.25rem 6rem;
}

/* Flat. The site shipped with two 10%-alpha radials fixed to the viewport here,
 * and they came out for two reasons (B4): they read as a generated-site tell,
 * and they were a live accessibility defect. Composited at their peak they
 * lifted the ground from #08090b to #0c2838 and took roughly a point off every
 * ratio on the page -- so the same text passed in one corner and failed in
 * another, which no token audit can catch and no palette fix can repair while
 * the gradient is there. tools/check-contrast.py now refuses a gradient on
 * `body` outright, so this cannot come back quietly. */
body {
  background: var(--ds-ground);
  -webkit-font-smoothing: antialiased;
  font-size: var(--ds-body);
}

/* ---- headings ---------------------------------------------------------- */
/* FOUR ROLES, AND A HEADING HAS EXACTLY ONE. Emitted by render-html.py, which
 * owns the one table deciding which (`ROLE_BY_BLOCK`). Declared in
 * plans/done/block-vocabulary.md and checked by check-block-vocabulary.py.
 *
 * ⚠️ NO DESCENDANT SELECTOR MAY SET A HEADING'S `font-size`, `font-family`,
 * `color`, `text-transform` OR `letter-spacing`. check-heading-roles.py fails
 * the build on one. That prohibition is the whole point and it is not a style
 * preference: before roles there were SEVEN treatments across 12 selectors at
 * five sizes, `nav h2` was styled in two files at once with only <link> order
 * deciding which won, and the same eyebrow recipe was written out longhand
 * three times and had already drifted into two sizes and two colours.
 *
 * The tag says how deep the heading is. The ROLE says what it looks like. They
 * are different questions and conflating them is what produced the seven. */
/* ⚠️ NO MEASURE, AND NO `text-wrap`. Both were added on 2026-09-14 to make the
 * front page's title break after its accent word, and removed the same day.
 * A 20ch measure plus balanced wrapping did produce the two even lines, and
 * that is the problem with it: the break was a side effect of the string being
 * 510.9px against a 457px box, so it was invisible in the markdown, unavailable
 * to an author who wanted one, and would move or vanish the moment anybody
 * rewrote the sentence. Joseph, rejecting it: *"Don't do weird max widths or
 * other occult stuff to force line breaks. I'd rather no line break than
 * whatever this is."*
 *
 * The rule that survives: **a title is one line of text and wraps only because
 * it ran out of page**, like every other line on the site. If a two-line title
 * is ever actually wanted, it needs a way to SAY so -- an authoring surface
 * question, not a width to tune here. */
[data-role="title"] {
  font-family: var(--ds-font-body);
  font-size: var(--ds-h1); line-height: var(--ds-lh-h1);
  letter-spacing: var(--ds-track-h1);
  font-weight: var(--ds-weight-bold);
  color: var(--content-default);
  text-transform: none;
}

/* ---- the accent word in a title: DELETED 2026-09-15 ----------------------
 * `[data-role="title"] strong` painted the product's name in the site's warm on
 * the front page, and it was the one selector `check-heading-roles.py` excused
 * by name. It went the way todo.md said it would: **the hero SVG replaces it**.
 * `site-art/illustrations/home-hero.svg` carries the graphics, the lockup, the
 * positioning and the two-line break, none of which we wanted to express in
 * CSS -- and `content/index.md` no longer has a title for an accent to sit in.
 *
 * Three things went in the same sitting, because the check fails on a stale
 * entry and would not have let them separate: this rule, the `ALLOWED` entry in
 * `tools/check-heading-roles.py`, and the `**AppSignal**` in the title.
 *
 * ⚠️ THE PROHIBITION NOW HAS NO EXCEPTIONS, which is the state it was written
 * for: no descendant selector may set a heading's typography. Do not re-open
 * the list -- one exception was argued at length above this line for a day; two
 * is a treatment vocabulary, which is the thing roles replaced.
 *
 * `--action-selected` keeps its name honestly now. This was its only non-action
 * call site, and the rename item in todo.md collapses with it. */
[data-role="section"] {
  font-family: var(--ds-font-body);
  font-size: var(--ds-h3); line-height: var(--ds-lh-h3);
  letter-spacing: var(--ds-track-h3);
  font-weight: var(--ds-weight-bold);
  color: var(--content-default);
  text-transform: none;
}
[data-role="subhead"] {
  font-family: var(--ds-font-body);
  font-size: var(--ds-h4); line-height: var(--ds-lh-h4);
  letter-spacing: normal;
  font-weight: var(--ds-weight-bold);
  color: var(--content-secondary);
  text-transform: none;
}
/* The most frequent heading on the site -- 2,919 of its 3,899 are the three nav
 * group labels, repeated on every page. It has been the least legible thing
 * here twice, and the second fix is this rule.
 *
 * It was five groups and 4,865 until 2026-09-11, when the labels became
 * `Principles` / `Primitives` / `Components` and two groups were dropped
 * entirely (see NAV in tools/render-html.py). The strings quoted below are
 * those FORMER labels -- they are the evidence for this rule, not live copy.
 *
 * FIRST it was 13px mono uppercase tracked 0.1em in `--content-subtle` (5.33:1).
 * That cleared AA and was still the hardest string on the page to read, because
 * 4.5:1 is calibrated for normal-weight body text and nothing in
 * check-contrast.py can see size, weight or case. Moving it one rung brighter
 * fixed the contrast and left the scanning problem exactly where it was.
 *
 * ⚠️ THE SCANNING PROBLEM WAS THE UPPERCASE MONO, and three things say so:
 *
 *   1. MONO IS A LIE HERE. The strongest signal on this site is that mono means
 *      a machine derived it and sans means a person wrote it (see the font
 *      block above). Every string wearing this role is editorial prose a person
 *      typed -- "Choosing a control", "What is missing or inconsistent",
 *      "Gaps on this page". Rendering them as machine output is the one thing
 *      the typeface split exists to prevent.
 *   2. ⚠️ THE LABELS WERE SENTENCES WHEN THIS WAS WRITTEN, AND ARE NOT ANY
 *      MORE. The NAV table read "Choosing a control" / "What is missing or
 *      inconsistent"; measured 2026-09-11 against the shipped Roboto Mono at
 *      wght 500, the longest set **282px wide in a 216px rail column**, so the
 *      rail wrapped its own group heading. The table was rewritten to
 *      "Principles" / "Primitives" / "Components" the same day, and at those
 *      lengths nothing wraps either way -- the widest is 91px of 216.
 *
 *      **So this leg no longer carries weight, and is kept because it says
 *      what the treatment costs per character rather than what it cost once.**
 *      Uppercase plus 0.1em tracking is ~55% wider than sentence-case sans at
 *      the same size; that is affordable on `PRIMITIVES` and was not on a
 *      31-character clause. The other two legs do not depend on label length.
 *   3. IT WAS 1.4px FROM THE THING IT LABELS. The rail's links rendered at
 *      14.4px (`nav { font-size: 0.9em }`, since deleted -- see structure.css
 *      in render-html.py), so case and typeface were carrying the whole
 *      distinction on their own.
 *
 * The replacement is the shape Pajamas uses and Primer nearly does: ONE RUNG
 * DOWN the ladder from the text it heads, ONE RUNG UP in weight. Links are Body
 * at 400, this is Caption at 600. Nord's smaller-and-fainter was considered and
 * rejected -- fainter is what produced the first failure above.
 *
 * ⚠️ IT TAKES `--content-default`, AND THE FIRST ATTEMPT DID NOT. Shipped at
 * `--content-secondary` on 2026-09-11 and rejected the same day -- Joseph,
 * looking at it in dark mode: *"the headers look like jacked-up mismatched
 * text, not clear markers."* Weight alone cannot do this job, and the reason is
 * measurable rather than a matter of taste.
 *
 * Measured against the shipped Roboto, stem thickness of `l`:
 *
 *     link   Body 16px @ 400     1.453px
 *     label  Cap  13px @ 600     1.685px     +0.232px
 *     label  Cap  13px @ 700     1.834px     +0.381px
 *
 * **The whole weight signal was a quarter of a pixel**, and the two axes partly
 * CANCEL: Caption is a step down in size, which reads as less important, while
 * weight is a step up. A heavier weight cannot rescue it either -- 600 -> 700
 * buys 0.150px and every step above it buys the same, so even wght 900 is only
 * 0.69px clear of the links. Dark mode is where it fails first because halation
 * thickens every light glyph on a dark ground by roughly a constant, which adds
 * to both stems and compresses the ratio between them.
 *
 * Colour is the only axis with room left, and only one move on it is big
 * enough. The content ladder separates like this:
 *
 *     default  vs secondary    2.20:1     a step a reader sees
 *     secondary vs subtle      1.45:1     the same failure again
 *
 * ⚠️ THIS DOES NOT SPEND THE POSITION CUE'S COLOUR, and the earlier version of
 * this comment claimed it did. The nav block below argues that default links
 * must be muted *"or the two position cues have nothing to be brighter than"* --
 * that is an argument about LINKS. A group heading is not a link: it has no
 * hover, no underline and no target, and every link in the rail is still one
 * full rung below both cues. What this DOES collide with is
 * `nav a[data-section="current"]`, which is colour-only and now shares its
 * colour with the heading directly above it. See the note on that rule. */
/* ⚠️ AMENDED A THIRD TIME, 2026-09-14, AND THIS ONE MOVES THE COLOUR BACK TO
 * `--content-secondary` -- the value rejected on sight on 2026-09-11. Read the
 * paragraphs above as the record of why that failed, not as a description of
 * this rule, because the thing that changed is not the colour. It is that
 * colour is no longer the only axis carrying the label.
 *
 * Jieun's rail, 2026-09-14, proposed three changes at once, and they are one
 * change: secondary colour, UPPERCASE, and a rule above each group. The
 * rejection above stands on a measurement -- at Caption size the whole 400->600
 * weight signal is 0.232px of stem, and dark-mode halation compresses it
 * further -- and that measurement was taken with case and rules OFF THE TABLE.
 * It says weight cannot do this job alone. It does not say colour is the only
 * axis that can.
 *
 * So the label now separates from the links under it on FOUR axes at once:
 * one rung down in size, one rung up in weight, uppercase, and a
 * `--ds-rule-strong` hairline above it (structure.css, wide rail only). Colour
 * is the axis given back, and it is given back to the links: at
 * `--content-default` the group heading was the brightest thing in the rail,
 * competing with the two position cues that are the only things in there a
 * reader actually needs to find.
 *
 * ⚠️ UPPERCASE IS WHAT FAILED THE FIRST TIME AND THIS IS NOT THAT. The first
 * treatment was 13px MONO uppercase tracked 0.1em in `--content-subtle`, and
 * the three legs of its rejection are above. Two are already retired by this
 * file's own account: the labels were 31-character clauses then and are one
 * word now, and the rail's `font-size: 0.9em` that put the label 1.4px from its
 * links is deleted. The surviving leg is that MONO ON THIS SITE MEANS A MACHINE
 * DERIVED IT, and it does not apply -- this role is sans and stays sans. Case
 * is not the thing that lied; the typeface was.
 *
 * ⚠️ WHAT THIS COSTS, PLAINLY. The label is now the same colour as the links it
 * heads, and it is smaller than them. "Smaller and fainter" is Nord's treatment
 * and is what produced the FIRST failure here. The bet is that uppercase plus a
 * rule is worth more than the 2.20:1 brightness step it spends, and that bet is
 * a judgement rather than a measurement -- both previous failures were caught by
 * eye in dark mode, not by a check, and nothing here can catch the third.
 * **Look at the rail before trusting this rule.** Joseph, 2026-09-14, agreeing
 * to try it and not to it: *"same color is hard to scan and we're patching that
 * by significantly reducing information density to add whitespace."*
 *
 * Tracking is `--ds-track-head`, which already exists and already means "every
 * uppercase label". Its comment says *uppercase mono*; this is the first sans
 * wearer and the comment is corrected there rather than a second value minted. */
[data-role="eyebrow"] {
  font-family: var(--ds-font-body);
  font-size: var(--ds-caption); line-height: var(--ds-lh-caption);
  letter-spacing: var(--ds-track-head);
  font-weight: var(--ds-weight-bold);
  color: var(--content-secondary);
  text-transform: uppercase;
}

/* ---- prose -------------------------------------------------------------- */
p, li { color: var(--content-default); }
strong { color: var(--content-default); font-weight: var(--ds-weight-bold); }
em { color: var(--content-secondary); }

/* An inline link in prose has normal / hover / active / visited. */
a { color: var(--action-rest); text-decoration-thickness: 1px;
    text-underline-offset: 0.15em; }
a:hover { color: var(--action-hover); }
/* Pressed is a NON-COLOUR change, deliberately: its whole job is to confirm
 * that a click registered, and a hue shift competes with hover for the same
 * signal. */
a:active { opacity: var(--action-pressed-opacity); }

/* ⚠️ VISITED: DECIDED, NOT INHERITED — and the decision is no treatment.
 * On a reference site "have I read this?" is normally worth having, so this is
 * a measurement rather than a shrug. Across the 972 built pages there are
 * 2,984 in-content links pointing at just **188 distinct targets**, and the
 * top six account for 80% of them — `/llms.txt` and `/data/` alone appear on
 * every single page. Two page visits would therefore mark roughly two-thirds
 * of the links on every subsequent page as visited. That is the same decay
 * that makes visited meaningless in the menu, only faster: it would cost a
 * colour and tell the reader nothing. Re-open it if the link graph ever gets
 * wider than it is deep. */

/* No focus treatment came back with the identity, and a docs site is read by
 * keyboard. This is ours, and it is deliberately loud.
 *
 * ⚠️ NOT the link hue. A focus ring that matches the hover colour is not a
 * focus ring — it says "you are pointing at this" when it means "your keyboard
 * is here". It was a third consumer of the deleted cyan; it is now the
 * brightest neutral on the site, which nothing else uses as an outline. */
:focus-visible { outline: 2px solid var(--content-default); outline-offset: 2px;
                 border-radius: 2px; }

/* Reads the token rather than restating its channels. It was
 * `rgba(77, 216, 238, .25)` -- `--ds-link` hardcoded in decimal, so a change to
 * the hue silently left the selection behind. */
::selection { background: color-mix(in srgb, var(--action-hover) 25%, transparent);
              color: var(--content-default); }

/* ---- inline code -------------------------------------------------------- */
/* The most repeated element in the whole design. `overflow-wrap` matters:
 * token names and module paths are long, and a table cell must never push the
 * page sideways. */
code {
  background: var(--ds-fill-subtle);
  border-radius: var(--ds-radius-chip);
  padding: 1px 5px;
  color: var(--content-default);
  overflow-wrap: anywhere;
}
pre {
  background: var(--ds-paper);
  border: 1px solid var(--ds-edge);
  border-radius: 8px;
  color: var(--content-default);
}
pre code { background: none; padding: 0; }

/* ---- tables ------------------------------------------------------------- */
/* Most of this site is tables, and most of a table is derived values, so the
 * body is mono and muted and the identifiers inside it are what light up. */
/* Body, not Caption. A table cell is the answer the reader came for; it was
 * 12px because tables feel like they want to be dense, and that is what made
 * the site read as fine print. */
table { font-size: var(--ds-body); }
th, td { border-bottom: 1px solid var(--ds-rule); }
thead th {
  border-bottom: 1px solid var(--ds-rule-strong);
  border-bottom-width: 1px;
  font-family: var(--ds-font-mono);
  font-size: var(--ds-caption);
  font-weight: var(--ds-weight-medium);
  text-transform: uppercase;
  letter-spacing: var(--ds-track-head);
  color: var(--content-secondary);
}
td { font-family: var(--ds-font-mono); color: var(--content-secondary); }
td code { font-size: 1em; }
td em { color: var(--content-subtle); font-style: normal; }

/* ---- rules and quotes ---------------------------------------------------- */
hr { border-top: 1px solid var(--ds-rule-strong); opacity: 1; }
blockquote { border-left: 3px solid var(--ds-rule-strong); color: var(--content-secondary); }

/* ---- authored pictures --------------------------------------------------- */
/* A screenshot written into a content file with `![…](…)`. Containment is in
 * structure.css; this is the ground it is shown against.
 *
 * ⚠️ PAINTED LIGHT, ON A DARK SITE, FOR THE REASON ALREADY ARGUED IN blocks.css
 * §4 — and here the argument is not a preference, it is measured. Every one of
 * the 123 images in the 2026-09-15 scrape is fully opaque light-mode product UI:
 * `01-image.png` in the Map legend reads (255,255,255,255) at every corner, with
 * dark navy label text baked into the pixels. So the white is arriving whether we
 * paint it or not. Dropping that onto `--ds-ground` gives a white rectangle
 * floating in near-black with a torn edge where the capture stops, which is
 * exactly what the live-preview frame was written to avoid. Stating the ground
 * means the edge is ours and the corner is rounded.
 *
 * No `--ds-surface-card` and no gradient, same as those frames: a card means
 * INTERRUPTION on this site, and a picture in a page of tables already
 * interrupts by being a picture.
 *
 * The padding is what makes the light ground visible around a capture whose own
 * background is white -- without it the border sits directly on the artwork and
 * the frame reads as part of the screenshot.
 *
 * ⚠️ KEYED ON THE ROUTE, AND THE ROUTE IS THE CARVE-OUT. Everything above
 * argues from a property of a CAPTURE: it arrives opaque and light whether we
 * paint it or not, so the frame makes the edge ours. A DRAWING is the other
 * case -- we made it, on a transparent ground, against this site's own palette
 * -- and the frame is not merely unnecessary there, it destroys the artwork.
 * Measured 2026-09-15 with rsvg-convert at both grounds: home-hero.svg letters
 * "The … design system." in #E9EDF3, which reads clearly on --ds-ground and is
 * very nearly invisible on #ffffff.
 *
 * So `site-art/screens/` is framed and `site-art/illustrations/` is not, and
 * the picture says which it is by where it lives:
 *
 *     ![…](/assets/screens/patterns/map/legend-service@2x.png)   framed
 *     ![…](/assets/illustrations/home-hero.svg)                  bare
 *
 * ⚠️ IT WAS KEYED ON `.png` FOR ABOUT AN HOUR, AND THAT WAS WRONG IN A WAY
 * WORTH RECORDING. Format is a property of the file, not a statement by the
 * author: the first SVG diagram of ours that wants a light ground, or the first
 * screenshot somebody exports as a PNG of a dark-mode screen, lands on the wrong
 * side silently. The directory is a decision somebody took and can be read off
 * the authored line in the content file, which is where the coupling belongs.
 *
 * The precedent is `tools/check-images.py`, whose contact-sheet exemption is
 * "BY ROUTE PREFIX, `/assets/figma/`, AND NOT BY 'it is generated'" for exactly
 * this reason. Same shape, same argument.
 *
 * ⚠️ AND THE ALTERNATIVE IS THE BRITTLE ONE. Joseph, 2026-09-15, on painting the
 * ground into the drawing instead: *"even if i set the bg color by hand (which
 * is brittle, because our page bg color could shift)"*. He is right and it is
 * worse than it looks -- an SVG carrying a baked #14161a would not merely go
 * stale when --ds-ground moves, it would go stale invisibly, as a rectangle
 * slightly off the page it sits on. A drawing on a transparent ground cannot
 * drift, because it states no ground at all. */
img.shot[src^="/assets/screens/"] {
  background: #ffffff;
  border: 1px solid var(--ds-edge);
  border-radius: var(--ds-radius-card);
  padding: 0.4em;
}

/* ---- authored cards ------------------------------------------------------ */
/* The `:::card{kind=…}` fence -- project/plans/done/build-the-card-fence.md. The
 * second authored-prose device, and THE FIRST CONSUMER `--ds-surface-card` has
 * ever had: the elevation recipe was defined at the top of this file on
 * 2026-09-10 and sat unused for five days, because every block in the vocabulary
 * is a table and a table is the document rather than an interruption in it.
 * Containment is in structure.css; this is what the surface looks like.
 *
 * ⚠️ WHAT A CARD IS FOR, because it governs every line below. Joseph,
 * 2026-09-15: a card is DECORATION. It focuses a human reader's attention and
 * that is its whole job. It carries no meaning, and nothing an agent needs may
 * live in the fact that a passage is fenced -- strip the fence, the kind and the
 * heading, and the prose must still say what the author meant. So this file may
 * make a card visible and must never make it mean anything.
 *
 * ⚠️ THE CARD IS SQUARE, and that is the house rule from 2026-09-11 rather than
 * a taste call here: a rounded box with a colour bar down one side is banned --
 * pick a colour on all four sides, or pick no radius. This stage has no colour,
 * so it has no radius question, and `--ds-radius-card` keeps exactly the two
 * consumers it already had (the live-preview figure, and `img.shot` above).
 * Do NOT port the 2026-09-14 specimen's card recipe: its gradient, 16-18px
 * radius and 48px drop shadow are not adopted.
 *
 * `--ds-shadow-card` is taken whole rather than split. The inset hairline is the
 * half that does the work -- it reads as a light source above the page, which is
 * what stops a dark surface looking like a hole -- and the drop half is what
 * separates one card from the next in a run without drawing a rule between them.
 *
 * ⚠️ `[data-card]` IS A HOOK WITH NOTHING HANGING OFF IT, deliberately, AND ALL
 * FIVE KINDS LOOK IDENTICAL. Colour is held -- the palette is allocated by
 * question and check-contrast.py's MIN_DISTANCE of 40 leaves no room -- and the
 * icons are deferred (Joseph, 2026-09-16). So there is nothing for the kind to
 * render AS, and nothing here pretends otherwise.
 *
 * That is survivable for `prefer`, `avoid` and `caution` because the polarity is
 * in the HEADING TEXT, where a reader scanning the page meets it in words whether
 * or not the box is ever tinted. A card that needed its tint to be understood
 * would be a card carrying meaning, which is the one thing a card may not do.
 *
 * ⚠️ NOTHING ENFORCES THAT, and an earlier version of this comment said a
 * checker did. `tools/check-cards.py` had a rule refusing an `avoid` heading that
 * did not lead with "Don't", "Never" or "Avoid"; it was removed on 2026-09-16 as
 * a magic-word requirement that fails builds nobody can debug. So the heading is
 * still where the polarity belongs, and it is now advice in content/README.md
 * rather than a guarantee this file may lean on.
 *
 * When the kind does get a visible channel it must be a WORD beside any glyph,
 * never the glyph alone: content/writing/accessibility.md says so in as many
 * words, and the shipped `✅ Prefer | ❌ Avoid` table headers are the precedent
 * to copy -- the glyph decorates and the word carries. */
.card {
  background: var(--ds-surface-card);
  border: 1px solid var(--ds-edge);
  box-shadow: var(--ds-shadow-card);
  padding: 1.25em 1.5em;
}

/* ---- orientation: DELETED 2026-09-11 -------------------------------------- */
/* The aside above the facts on every page. It was folded into the class
 * templates on 2026-09-11 (see the note where `load_copy()` used to be in
 * render-html.py) and nothing has emitted `[data-orientation]` since; the only
 * file in the whole build still carrying the string was this one. Six rules
 * matching zero elements, kept alive by nothing but their own comments.
 *
 * Two of them are worth naming, because both are defects this file argues
 * against elsewhere and both would have been copied forward by anyone reviving
 * the block:
 *
 *   `[data-orientation] > p:first-child` promoted a paragraph for being FIRST.
 *   Same species as the lede rule deleted from blocks.css the same day — the
 *   source is markdown, which already has `##` for a reader who wants emphasis.
 *
 *   `[data-orientation] summary` was the fifth hand-written copy of mono +
 *   uppercase + Caption + tracking, and the sole consumer of --ds-track-caps.
 *   It also matched no element even before the fold: the site emits zero
 *   <summary>. Deleting it retires the token, which is why the note on
 *   --ds-track-head above no longer has a second tracking value to reconcile. */


/* ---- nav ----------------------------------------------------------------- */
/* The rail mirrored the body's wash with `background-attachment: fixed` so the
 * seam between chrome and content disappeared at every scroll position. With
 * the wash gone (B4) there is no seam to hide: structure.css's flat
 * `--ds-ground` now matches the body exactly, so this override is deleted
 * rather than replaced. A fix and the thing it fixed, removed together. */
nav { color: var(--content-secondary); }
/* ⚠️ THE RAIL'S LIST ITEMS HAVE TO SAY THIS AGAIN, and the reason is a cascade
 * trap rather than a second opinion. `p, li { color: var(--content-default) }`
 * below is a PROSE rule, and it paints every `<li>` DIRECTLY -- including the
 * rail's, because the rail is a `<nav><ul><li>`. A directly-applied colour beats
 * an inherited one at any specificity, so the `nav` rule above never reached a
 * list item and the two rules were not in conflict; the prose one simply won by
 * being on the element.
 *
 * Links never showed it, because `nav a` sets its own colour one line down. The
 * one thing in the rail that does NOT is a container's `<summary>`, which had
 * no rest-state rule at all -- only hover and the section cue were ever written.
 * So every expander label rendered at `--content-default`: the brightest step on
 * the site, on the one item in the list that is not even a link, sitting directly
 * under a group label that gave up that same step on 2026-09-14. Reported by
 * Joseph the same day: *"expander-rows are still using the primary text color."*
 *
 * ⚠️ FIXED ON `nav li`, NOT ON `nav summary`, deliberately. Scoping it to the
 * summary would repair the one instance and leave the mechanism loaded for the
 * next thing put in a rail list item that is not an `<a>`. The honest statement
 * is that the rail is chrome and prose rules do not reach it. */
nav li { color: var(--content-secondary); }
/* The strip is sticky and now WRAPS rather than scrolls, so at 380px it is four
 * lines of chrome with page content passing underneath it. Without an edge
 * there is nothing marking where the chrome stops and the document starts, and
 * a reader watching text disappear into an invisible boundary reads it as a
 * rendering fault. The rail gets the same hairline on its right, below. */
nav { border-bottom: 1px solid var(--ds-rule-strong); }
/* ⚠️ `nav h2` USED TO BE STYLED HERE AND IN structure.css AT THE SAME TIME --
 * same specificity, so only the order of four <link> tags decided which won.
 * Both are gone: a nav group label is an `eyebrow`, structure.css keeps nothing
 * but its `display`, and there is one rule for it in the whole site. */
/* Default links are MUTED, not the brightest step. The rail is on all 972
 * pages and is read
 * past far more often than it is read, so it recedes — and it has to, or the
 * two position cues below have nothing to be brighter than. */
nav a { color: var(--content-secondary); text-decoration: none; }
nav a:hover { color: var(--action-hover); text-decoration: underline; }

/* A container's label is a `<summary>`, not a link, and the treatment has to say
 * so. It takes the same muted default as a link -- it is the same kind of thing
 * in the same list -- but it does NOT take the hover underline, because an
 * underline is the one mark on this site that means "this navigates" and a
 * summary does not. It opens in place. Colour alone carries the hover.
 *
 * Nothing here sets its size or family: a summary is not a heading, so it is not
 * a `role`, and the rail's links are Body. The label and the pages under it are
 * the same rung on purpose -- the indent and the marker say which is which. */
nav summary:hover { color: var(--action-hover); }
nav summary:hover::before { color: var(--action-hover); }
/* You are somewhere inside this container. Same hue as the section cue on a
 * link, because it is the same statement -- and, like that one, it is colour
 * with no shape behind it. That is the open item in todo.md and this adds one
 * more place it applies rather than settling it. The expander being OPEN is a
 * second, structural signal that a link's section cue has never had, so this is
 * the one instance of that state that does not rest on hue alone. */
nav summary[data-section="current"] { color: var(--action-selected); }

/* ---- where you are ------------------------------------------------------- */
/* Two cues, and every page carries exactly one — an exact match suppresses the
 * section highlight, so they can never both fire. Section ownership is declared
 * in the NAV table rather than derived from the path, which is what lets a
 * construct page under /c/ light up "Full list".
 *
 * This is most of what a persistent rail is FOR. Before these, 962 of 972 pages
 * had no cue at all: the rail was on screen the whole time and never said where
 * you were, which is a rail with the navigation removed. */

/* You are somewhere inside this entry's section. Promoted to the brightest step:
 * present, but
 * not claiming to be the page you are on.
 *
 * ⚠️ THIS IS THE ONE CUE ON THE SITE CARRIED BY COLOUR ALONE, and the move to
 * `--action-selected` cuts both ways for it. It gets its colour back: for a few
 * hours on 2026-09-11 this and the `eyebrow` heading two lines above it were
 * both `--content-default`, so the group label and the entry you are inside
 * were the same colour and the cue was competing with a heading. That is gone.
 * What it pays is brightness -- the step over a `--content-secondary` link was
 * 2.20:1 and is now 1.54:1, with a hue making up the difference for readers who
 * can see it and nothing making it up for readers who cannot.
 *
 * `selected` can afford that because it has a structural mark. THIS STATE HAS
 * NONE, which is exactly the open item in todo.md: the cue that carries a shape
 * is on 11 pages and the colour-only one is on 962. That was already the
 * argument; this change makes it stronger and does not settle it. */
nav a[data-section="current"] { color: var(--action-selected); }

/* You are on exactly this page — SELECTED, which is a menu state and not "a
 * stronger hover". It is a different axis: where you ARE, not what you would
 * DO. So it does not take the interaction hue; it takes the brightest content
 * step plus
 * the structural mark below, and it is the only thing on the page wearing that
 * mark. That is what makes hovering the current page finally do something. */
nav a[aria-current="page"] {
  color: var(--action-selected);
  font-weight: var(--ds-weight-medium);
}

/* Hover is the one menu state that owns the hue, and it must beat `selected`
 * in the cascade or the current item is the one link that cannot be hovered. */
nav a:hover, nav a[aria-current="page"]:hover,
nav a[data-section="current"]:hover { color: var(--action-hover); }
nav a:active { opacity: var(--action-pressed-opacity); }

/* ⚠️ NO `:visited` in the menu, and this one is structural rather than
 * measured: every item in a ten-item menu is visited within a session or two,
 * so the style decays to "everything is grey" and stops carrying information.
 * A menu item has normal / hover / active / SELECTED; an anchor has normal /
 * hover / active / VISITED. Two models, and the menu simply lacks that state.
 *
 * The MARKUP stays anchors regardless. `role="menu"` / `role="menuitem"` are
 * for application menus with arrow-key navigation; applying them to site
 * navigation is a known ARIA anti-pattern that costs open-in-new-tab and
 * copy-address and misreports the nav to a screen reader. `<nav><ul><li><a>`
 * plus `aria-current="page"` already IS "menu item, selected". */

/* ---- scrollbars ----------------------------------------------------------- */
/* WebKit only, and purely so a pale default bar does not sit against the dark
 * ground. Firefox keeps its own, which is fine. */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: #23272f; border-radius: 6px; }
::-webkit-scrollbar-track { background: transparent; }

/* The cue needs a SHAPE as well as a colour — colour alone is not a position
 * indicator for a reader who cannot separate cyan from grey. Its form differs
 * per rail state, because the rail's own axis differs: a horizontal strip
 * marks the current item underneath it, a vertical rail marks it alongside. */

/* NARROW — the sticky strip. Underline, matching the strip's axis. */
nav a[aria-current="page"] {
  border-bottom: 2px solid var(--action-selected);
  padding-bottom: 2px;
}

@media (min-width: 60rem) and (min-height: 36rem) {
  /* WIDE — the persistent rail. The underline becomes a bar in the margin, so
   * the cue reads down the rail's own edge rather than across the link. */
  nav a[aria-current="page"] { border-bottom: 0; padding-bottom: 0; }
  nav li:has(> a[aria-current="page"]) { position: relative; }
  nav li:has(> a[aria-current="page"])::before {
    content: "";
    position: absolute;
    left: -0.75rem;
    top: 0.2em;
    height: 1.1em;
    width: 2px;
    border-radius: 1px;
    background: var(--action-selected);
  }
  /* The rail is chrome and the content is not: one hairline says which is
   * which. `--ds-rule-strong`, not `--ds-rule` — the token block above defines
   * the strong weight as the one that separates a section from the next, and
   * chrome from document is the largest such division on the page. (This read
   * `--ds-rule` first, with a comment arguing the opposite of what the token
   * doc says. The hairline was then invisible at 380px, which is what a rule
   * used one weight below its job looks like.) */
  nav { border-right: 1px solid var(--ds-rule-strong); border-bottom: 0; }
}

/* The skip link is the first focusable thing on the page and is seen only by
 * keyboard readers, so it is loud on purpose. */
.skip:focus {
  background: var(--ds-paper);
  color: var(--content-default);
  border-radius: var(--ds-radius-chip);
  outline: 2px solid var(--content-default);
  font-family: var(--ds-font-mono);
  font-size: var(--ds-body);
}
