@charset "utf-8";
/* ==========================================================================
   KeyboardLatencyTest.com — design-system.css
   The single stylesheet every page loads. Tool pages may add a tiny tool.css
   for genuinely page-specific bits, but must reuse the classes defined here.

   Aesthetic target: lab instrument / oscilloscope. Crisp 1px rules, restrained
   accent, generous whitespace, data forward. Dark is the default; light is a
   separately tuned palette (not an inversion). Every text/background pair used
   for real copy was checked against WCAG AA (4.5:1 body, 3:1 large/UI).

   Section index
     01 reset
     02 tokens (dark default + light theme)
     03 base elements & typography
     04 layout: .wrap .prose
     05 a11y utilities: .sr-only .skip-link, focus ring
     06 header, nav, drawer, dropdowns
     07 footer
     08 hero, crumbs
     09 card
     10 buttons  (10.1 segmented control)
     11 metrics
     12 stage
     13 chart    (13.1 progress bar)
     14 keyboard map
     15 tables
     16 notes / disclose / faq
     17 toolgrid
     18 toast
     19 forms
     20 utilities
     21 responsive breakpoints (480/768/1024/1280/1440) + touch-target policy
     22 forced colors (Windows High Contrast)
     23 reduced motion
     24 print
   ========================================================================== */

/* ==========================================================================
   01 — RESET
   ========================================================================== */

*,
*::before,
*::after { box-sizing: border-box; }

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* Native form controls, scrollbars and <dialog> backdrops follow the theme. */
  color-scheme: dark;
}

body {
  min-block-size: 100vh;
  min-block-size: 100svh;
  /* Safety net only — the layout is built so nothing ever exceeds the viewport.
     `clip` (not `hidden`) so position:sticky keeps working. */
  overflow-x: clip;
}

/* QA ONLY — never set in production markup.
   `overflow-x: clip` removes the scrollable overflow region entirely, so
   documentElement.scrollWidth reports clean whether or not a child overflows.
   That means the standard overflow test cannot detect the bug it exists to
   detect: a regression presents as "the right edge of my table is missing",
   with no scrollbar and no failing test.
   qa-static.js sets html[data-qa="overflow"] before measuring; with clipping
   off, scrollWidth > clientWidth is once again a real signal. */
html[data-qa="overflow"] body { overflow-x: visible; }

img,
svg,
video,
picture { display: block; max-inline-size: 100%; block-size: auto; }

/* Canvas keeps its own intrinsic sizing: charts set the backing store from the
   CSS box, so we must not force block-size:auto here. */
canvas { display: block; max-inline-size: 100%; }

input,
button,
textarea,
select { font: inherit; color: inherit; letter-spacing: inherit; }

button { background: none; border: 0; }

p, h1, h2, h3, h4, h5, h6, li, dt, dd, figcaption {
  overflow-wrap: break-word;
}

/* Long unbroken strings (key codes, URLs) must not push the page sideways. */
code, kbd, samp, pre { overflow-wrap: anywhere; }

ul[role="list"],
ol[role="list"] { list-style: none; padding: 0; }

/* ==========================================================================
   02 — TOKENS
   ========================================================================== */

:root {
  /* surface */
  --bg: #0a0c10;
  --bg-2: #11141b;
  --surface: #161a23;
  --surface-2: #1d222d;
  --border: #262d3a;
  --border-strong: #38414f;

  /* text */
  --text: #e8ecf3;
  --text-2: #a8b3c5;
  /* Lifted from the spec's #6f7b8f: that value is real text (chart axis ticks,
     ::placeholder, dead keycap legends) and only reached 4.07:1 on --surface and
     3.72:1 on --surface-2. #808c9f clears 4.5:1 on every surface in this theme. */
  --text-3: #808c9f;

  /* brand: signal cyan + amber accent */
  --accent: #22d3ee;
  --accent-2: #0891b2;
  --accent-ink: #052e35;
  --amber: #fbbf24;
  --good: #34d399;
  --warn: #fbbf24;
  --bad: #f87171;

  /* chart series (colour-blind safe, used in this order) */
  --c1: #22d3ee;
  --c2: #a78bfa;
  --c3: #fbbf24;
  --c4: #34d399;
  --c5: #f472b6;
  /* Series 6-8 exist for the one graphic that needs eight fills (the latency
     calculator's stacked contribution bar). They live here, not in that page's
     tool.css, so the next graphic that needs a sixth series finds them.
     --c7 / --c8 are a deliberate light/dark pair of the same hue: they are
     ADJACENT bands in that bar, separated only by a 1px rule, so they are
     picked for a 3.07:1 luminance ratio against each other (both also clear
     3.3:1 against --surface). A hue-only pair reads as one band. */
  --c6: #94a3b8;
  --c7: #fdba74;
  --c8: #c2410c;

  /* geometry */
  --r-sm: 6px;
  --r: 10px;
  --r-lg: 16px;
  --space: clamp(1rem, 2.5vw, 1.5rem);
  --maxw: 1200px;
  /* Instrument-width escape hatch. Prose, FAQs and metric grids keep --maxw;
     a section holding a full-size keyboard map (22.5u + chrome = up to 1188px)
     opts into .wrap--wide so it stops overflowing its own scroller above
     ~1441px while 360px of gutter sits empty either side. */
  --maxw-wide: 1400px;
  /* 68ch measured 85 rendered characters per line in this font (the `ch` unit
     is the advance of "0" = 8.63px, the average glyph here is ~6.9px). 60ch
     lands at ~75, the top of the comfortable range. */
  --maxw-prose: 60ch;

  /* Spacing scale. --space and --gutter stay the fluid LAYOUT values; these are
     the fixed steps for component padding, margins and gaps so a value can be
     reviewed against something. Optical values on sub-pixel objects (1-2px
     carets, negative thumb offsets) are exempt. */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.5rem;
  --s-6: 2rem;
  --s-7: 3rem;

  /* Display type scale for measurement readouts — "the answer" on a tool page.
     Every tool must pick one of these rather than inventing a clamp curve, so
     the result number is the same size on every page at every viewport. */
  --fs-readout-lg: clamp(2.25rem, 1.4rem + 5.5vw, 4.5rem);   /* .stage__big */
  --fs-readout: clamp(2rem, 1.4rem + 3.2vw, 3.25rem);        /* .metric--lead */
  --fs-readout-sm: clamp(1.25rem, 1rem + 1.2vw, 1.75rem);    /* inline/among prose */
  --fs-subhead: clamp(1.05rem, 0.95rem + 0.8vw, 1.35rem);    /* section subtitle */

  /* type */
  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  /* ---- derived tokens (not in the spec table, used by this stylesheet) ---- */

  /* Ink for text placed on a filled --good/--warn/--bad/--amber block. Flips
     with the theme because those swatches are light in dark mode and dark in
     light mode. */
  --on-solid: #0a0c10;

  /* Boundary colour for interactive controls (buttons, inputs, keycaps).
     --border-strong is only 1.9:1 on --bg, which fails WCAG 1.4.11 when the
     edge is what identifies the control, so controls use this instead:
     3.75:1 on --bg, 3.30:1 on --surface, 3.05:1 on --surface-2. */
  --border-ui: #626d80;

  --gutter: clamp(1rem, 4vw, 2rem);
  --header-h: 56px;
  --shadow: 0 1px 2px rgb(0 0 0 / .55), 0 10px 28px rgb(0 0 0 / .38);
  --shadow-sm: 0 1px 2px rgb(0 0 0 / .45);
  --ring: 0 0 0 3px color-mix(in srgb, var(--accent) 32%, transparent);

  /* Faint graticule used on the test stage and chart frames. */
  --grid-line: color-mix(in srgb, var(--border) 70%, transparent);

  /* One keyboard unit + the gutter that is subtracted from every key box, so a
     1.25u key plus its gap spans exactly 1.25 columns. The floor is set once,
     in §14, and this fallback mirrors it — see the reasoning there. */
  --u: clamp(40px, 3.4vw, 52px);
  --kbd-gap: 4px;

  /* Read by site.js so the JS nav mode never disagrees with the CSS. */
  --nav-mode: drawer;
}

html[data-theme="light"] {
  color-scheme: light;

  /* surface — cool paper, white panels so data sits on the brightest plane */
  --bg: #f7f9fc;
  --bg-2: #eef2f8;
  --surface: #ffffff;
  --surface-2: #f1f5fa;
  --border: #dbe1ea;
  --border-strong: #7c8899;   /* 3.6:1 on white — valid UI boundary */
  --border-ui: #6b7686;       /* 4.6:1 on white, 4.2:1 on --surface-2 */

  /* text — near-black slate, not pure black */
  --text: #0d1420;            /* 17.5:1 on --bg */
  --text-2: #48566b;          /* 7.1:1  on --bg */
  --text-3: #616c7d;          /* 5.0:1  on --bg */

  /* brand — cyan has to darken hard to stay legible on paper */
  --accent: #0e7490;          /* 5.1:1 on --bg, 5.4:1 on white */
  --accent-2: #155e75;        /* hover/pressed, 7.3:1 on white */
  --accent-ink: #ecfeff;      /* 5.1:1 on --accent */
  --amber: #b45309;           /* 5.0:1 on white */
  --good: #047857;            /* 5.5:1 on white */
  --warn: #b45309;
  --bad: #be123c;             /* 6.3:1 on white */

  --c1: #0e7490;
  --c2: #6d28d9;
  --c3: #b45309;
  --c4: #047857;
  --c5: #be185d;
  /* --c6 is a slate that is NOT --text-2 (#48566b). The previous page-local
     value was #475569, a 1.017:1 duplicate of --text-2 — the same colour to any
     eye. #64748b is 4.76:1 on white and 1.57:1 from --text-2, so it reads as a
     series colour rather than as body text. --c7/--c8 keep the 3:1 adjacency
     rule: 3.56:1 from each other, 3.56:1 and 12.69:1 on --surface. */
  --c6: #64748b;
  --c7: #ea580c;
  --c8: #5c1f09;

  --on-solid: #ffffff;
  --shadow: 0 1px 2px rgb(15 23 42 / .07), 0 10px 28px rgb(15 23 42 / .09);
  --shadow-sm: 0 1px 2px rgb(15 23 42 / .08);
  --grid-line: color-mix(in srgb, var(--border) 85%, transparent);
}

/* ==========================================================================
   03 — BASE ELEMENTS & TYPOGRAPHY
   ========================================================================== */

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 1rem;
  line-height: 1.65;
  display: flex;
  flex-direction: column;
}

body > main { flex: 1 0 auto; }

h1, h2, h3, h4, h5, h6 {
  line-height: 1.2;
  font-weight: 650;
  letter-spacing: -0.011em;
  text-wrap: balance;
}

h1 { font-size: clamp(1.75rem, 1.18rem + 2.6vw, 2.75rem); letter-spacing: -0.022em; }
h2 { font-size: clamp(1.375rem, 1.12rem + 1.3vw, 1.875rem); letter-spacing: -0.016em; }
h3 { font-size: clamp(1.125rem, 1.02rem + 0.55vw, 1.375rem); }
h4 { font-size: 1.0625rem; }
h5, h6 { font-size: 0.9375rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-2); }

a { color: var(--accent); text-underline-offset: 0.18em; text-decoration-thickness: from-font; }
a:hover { color: var(--accent-2); }
html[data-theme="light"] a:hover { color: var(--accent-2); }

hr {
  border: 0;
  border-block-start: 1px solid var(--border);
  margin-block: var(--space);
}

small { font-size: 0.8125rem; }

strong, b { font-weight: 650; }

code, kbd, samp, pre {
  font-family: var(--mono);
  font-size: 0.9em;
}

code:not(pre code) {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 0.1em 0.36em;
}

pre {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 0.875rem 1rem;
  overflow-x: auto;
  line-height: 1.5;
}

kbd {
  display: inline-block;
  min-inline-size: 1.6em;
  text-align: center;
  background: var(--surface-2);
  border: 1px solid var(--border-ui);
  border-block-end-width: 2px;
  border-radius: var(--r-sm);
  padding: 0.05em 0.4em;
  font-size: 0.8125em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

::selection {
  background: var(--accent);
  color: var(--accent-ink);
}

:target { scroll-margin-block-start: calc(var(--header-h) + 1rem); }

/* Thin, theme-aware scrollbars where supported. */
* { scrollbar-width: thin; scrollbar-color: var(--border-ui) transparent; }

/* Numeric readouts: mono + tabular figures so digits never re-flow while a
   measurement updates. Applied everywhere a number is displayed. */
.num,
[data-num],
output,
.metric__value,
.metric__unit,
.stat__value,
.kbd-key__stat,
.tbl [data-num],
.tbl .num,
.tbl td.num,
.chart__summary [data-num] {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* ==========================================================================
   04 — LAYOUT
   ========================================================================== */

.wrap {
  inline-size: 100%;
  max-inline-size: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Wide-content escape hatch. Put it on the SECTION that holds an instrument
   (a keyboard map, a wide chart pair) — never on prose. --maxw stays 1200px
   everywhere else, which is the right measure for reading.

   Written as `.wrap.wrap--wide` so it beats the 1440px note below without
   relying on source order, and so `<section class="wrap wrap--wide">` works
   with no extra markup. */
.wrap.wrap--wide { max-inline-size: var(--maxw-wide); }

.prose {
  max-inline-size: var(--maxw-prose);
}

.prose > * + * { margin-block-start: 1em; }
.prose > h2 { margin-block-start: 1.9em; }
.prose > h3 { margin-block-start: 1.5em; }
.prose > h2 + *,
.prose > h3 + * { margin-block-start: 0.6em; }
.prose ul,
.prose ol { padding-inline-start: 1.35em; }
.prose li + li { margin-block-start: 0.35em; }
.prose blockquote {
  border-inline-start: 3px solid var(--accent);
  padding-inline-start: 1rem;
  color: var(--text-2);
}

/* Vertical rhythm helper for page sections. */
.stack > * + * { margin-block-start: var(--space); }
.section { margin-block: clamp(2rem, 5vw, 3.5rem); }

/* Flex/grid children must be allowed to shrink or long content overflows. */
.wrap > *,
.metrics > *,
.toolgrid > * { min-inline-size: 0; }

/* ==========================================================================
   05 — A11Y UTILITIES + FOCUS
   ========================================================================== */

.sr-only {
  position: absolute !important;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  z-index: 200;
  transform: translateY(-120%);
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 650;
  padding: 0.7rem 1.1rem;
  border-radius: 0 0 var(--r-sm) 0;
  text-decoration: none;
  transition: transform 0.16s ease;
}

.skip-link:focus-visible,
.skip-link:focus {
  transform: translateY(0);
}

/* Never removed anywhere in this stylesheet. The 2px offset puts the ring on
   the page background, where --accent clears 10:1 (dark) / 5:1 (light). */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Elements that are scroll containers or large surfaces get a matching ring
   plus a soft halo so the target is obvious at a glance. */
.stage:focus-visible,
.tbl-scroll:focus-visible,
.kbd-map:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--ring);
}

/* ==========================================================================
   06 — HEADER / NAV / DRAWER / DROPDOWNS
   Expected markup (see notes returned with this file):
   header.site-header > .wrap.site-header__inner >
     a.site-logo, nav.site-nav > ul.nav-menu > li.nav-item[.nav-item--group],
     button.nav-toggle, button.theme-toggle
   ========================================================================== */

.site-header {
  position: sticky;
  inset-block-start: 0;
  z-index: 70;
  background: var(--bg-2);
  border-block-end: 1px solid var(--border);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  block-size: var(--header-h);
}

.site-logo {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-inline-end: auto;
  font-weight: 700;
  font-size: clamp(0.9375rem, 0.86rem + 0.4vw, 1.0625rem);
  letter-spacing: -0.02em;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}

.site-logo::before {
  content: "";
  inline-size: 10px;
  block-size: 10px;
  border-radius: 2px;
  background: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  flex: none;
}

.site-logo:hover { color: var(--text); }

/* ---- toggle buttons -------------------------------------------------- */

.nav-toggle,
.theme-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 44px;
  block-size: 44px;
  flex: none;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-ui);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.nav-toggle:hover,
.theme-toggle:hover { background: var(--surface); border-color: var(--accent); }

/* Hamburger drawn in CSS. The button must contain only a .sr-only label. */
.nav-toggle::before,
.nav-toggle::after {
  content: "";
  position: absolute;
  inset-inline: 12px;
  block-size: 2px;
  background: currentColor;
  border-radius: 2px;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.nav-toggle::before { inset-block-start: 14px; box-shadow: 0 7px 0 currentColor; }
.nav-toggle::after  { inset-block-start: 28px; }

.nav-toggle[aria-expanded="true"]::before { transform: translateY(7px) rotate(45deg); box-shadow: none; }
.nav-toggle[aria-expanded="true"]::after  { transform: translateY(-7px) rotate(-45deg); }

/* Half-filled disc = contrast/theme. Reads identically in both palettes and
   needs no icon font. */
.theme-toggle::before {
  content: "";
  inline-size: 17px;
  block-size: 17px;
  border: 2px solid currentColor;
  border-radius: 50%;
  background: linear-gradient(90deg, currentColor 0 50%, transparent 50% 100%);
  transition: transform 0.25s ease;
}

html[data-theme="light"] .theme-toggle::before { transform: rotate(180deg); }

/* ---- nav shell ------------------------------------------------------- */

.site-nav { display: flex; align-items: center; }

.nav-menu {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* Mobile: off-canvas drawer. */
.nav-menu {
  position: fixed;
  inset-block: var(--header-h) 0;
  inset-inline-end: 0;
  inline-size: min(88vw, 21rem);
  z-index: 60;
  background: var(--bg-2);
  border-inline-start: 1px solid var(--border);
  box-shadow: var(--shadow);
  padding: 0.5rem 0 2rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  visibility: hidden;
  transform: translateX(101%);
  /* Visibility must flip instantly on open (otherwise the drawer's contents
     are still `hidden` when JS moves focus into them) and only after the
     slide-out finishes on close. Hence the 0s duration with a delay. */
  transition: transform 0.22s ease, visibility 0s linear 0.22s;
}

.nav-menu.is-open {
  visibility: visible;
  transform: translateX(0);
  transition: transform 0.22s ease, visibility 0s linear 0s;
}

/* Scrim without extra markup. Header (z 70) stays above it. */
body.is-nav-open::before {
  content: "";
  position: fixed;
  inset: var(--header-h) 0 0;
  z-index: 40;
  background: rgb(0 0 0 / .5);
}

body.is-nav-open { overflow: hidden; }

.nav-item { position: relative; }

.nav-link {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  inline-size: 100%;
  min-block-size: 44px;
  padding: 0.5rem 1.1rem;
  color: var(--text);
  font-size: 0.9375rem;
  font-weight: 550;
  text-align: start;
  text-decoration: none;
  cursor: pointer;
  border-radius: var(--r-sm);
  transition: background-color 0.14s ease, color 0.14s ease;
}

.nav-link:hover { background: var(--surface); color: var(--text); }

/* Current page. An accent tint here dropped --accent to 4.18:1 in the light
   theme, so the plate is a neutral surface (--accent clears 4.89:1 light /
   8.81:1 dark on it) and a solid leading rule carries the state non-colour-only
   per §3.4. */
.nav-link[aria-current="page"],
.nav-link.is-current {
  color: var(--accent);
  background: var(--surface-2);
  box-shadow: inset 3px 0 0 var(--accent);
}

/* Group buttons carry a disclosure caret. */
.nav-item--group > .nav-link::after {
  content: "";
  inline-size: 0.42em;
  block-size: 0.42em;
  margin-inline-start: auto;
  border-inline-end: 2px solid currentColor;
  border-block-end: 2px solid currentColor;
  transform: translateY(-0.15em) rotate(45deg);
  transition: transform 0.18s ease;
  opacity: 0.75;
}

.nav-item--group.is-open > .nav-link::after {
  transform: translateY(0.1em) rotate(-135deg);
}

/* Group marked by site.js when it contains the current page. */
.nav-item--group.has-current > .nav-link { color: var(--accent); }

.nav-drop {
  list-style: none;
  margin: 0;
  padding: 0 0 0.35rem 0;
  display: none;
}

.nav-item--group.is-open > .nav-drop { display: block; }

/* No-JS fallback only: site.js sets html.js and then owns aria-expanded, so
   the two mechanisms never fight. */
html:not(.js) .nav-item--group:hover > .nav-drop,
html:not(.js) .nav-item--group:focus-within > .nav-drop { display: block; }

.nav-drop__link {
  display: flex;
  align-items: center;
  min-block-size: 44px;
  padding: 0.4rem 1.1rem 0.4rem 2rem;
  font-size: 0.9063rem;
  color: var(--text-2);
  text-decoration: none;
  border-radius: var(--r-sm);
}

.nav-drop__link:hover,
.nav-drop__link:focus-visible { background: var(--surface); color: var(--text); }

.nav-drop__link[aria-current="page"] {
  color: var(--accent);
  background: var(--surface-2);
  box-shadow: inset 3px 0 0 var(--accent);
}

/* ==========================================================================
   07 — FOOTER
   ========================================================================== */

.site-footer {
  margin-block-start: clamp(2.5rem, 6vw, 4.5rem);
  background: var(--bg-2);
  border-block-start: 1px solid var(--border);
  padding-block: clamp(1.75rem, 4vw, 2.75rem);
  font-size: 0.9375rem;
}

.footer-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
  gap: 1.5rem var(--space);
}

.footer-col h2,
.footer-col h3,
.footer-col__title {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--text-2);
  margin-block-end: 0.6rem;
}

.footer-col ul { list-style: none; padding: 0; margin: 0; }
.footer-col li + li { margin-block-start: 0.35rem; }

.footer-col a {
  color: var(--text-2);
  text-decoration: none;
  display: inline-block;
  padding-block: 0.15rem;
}

.footer-col a:hover { color: var(--accent); text-decoration: underline; }
.footer-col a[aria-current="page"] { color: var(--accent); }

/* Two flex items ONLY: the standing claim, then the copyright line.
 * This is a flex container, so any bare text node inside it becomes its own
 * anonymous flex item — dropping raw text and a <br> in here splits the
 * sentence into pieces that justify-content then spreads across the row.
 * Always wrap the contents in the two spans below. */
.footer-note {
  margin-block-start: 1.75rem;
  padding-block-start: 1.25rem;
  border-block-start: 1px solid var(--border);
  color: var(--text-2);
  font-size: 0.875rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.5rem;
  align-items: baseline;
  line-height: 1.6;
}

.footer-note__claim {
  flex: 1 1 30rem;      /* takes the free width; forces a wrap before it gets cramped */
  min-inline-size: 0;
}

.footer-note__legal {
  flex: 0 0 auto;
  color: var(--text-3);  /* de-emphasised: it is boilerplate, not a claim */
}

/* ==========================================================================
   08 — HERO / BREADCRUMBS
   ========================================================================== */

.hero {
  padding-block: clamp(1.5rem, 4vw, 2.75rem) clamp(1rem, 2.5vw, 1.5rem);
}

.hero__title { margin-block-end: 0.5rem; }

.hero__sub {
  color: var(--text-2);
  font-size: clamp(1rem, 0.95rem + 0.45vw, 1.1875rem);
  max-inline-size: var(--maxw-prose);
  text-wrap: pretty;
}

.crumbs {
  padding-block: 0.85rem;
  font-size: 0.8125rem;
  color: var(--text-2);
}

.crumbs ol {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.15rem 0.4rem;
  padding: 0;
  margin: 0;
}

.crumbs a { color: var(--text-2); text-decoration: none; }
.crumbs a:hover { color: var(--accent); text-decoration: underline; }

.crumbs li + li::before {
  content: "/";
  margin-inline-end: 0.4rem;
  color: var(--text-3);
}

.crumbs [aria-current="page"] { color: var(--text); }

/* ==========================================================================
   09 — CARD
   ========================================================================== */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: clamp(1rem, 2.4vw, 1.5rem);
}

.card + .card { margin-block-start: var(--space); }

.card__title {
  font-size: 1.0625rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  margin-block-end: 0.6rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.card__title::before {
  content: "";
  inline-size: 3px;
  block-size: 1em;
  background: var(--accent);
  border-radius: 2px;
  flex: none;
}

.card__body { color: var(--text-2); }
.card__body > * + * { margin-block-start: 0.75rem; }
.card__body strong,
.card__body h3,
.card__body h4 { color: var(--text); }

/* ==========================================================================
   10 — BUTTONS
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  min-block-size: 44px;
  min-inline-size: 44px;
  /* nowrap keeps labels on one line, so the box must be capped or a long label
     would push the page sideways at 320px. */
  max-inline-size: 100%;
  padding: 0.5rem 1.05rem;
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-ui);
  border-radius: var(--r-sm);
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
  -webkit-appearance: none;
  appearance: none;
}

.btn:hover { border-color: var(--accent); background: var(--surface); color: var(--text); }
.btn:active { transform: translateY(1px); }

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

/* Mixing toward --text lightens in dark mode and darkens in light mode, so the
   ink contrast improves in both directions instead of collapsing. */
.btn--primary:hover {
  background: color-mix(in srgb, var(--accent) 86%, var(--text));
  border-color: color-mix(in srgb, var(--accent) 86%, var(--text));
  color: var(--accent-ink);
}

.btn--ghost {
  background: transparent;
  border-color: var(--border-ui);
  color: var(--text);
}

.btn--ghost:hover { background: var(--surface-2); border-color: var(--accent); }

.btn--danger {
  background: var(--bad);
  border-color: var(--bad);
  color: var(--on-solid);
}

.btn--danger:hover {
  background: color-mix(in srgb, var(--bad) 86%, var(--text));
  border-color: color-mix(in srgb, var(--bad) 86%, var(--text));
  color: var(--on-solid);
}

/* Size ladder: sm / base / lg, all three in one place.
   --sm keeps the 13px type and tight horizontal padding so it still reads as a
   secondary control, but BUILD-SPEC §3.3 sets a 44x44 touch target and the old
   36px base put every export, preset and (worst) both delete buttons on
   /keyboard-comparison/ under it. 40px is the floor anywhere; §21 lifts it to
   the full 44px on any narrow or touch-driven viewport. */
.btn--sm {
  min-block-size: 40px;
  padding: 0.3rem 0.7rem;
  font-size: 0.8125rem;
  gap: 0.35rem;
}

/* One step up for a page's single hero CTA. Lived in mother-tool.css and was
   used exactly once, which put the rule that sizes the site's most important
   control in the one file nobody editing the button ladder would open. */
.btn--lg {
  min-block-size: 48px;
  padding-inline: 1.5rem;
  font-size: 1rem;
}

/* A row of controls that wraps cleanly on narrow screens. */
.btnrow {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

.btnrow > * { min-inline-size: 0; }

/* --------------------------------------------------------------------------
   10.1 — SEGMENTED CONTROL
   A row of related toggle buttons that reads as one control: sample target,
   passage length, unit picker. Buttons carry aria-pressed, so the selected
   state is never colour-only.

   Promoted here from mother-tool.css because /keyboard-typing-test/ had built
   the same interaction a second, visually incompatible way (an outlined .btn
   with an accent underline) — the same state cannot mean "filled plate" on one
   page and "underline" on the next.

   Markup:
     <div class="seg" role="group" aria-label="Sample target">
       <span class="seg__label" aria-hidden="true">Samples</span>
       <button type="button" class="seg__btn" aria-pressed="true">40</button>
       …
     </div>
   -------------------------------------------------------------------------- */

.seg {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  max-inline-size: 100%;
}

.seg__label {
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--text-2);
  padding-inline: 0.45rem 0.15rem;
}

.seg__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 40px;
  min-inline-size: 44px;
  padding: 0.25rem 0.8rem;
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-align: center;
  color: var(--text-2);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background-color 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}

.seg__btn:hover { color: var(--text); background: var(--surface-2); }

/* Selected: filled plate + accent ink. The aria-pressed attribute is the
   non-visual half of the same statement, and the fill is a shape change (a
   plate appears) as well as a colour change. */
.seg__btn[aria-pressed="true"],
.seg__btn[aria-checked="true"],
.seg__btn[aria-selected="true"],
.seg__btn.is-on {
  color: var(--accent-ink);
  background: var(--accent);
  border-color: var(--accent);
}

.seg__btn[aria-pressed="true"]:hover,
.seg__btn[aria-checked="true"]:hover,
.seg__btn[aria-selected="true"]:hover,
.seg__btn.is-on:hover {
  background: color-mix(in srgb, var(--accent) 86%, var(--text));
  color: var(--accent-ink);
}

.seg__btn[disabled],
.seg__btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

/* ==========================================================================
   11 — METRICS
   ========================================================================== */

/* The spec's minmax(140px,1fr) overflows any container narrower than 140px —
   reachable at 320px once a card sits inside a <details> inside another card.
   min(140px,100%) keeps the identical layout everywhere the track fits and
   collapses to the container width instead of overflowing when it does not. */
.metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr));
  gap: 0.75rem;
}

.metric {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 0.75rem 0.85rem 0.8rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-inline-size: 0;
}

.metric__label {
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--text-2);
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.metric__value {
  font-size: clamp(1.5rem, 1.15rem + 1.7vw, 2.125rem);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.02em;
  color: var(--text);
  display: block;
  overflow-wrap: anywhere;
}

/* Word-shaped readouts (consistency label, quality label, browser name) need a
   size that fits a 140px tile without breaking mid-word. */
.metric__value--text {
  font-family: var(--font);
  font-size: clamp(1rem, 0.94rem + 0.45vw, 1.25rem);
  font-weight: 650;
  letter-spacing: -0.01em;
  line-height: 1.25;
  font-variant-numeric: normal;
  overflow-wrap: break-word;
}

.metric__unit {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-2);
  margin-inline-start: 0.15em;
}

.metric__note {
  font-size: 0.75rem;
  line-height: 1.4;
  color: var(--text-2);
  margin-block-start: 0.2rem;
}

/* Flagship readout — one per page at most. */
.metric--lead {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--accent) 7%, transparent), transparent 60%),
    var(--surface);
  grid-column: 1 / -1;
}

.metric--lead .metric__value {
  font-size: var(--fs-readout);
  color: var(--accent);
}

.metric--good .metric__value { color: var(--good); }
.metric--warn .metric__value { color: var(--warn); }
.metric--bad  .metric__value { color: var(--bad); }

/* State is never colour-only: pair with a text label element.
   Geometry and type come from .tag (§20) — this was a second, near-identical
   pill inside the design system, differing only in horizontal padding and
   border colour. Both are now one component; see .tag--currentcolor. */
.metric__state { align-self: flex-start; }

/* ==========================================================================
   12 — STAGE
   ========================================================================== */

.stage {
  position: relative;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 0.5rem;
  text-align: center;
  min-block-size: clamp(190px, 34vh, 320px);
  padding: clamp(1rem, 3vw, 2rem);
  border: 1px dashed var(--border-ui);
  border-radius: var(--r-lg);
  cursor: pointer;
  overflow: hidden;
  color: var(--text);
  background-color: var(--surface);
  /* Oscilloscope graticule. */
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
  background-size: 28px 28px;
  background-position: center center;
  transition: border-color 0.18s ease, background-color 0.18s ease, box-shadow 0.18s ease;
  -webkit-tap-highlight-color: transparent;
}

.stage__hint {
  color: var(--text-2);
  font-size: 0.9375rem;
  max-inline-size: 44ch;
}

.stage__big {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-readout-lg);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
}

.stage--armed {
  border-style: solid;
  border-color: var(--accent);
  box-shadow: var(--ring);
}

.stage--running {
  border-style: solid;
  border-color: var(--accent);
  background-color: color-mix(in srgb, var(--accent) 6%, var(--surface));
}

/* Sweep line: motion is the only decorative animation in the system and it is
   removed entirely under prefers-reduced-motion. */
.stage--running::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inline-size: 2px;
  background: linear-gradient(180deg, transparent, var(--accent), transparent);
  opacity: 0.55;
  animation: klt-sweep 2.4s linear infinite;
}

/* Percentages resolve against the stage box, so the sweep tracks its width. */
@keyframes klt-sweep {
  from { inset-inline-start: 0%; }
  to   { inset-inline-start: 100%; }
}

.stage--done {
  border-style: solid;
  border-color: var(--good);
  background-color: color-mix(in srgb, var(--good) 6%, var(--surface));
}

/* ==========================================================================
   13 — CHART
   ========================================================================== */

.chart {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 0.75rem;
}

.chart__canvas {
  inline-size: 100%;
  /* Fixed CSS height: kb-charts.js reads the CSS box and sizes the backing
     store by devicePixelRatio. Override per page with --chart-h. */
  block-size: var(--chart-h, clamp(180px, 40vw, 340px));
  border-radius: var(--r-sm);
  background: transparent;
}

.chart__summary {
  margin-block-start: 0.6rem;
  padding-block-start: 0.6rem;
  border-block-start: 1px solid var(--border);
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--text-2);
}

.chart__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.9rem;
  font-size: 0.75rem;
  color: var(--text-2);
  margin-block-start: 0.5rem;
}

.chart__legend > * { display: inline-flex; align-items: center; gap: 0.35rem; }

.chart__legend i {
  inline-size: 10px;
  block-size: 10px;
  border-radius: 2px;
  background: currentColor;
  flex: none;
}

/* Small heading above a canvas inside a .chart-pair. */
.chart__head {
  font-size: 0.8125rem;
  font-weight: 650;
  color: var(--text);
  margin-block-end: var(--s-2);
}

/* Two charts: stacked on phones, side by side on wide screens.
   Four pages had independently invented this — .chartgrid, .chart-pair,
   .tt-charts and .rt-charts — and they flipped to two columns at 1024, 768,
   900 and 768 respectively, so the same block reflowed at a different moment on
   every page. 1024 is the documented ladder point and the only one that leaves
   each canvas a workable width: two charts in a 706px content column at 768px
   gives each one ~341px. Per-page --chart-h overrides stay legitimate. */
.chart-pair {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space);
}

/* A canvas' intrinsic 300px would otherwise set the column floor. */
.chart-pair > * { min-inline-size: 0; }

@media (min-width: 1024px) {
  .chart-pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --------------------------------------------------------------------------
   13.1 — PROGRESS BAR
   One bar, one contract. Three pages each built their own (.prog,
   .progress__track, .kt-bar) with different heights, different border tokens
   and — the real trap — two incompatible meanings for the same custom property
   name: one expected `--p: 42`, the other `--p: 42%`. Moving markup between
   them silently produced a broken bar.

   THE CONTRACT: --p is a BARE NUMBER, 0-100.
     <div class="bar"><span class="bar__fill" style="--p:42"></span></div>
   Pair it with role="progressbar" + aria-valuenow on the .bar, or with visible
   text — the bar itself is never the only statement of progress.
   -------------------------------------------------------------------------- */

.bar {
  inline-size: 100%;
  block-size: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border-ui);
  border-radius: 999px;
  overflow: hidden;
}

.bar__fill {
  display: block;
  block-size: 100%;
  inline-size: calc(var(--p, 0) * 1%);
  border-radius: 999px;
  background: var(--accent);
  transition: inline-size 0.18s ease;
}

/* Coverage/completeness meters read as "good", not as "in progress". */
.bar__fill--good { background: var(--good); }
.bar__fill--warn { background: var(--warn); }

/* ==========================================================================
   14 — KEYBOARD MAP
   ========================================================================== */

/* THE KEYCAP SIZE POLICY — defined once, here, for every page that mounts a
   map. Do not re-declare --u in a page's tool.css.

   Floor 40px. Every keycap is a button, and --u is the whole key box: the cap
   measures --u minus --kbd-gap. The old 22px floor rendered an 18x18px target,
   below WCAG 2.5.8's 24x24 minimum, with a 7px legend — unreadable, and
   un-hittable on the device where it mattered. 40px yields a 36x36 cap with a
   10.4px legend. It is also the point at which the six-letter nav-cluster
   legends (Delete, Insert, PrtSc, ScrLk, Pause) stop being clipped, because the
   cap's padding and border do not scale with --u while its font does.

   Ceiling 52px, curve 3.4vw — unchanged, so desktop rendering is identical to
   what it has always been.

   A wider map costs SCROLL DISTANCE inside this element's own scroller, never
   page overflow: .kbd-map is itself the horizontal scroller, and a host that
   wants to own the scroll instead (so the map's border spans the widest row)
   sets `overflow-x: visible; inline-size: max-content` on it — see .kbdhost. */
.kbd-map {
  --u: clamp(40px, 3.4vw, 52px);
  display: flex;
  flex-direction: column;
  gap: var(--kbd-gap);
  padding: var(--kbd-gap);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  /* Full layouts are ~23u wide — always scroll the map, never the page. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.kbd-map--lg { --u: clamp(48px, 5.4vw, 62px); --kbd-gap: 5px; }

/* Touch: 44x44 caps, per BUILD-SPEC §3.3's touch-target rule. This branch used
   to exist only on the homepage, which is why five diagnostic pages shipped an
   18px cap to the exact devices that could not hit it. */
@media (pointer: coarse) {
  .kbd-map { --u: clamp(48px, 3.4vw, 52px); }
  .kbd-map--lg { --u: clamp(52px, 5.4vw, 62px); }
}

.kbd-row {
  display: flex;
  gap: var(--kbd-gap);
  inline-size: max-content;
  min-inline-size: 100%;
}

/* Spacer for stepped rows (nav cluster gaps): set --k like a key. */
.kbd-gap { inline-size: calc(var(--u) * var(--k, 1) - var(--kbd-gap)); flex: none; }

/* kb-keyboard.js emits <span class="kbd-spacer"> with the width already inline
   as `calc(var(--u) * N)`. Cancelling one flex gap turns that into a pitch of
   exactly N units, matching the keys either side of it. */
.kbd-spacer {
  flex: none;
  align-self: stretch;
  margin-inline-end: calc(var(--kbd-gap) * -1);
  pointer-events: none;
}

.kbd-key {
  /* --kw / --kh are written inline by kb-keyboard.js on every key, so any width
     in the layout data works whether or not a modifier class exists for it.
     The .kbd-key--wNNN / --hNNN modifiers below cover hand-written markup, and
     --k is kept as a legacy alias. */
  --kw: var(--k, 1);
  --kh: 1;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  flex: none;
  /* Subtracting the gutter makes N-unit keys line up with N grid columns. */
  inline-size: calc(var(--u) * var(--kw) - var(--kbd-gap));
  block-size: calc(var(--u) * var(--kh) - var(--kbd-gap));
  /* A 2u-tall key (ISO Enter) must overhang into the row below rather than
     stretch its own row: cancel every unit past the first. Resolves to 0 for
     the normal --kh:1 case. */
  margin-block-end: calc((1 - var(--kh)) * var(--u));
  padding: 2px;
  overflow: hidden;
  font-family: var(--font);
  font-size: clamp(7px, calc(var(--u) * 0.26), 13px);
  line-height: 1.05;
  font-weight: 550;
  text-align: center;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-ui);
  border-radius: max(3px, calc(var(--u) * 0.12));
  box-shadow: inset 0 -1px 0 rgb(0 0 0 / .18);
  cursor: default;
  -webkit-appearance: none;
  appearance: none;
  transition: background-color 0.09s ease, border-color 0.09s ease, color 0.09s ease;
}

button.kbd-key { cursor: pointer; }

/* Keys that can never reach the browser (Fn) are rendered aria-disabled by
   kb-keyboard.js — they must not look clickable. */
.kbd-key[aria-disabled="true"] { cursor: default; }

.kbd-key__label { display: block; max-inline-size: 100%; overflow: hidden; }

/* Shifted legend (the upper glyph on a dual-legend cap). Rendered above the
   primary label by the column flex direction, one step smaller and quieter. */
.kbd-key__shift {
  display: block;
  max-inline-size: 100%;
  overflow: hidden;
  font-size: 0.82em;
  line-height: 1;
  color: var(--text-2);
}

.kbd-key__stat {
  display: block;
  font-size: 0.72em;
  line-height: 1;
  color: var(--text-2);
}

/* Width modifiers — multiples of one unit. Every width kb-layouts.js can emit
   has an entry here (1.25 … 7), so class-only markup renders identically to the
   JS-rendered map. */
.kbd-key--w125 { --kw: 1.25; }
.kbd-key--w150 { --kw: 1.5; }
.kbd-key--w175 { --kw: 1.75; }
.kbd-key--w200 { --kw: 2; }
.kbd-key--w225 { --kw: 2.25; }
.kbd-key--w250 { --kw: 2.5; }
.kbd-key--w275 { --kw: 2.75; }
.kbd-key--w500 { --kw: 5; }
.kbd-key--w525 { --kw: 5.25; }
.kbd-key--w625 { --kw: 6.25; }
.kbd-key--w700 { --kw: 7; }

/* Height modifier — the ISO Enter is the only 2u key in the layout set. */
.kbd-key--h200 { --kh: 2; z-index: 1; }

/* States. Each one carries a non-colour cue as well as a colour, per §3.4. */

.kbd-key--active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  box-shadow: var(--ring);
  transform: translateY(1px);
}

.kbd-key--hit {
  background: color-mix(in srgb, var(--good) 20%, var(--surface-2));
  border-color: var(--good);
  color: var(--text);
}

/* Corner tick = "registered at least once", independent of colour. */
.kbd-key--hit::after {
  content: "";
  position: absolute;
  inset-block-start: 2px;
  inset-inline-end: 2px;
  inline-size: 4px;
  block-size: 4px;
  border-radius: 50%;
  background: var(--good);
}

.kbd-key--dead {
  color: var(--text-3);
  border-color: var(--border);
  border-style: dashed;
  /* Diagonal strike = "never registered", independent of colour. */
  background-image: repeating-linear-gradient(
    -45deg,
    color-mix(in srgb, var(--bad) 26%, transparent) 0 2px,
    transparent 2px 6px
  );
}

.kbd-map__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  font-size: 0.75rem;
  color: var(--text-2);
  margin-block-start: 0.6rem;
}

/* ==========================================================================
   15 — TABLES
   ========================================================================== */

/* Every table is wrapped in .tbl-scroll. Give it tabindex="0", role="region"
   and an aria-label so keyboard users can reach the scroll. */
.tbl-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface);
  max-inline-size: 100%;
  /* Must be allowed to shrink when it is a flex/grid child, or the 30rem table
     inside it becomes the container's min-content width. */
  min-inline-size: 0;
  /* Sticky cells inside a table that is wider than this box were escaping the
     scroller's containment and inflating the page's initial containing block —
     measured 525px of ICB at a 375px viewport on /keyboard-comparison/, which
     puts every vw unit and every position:fixed offset on that page 40% out.
     A paint containment box stops the sticky descendants leaking outward. It
     does not disable sticky: the scroll container is still this element. */
  contain: paint;
}

.tbl {
  inline-size: 100%;
  min-inline-size: 30rem;
  border-collapse: collapse;
  font-size: 0.875rem;
}

.tbl caption {
  caption-side: top;
  text-align: start;
  padding: 0.7rem 0.85rem;
  font-size: 0.8125rem;
  color: var(--text-2);
  border-block-end: 1px solid var(--border);
}

.tbl th,
.tbl td {
  padding: 0.55rem 0.85rem;
  text-align: start;
  vertical-align: top;
}

.tbl thead th {
  position: sticky;
  inset-block-start: 0;
  /* Stacking order inside a table that scrolls in BOTH axes:
       1  ordinary sticky row header (tbody th)
       2  sticky column header (thead th)
       3  the corner cell, which is sticky in both axes and must sit over both */
  z-index: 2;
  background: var(--surface-2);
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-2);
  white-space: nowrap;
  border-block-end: 1px solid var(--border-strong);
}

/* ---- pinned row header ----------------------------------------------------
   .tbl carries min-inline-size: 30rem and these tables run to eight columns, so
   on a phone the scroller shows well under half the table. With nothing pinned,
   scrolling right to read "Jitter (ms)" loses the column that says WHICH KEY
   the number belongs to, which makes the densest output on the site unusable on
   the most common device.

   Requires the first cell of every body row to be <th scope="row">, which is
   already the convention (215 instances across the site) and is the correct
   markup regardless. A <td> first column simply stays unpinned.

   The background must be opaque — a transparent pinned cell lets the scrolled
   numbers slide visibly underneath it.
   -------------------------------------------------------------------------- */
.tbl tbody th {
  position: sticky;
  inset-inline-start: 0;
  z-index: 1;
  background: var(--surface);
  font-weight: 600;
  color: var(--text);
  /* Keeps the pinned column from eating the scroller: at 320px the .tbl-scroll
     inner width is ~286px and this caps the pin at ~34% of it. */
  max-inline-size: 10rem;
}

.tbl thead th:first-child {
  inset-inline-start: 0;
  z-index: 3;
}

/* A pinned cell needs its own edge, or the scrolled columns appear to run into
   the key name. */
.tbl tbody th,
.tbl thead th:first-child { border-inline-end: 1px solid var(--border); }

.tbl tbody tr + tr th,
.tbl tbody tr + tr td { border-block-start: 1px solid var(--border); }

/* The row-header cell is opaque, so it cannot inherit a translucent hover tint
   without the columns showing through it. Mix the same tint into --surface. */
.tbl tbody tr:hover td { background: color-mix(in srgb, var(--accent) 5%, transparent); }
.tbl tbody tr:hover th { background: color-mix(in srgb, var(--accent) 5%, var(--surface)); }

.tbl [data-num],
.tbl .num,
.tbl td.num { text-align: end; white-space: nowrap; }

/* ==========================================================================
   16 — NOTES / DISCLOSE / FAQ
   ========================================================================== */

/* The leading rule identifies the callout, so it needs 3:1. --border-strong is
   only 1.90:1 on --bg / 1.69:1 on --surface in the dark theme; --border-ui is
   3.33:1 dark and 4.60:1 light. */
.note {
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--border-ui);
  border-radius: 0 var(--r) var(--r) 0;
  background: var(--surface);
  padding: 0.85rem 1rem;
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--text-2);
  /* Notes are prose. Placed as a sibling of .prose rather than inside it they inherit the
     full 1200px container and run to ~165 characters per line — twice the body measure,
     on the "what this cannot measure" callouts that are the site's whole differentiator.
     Cap the component itself so it reads correctly wherever it is placed. */
  max-inline-size: var(--maxw-prose);
}

/* Inside a card, panel or grid cell the parent already constrains the measure, and the note
   should fill it rather than leaving a ragged gap. */
.metrics .note,
.stage .note { max-inline-size: none; }

.note > * + * { margin-block-start: 0.55rem; }
.note strong { color: var(--text); }

.note__title {
  display: block;
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--text);
  margin-block-end: 0.3rem;
}

.note--info {
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
  border-inline-start-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
}

.note--info .note__title { color: var(--accent); }

.note--warn {
  border-color: color-mix(in srgb, var(--warn) 35%, var(--border));
  border-inline-start-color: var(--warn);
  background: color-mix(in srgb, var(--warn) 8%, var(--surface));
}

.note--warn .note__title { color: var(--warn); }

/* "What this test cannot tell you." Deliberately the loudest callout in the
   system: thick amber rule, hatched leading edge, distinct surface. */
.note--limit {
  border: 1px solid color-mix(in srgb, var(--amber) 40%, var(--border));
  border-inline-start: 4px solid var(--amber);
  border-radius: 0 var(--r) var(--r) 0;
  background-color: var(--surface-2);
  background-image:
    repeating-linear-gradient(
      -45deg,
      color-mix(in srgb, var(--amber) 16%, transparent) 0 4px,
      transparent 4px 10px
    ),
    linear-gradient(90deg, color-mix(in srgb, var(--amber) 9%, transparent), transparent 55%);
  background-size: 12px 100%, 100% 100%;
  background-repeat: no-repeat, no-repeat;
  background-position: left center, left center;
  padding-inline-start: 1.5rem;
  color: var(--text);
}

.note--limit .note__title { color: var(--amber); }

/* --amber (#b45309) on the light --surface-2 measures 4.59:1 — clearing AA by
   0.09 at the hardest possible treatment: 12px, 700 weight, uppercase, 0.09em
   tracking. This is the label on the honesty callouts, the one element the site
   least wants skimmed. #92400e is 6.48:1 on the same background. Scoped to this
   label so --amber itself (used for rules, hatching and diagram strokes, where
   4.59:1 is comfortably fine) is unchanged. */
html[data-theme="light"] .note--limit .note__title { color: #92400e; }

.disclose {
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface);
}

.disclose > summary {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  min-block-size: 44px;
  padding: 0.6rem 1rem;
  font-size: 0.9375rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  border-radius: var(--r);
}

.disclose > summary::-webkit-details-marker { display: none; }

.disclose > summary::before {
  content: "";
  flex: none;
  inline-size: 0.4em;
  block-size: 0.4em;
  border-inline-end: 2px solid var(--accent);
  border-block-end: 2px solid var(--accent);
  transform: rotate(-45deg);
  transition: transform 0.18s ease;
}

.disclose[open] > summary::before { transform: rotate(45deg); }
.disclose > summary:hover { color: var(--accent); }
.disclose > :not(summary) { padding-inline: 1rem; }
.disclose > :last-child:not(summary) { padding-block-end: 1rem; }
.disclose[open] > summary { border-block-end: 1px solid var(--border); border-radius: var(--r) var(--r) 0 0; }
.disclose[open] > summary + * { padding-block-start: 0.85rem; }

.faq {
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface);
  overflow: hidden;
}

.faq > details + details { border-block-start: 1px solid var(--border); }

.faq summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-block-size: 44px;
  padding: 0.75rem 1rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}

.faq summary::-webkit-details-marker { display: none; }

.faq summary::before {
  content: "";
  flex: none;
  inline-size: 0.4em;
  block-size: 0.4em;
  border-inline-end: 2px solid var(--accent);
  border-block-end: 2px solid var(--accent);
  transform: rotate(-45deg);
  transition: transform 0.18s ease;
}

.faq details[open] summary::before { transform: rotate(45deg); }
.faq summary:hover { color: var(--accent); }

.faq details > :not(summary) {
  padding: 0 1rem 1rem;
  color: var(--text-2);
  max-inline-size: var(--maxw-prose);
}

.faq details > :not(summary) + :not(summary) { padding-block-start: 0; margin-block-start: -0.4rem; }

/* ==========================================================================
   17 — TOOLGRID
   ========================================================================== */

.toolgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: 0.85rem;
  list-style: none;
  padding: 0;
  margin: 0;
}

.toolcard {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  text-decoration: none;
  color: var(--text);
  transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
}

.toolcard:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 5%, var(--surface));
  color: var(--text);
  transform: translateY(-2px);
}

.toolcard__title {
  font-size: 1rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--accent);
}

.toolcard__desc {
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--text-2);
}

/* ==========================================================================
   18 — TOAST (created by KLT.ui.toast)
   ========================================================================== */

.toast-region {
  position: fixed;
  inset-block-end: max(1rem, env(safe-area-inset-bottom));
  inset-inline: 0;
  z-index: 300;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding-inline: 1rem;
  pointer-events: none;
}

.toast {
  max-inline-size: min(30rem, 100%);
  padding: 0.6rem 1rem;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-ui);
  border-inline-start: 3px solid var(--accent);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow);
  font-size: 0.875rem;
  line-height: 1.45;
  pointer-events: auto;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.18s ease, transform 0.18s ease;
}

.toast.is-in { opacity: 1; transform: translateY(0); }
.toast--good { border-inline-start-color: var(--good); }
.toast--warn { border-inline-start-color: var(--warn); }
.toast--bad  { border-inline-start-color: var(--bad); }

/* ==========================================================================
   19 — FORMS
   ========================================================================== */

.field { display: flex; flex-direction: column; gap: 0.3rem; min-inline-size: 0; }

.field > label,
.label {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-2);
}

input[type="text"],
input[type="number"],
input[type="search"],
input[type="email"],
select,
textarea {
  inline-size: 100%;
  min-block-size: 44px;
  padding: 0.5rem 0.7rem;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-ui);
  border-radius: var(--r-sm);
  -webkit-appearance: none;
  appearance: none;
}

input[type="number"],
input[type="search"] { font-family: var(--mono); font-variant-numeric: tabular-nums; }

select {
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 17px) 50%, calc(100% - 12px) 50%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-inline-end: 2rem;
}

textarea { min-block-size: 6rem; resize: vertical; }

::placeholder { color: var(--text-3); opacity: 1; }

input:focus-visible,
select:focus-visible,
textarea:focus-visible { border-color: var(--accent); }

fieldset { border: 1px solid var(--border); border-radius: var(--r); padding: 0.85rem 1rem; }
legend { padding-inline: 0.35rem; font-size: 0.8125rem; font-weight: 600; color: var(--text-2); }

/* A checkbox or radio with its label. The native box is 16-18px however it is
   styled, so the LABEL carries the touch target: the whole thing is the hit
   area, which is also what a mouse user expects.

     <label class="checkline"><input type="checkbox" id="x"> <span>…</span></label>

   Use this instead of leaving a bare <input type="checkbox"> next to text —
   an 18px control that changes what a test will do is the smallest target on
   any row it sits in. */
.checkline {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-block-size: 44px;
  padding-block: 0.25rem;
  font-size: 0.875rem;
  color: var(--text-2);
  cursor: pointer;
  min-inline-size: 0;
}

.checkline > input {
  flex: none;
  inline-size: 18px;
  block-size: 18px;
  accent-color: var(--accent);
  cursor: pointer;
}

.checkline:hover { color: var(--text); }

/* Error state for a field whose value was rejected. Pair with aria-invalid so
   the signal exists for assistive tech as well — never colour alone. */
input[aria-invalid="true"],
select[aria-invalid="true"],
textarea[aria-invalid="true"] {
  border-color: var(--bad);
  box-shadow: inset 0 0 0 1px var(--bad);
}

/* Validation / clamp message under a field. --bad for a rejection, --warn for
   an advisory clamp that still produced a usable answer. */
.field__msg {
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--warn);
}

.field__msg--error { color: var(--bad); }
.field__msg:empty { display: none; }

/* ==========================================================================
   20 — UTILITIES
   ========================================================================== */

.mono { font-family: var(--mono); }
.muted { color: var(--text-2); }
.tiny { font-size: 0.75rem; }
.center { text-align: center; }
.nowrap { white-space: nowrap; }
.flow > * + * { margin-block-start: 0.75rem; }
.mt-0 { margin-block-start: 0; }
.mt { margin-block-start: var(--space); }
.mb { margin-block-end: var(--space); }
.hidden,
[hidden] { display: none !important; }

/* The small labelled pill. ONE implementation — .metric__state was a second
   copy inside this file and pages had added a third (.rt-flag), all differing
   only in a few hundredths of a rem. Page-local pills should become
   `.tag .tag--currentcolor` unless they have a written reason to differ
   (.keychip keeps key-legend capitalisation and a mono face — that reason is
   sound and stands). */
.tag,
.metric__state {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.1rem 0.5rem;
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  border-radius: 999px;
  /* The pill outline is the component boundary — same 3:1 reasoning as .note. */
  border: 1px solid var(--border-ui);
  color: var(--text-2);
}

.tag--good { color: var(--good); border-color: var(--good); }
.tag--warn { color: var(--warn); border-color: var(--warn); }
.tag--bad  { color: var(--bad);  border-color: var(--bad); }
.tag--accent { color: var(--accent); border-color: var(--accent); }

/* Pill that takes its outline from whatever colour the label is already using
   (metric state chips, per-key flags). Colour is never the only signal — the
   pill always contains a word. */
.tag--currentcolor,
.metric__state { border-color: currentColor; }

/* Environment strip: "Timer resolution: 0.1 ms · Focus: OK · …" */
.envline {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 0.85rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--text-2);
}

.envline > * { display: inline-flex; align-items: center; gap: 0.3rem; }

/* ==========================================================================
   21 — RESPONSIVE (mobile-first; 480 / 768 / 1024 / 1280 / 1440)
   ========================================================================== */

@media (min-width: 480px) {
  .metrics { gap: 0.85rem; }
}

/* ---- touch-target policy (BUILD-SPEC §3.3: >= 44x44) ----------------------
   Two conditions, deliberately, because neither alone is trustworthy:
     (pointer: coarse)   — misses touchscreen laptops that report a fine pointer
     narrow viewport     — catches the phone case even where the media query is
                           unreliable, and costs a desktop user nothing because
                           they are not at 500px
   .btn is already 44px at every width; these are the variants that were not. */
@media (max-width: 47.99rem) {
  .btn--sm,
  .seg__btn { min-block-size: 44px; }
  /* 4px between adjacent options in a picker is too tight to hit reliably. */
  .seg { gap: var(--s-2); }
}

@media (pointer: coarse) {
  .btn--sm,
  .seg__btn { min-block-size: 44px; }
  .seg { gap: var(--s-2); }
}

@media (min-width: 768px) {
  :root { --header-h: 60px; }
  .stage { min-block-size: clamp(230px, 32vh, 340px); }
  .footer-note { justify-content: space-between; }
  .tbl { font-size: 0.9063rem; }
}

/* Nav flips from drawer to menubar here. site.js reads --nav-mode, so this
   breakpoint is defined once, in CSS only. */
@media (min-width: 1024px) {
  :root { --header-h: 64px; }

  .site-header { --nav-mode: bar; }

  .nav-toggle { display: none; }

  .site-header__inner { gap: 0.75rem; }

  .nav-menu {
    position: static;
    inline-size: auto;
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
    overflow: visible;
    visibility: visible;
    transform: none;
    transition: none;
    display: flex;
    align-items: center;
    gap: 0.15rem;
  }

  body.is-nav-open::before { display: none; }
  body.is-nav-open { overflow: visible; }

  .nav-link {
    inline-size: auto;
    min-block-size: 38px;
    padding: 0.35rem 0.6rem;
    white-space: nowrap;
    font-size: 0.875rem;
  }

  /* Horizontal bar: the current-page rule reads as an underline, not a
     leading edge. Same 3px, same non-colour-only guarantee. */
  .nav-link[aria-current="page"],
  .nav-link.is-current { box-shadow: inset 0 -3px 0 var(--accent); }

  .nav-item--group > .nav-link::after { margin-inline-start: 0.1rem; }

  .nav-drop {
    position: absolute;
    inset-block-start: calc(100% + 6px);
    inset-inline-start: 0;
    z-index: 80;
    min-inline-size: 14rem;
    max-inline-size: min(20rem, calc(100vw - 2rem));
    padding: 0.3rem;
    background: var(--surface);
    border: 1px solid var(--border-ui);
    border-radius: var(--r);
    box-shadow: var(--shadow);
  }

  /* Right-align the panels nearest the end of the bar so they cannot spill
     past the viewport edge (body has overflow-x:clip). */
  .nav-menu > .nav-item--group:nth-last-child(-n + 3) > .nav-drop {
    inset-inline-start: auto;
    inset-inline-end: 0;
  }

  .nav-drop__link {
    min-block-size: 36px;
    padding: 0.35rem 0.6rem;
    font-size: 0.875rem;
    white-space: nowrap;
  }

  .footer-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .metrics { gap: 1rem; }
}

@media (min-width: 1280px) {
  :root { --gutter: 2.25rem; }
}

/* Container is already capped at --maxw (1200px), so above 1440 there is
   nothing left to settle.
   The old `--gutter: 2.5rem` step here made the content 8px NARROWER as the
   screen grew (1128px at 1280 -> 1120px at 1440 and at 1920) while 360px of
   margin sat empty on each side, and it moved the keyboard map's overflow
   threshold DOWN. Removed deliberately; the 1280 gutter carries upward. */
@media (min-width: 1440px) {
  /* NOTE: do NOT re-declare `.wrap { max-inline-size: var(--maxw) }` here.
     .wrap already sets it at the top of the file. Repeating it below .prose
     gives an equal-specificity rule later in source order, which beat
     `.prose { max-inline-size: var(--maxw-prose) }` on elements carrying both
     classes (`<div class="wrap prose section">` — every guide and the
     methodology page). Line length jumped from 75 to 162 characters at
     exactly 1440px and wider. */
}

/* Coarse pointers get roomier hit areas.
   `.btn--sm` is NOT re-declared here. The touch-target block earlier in this file
   already sets `.btn--sm, .seg__btn { min-block-size: 44px }` under the same
   `(pointer: coarse)` query. Repeating it here at 40px was an equal-specificity
   rule later in source order, so it silently won and capped every small button
   at 40px — four short of the 44px this file had just promised, and of the
   >=44x44 that BUILD-SPEC §3.3 declares. Same failure mode as the .wrap/.prose
   override above: the duplicate, not the original, is the bug. */
@media (pointer: coarse) {
  .nav-drop__link { min-block-size: 44px; }
}

/* Landscape phones. At 740x360 the sticky header takes 15.6% of the viewport
   and .stage's 190px floor takes another 53%, so the test surface and the
   readout below it cannot be on screen together — which for a timing test is
   the entire interaction. The header stays sticky (nav and theme toggle remain
   reachable mid-test, which is worth more than 56px); the stage gives back
   ~50px instead. */
@media (orientation: landscape) and (max-height: 480px) {
  .stage { min-block-size: clamp(140px, 40vh, 200px); }
}

/* ==========================================================================
   22 — FORCED COLORS (Windows High Contrast)
   Under forced colors the OS overrides background-color, border-color and
   color, so every state this system encodes in fill collapses to one
   appearance: a registered keycap, a never-registered keycap and the primary
   CTA all render white-on-black with a black border.

   The shape cues survive and are the real answer — .kbd-key--hit's corner tick
   and .kbd-key--dead's diagonal strike are background-IMAGES, which forced
   colors keeps. This block re-expresses the rest in system colours and in
   border-style, which is also never overridden.
   ========================================================================== */

@media (forced-colors: active) {
  /* The focus ring's colour is forced too; name the system colour explicitly so
     it cannot land the same as the element it is ringing. */
  :focus-visible,
  .stage:focus-visible,
  .tbl-scroll:focus-visible,
  .kbd-map:focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 2px;
  }

  /* The one control that must stay findable on the page. */
  .btn--primary {
    outline: 2px solid ButtonText;
    outline-offset: -4px;
  }

  .btn--danger { outline: 2px dashed ButtonText; outline-offset: -4px; }

  /* Selected segment: a filled plate is indistinguishable once fills are
     forced, so the selected option states itself with a heavier, solid edge.
     aria-pressed still carries it for assistive tech. */
  .seg__btn[aria-pressed="true"],
  .seg__btn[aria-selected="true"] {
    border: 2px solid Highlight;
    outline: 1px solid ButtonText;
    outline-offset: -4px;
  }

  /* Keycap states, in border-style + preserved shape marks. */
  .kbd-key { border: 1px solid ButtonText; }
  .kbd-key--active { border: 3px solid Highlight; }
  .kbd-key--hit { border: 2px solid Highlight; }
  .kbd-key--hit::after { forced-color-adjust: none; background: Highlight; }
  .kbd-key--dead { border: 1px dashed GrayText; color: GrayText; }

  /* Stage states differ only by border colour today. */
  .stage--armed,
  .stage--running { border: 2px solid Highlight; }
  .stage--done { border: 3px double ButtonText; }

  /* The callouts' leading rule is their identity; keep it visible and keep the
     "cannot measure" note the loudest of the three. */
  .note { border-inline-start: 3px solid ButtonText; }
  .note--limit { border-inline-start: 5px double ButtonText; }

  /* Charts are canvas pixels; forced colors cannot recolour them, so the text
     summary that already accompanies every canvas becomes the real output. */
  .chart__canvas { forced-color-adjust: none; }
  .chart__summary.sr-only {
    position: static !important;
    inline-size: auto;
    block-size: auto;
    clip-path: none;
    margin: 0;
    white-space: normal;
  }
}

/* ==========================================================================
   23 — REDUCED MOTION
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    /* Without this a delayed animation still fires — just late and instantly. */
    animation-delay: 0ms !important;
    transition-duration: 0.001ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  .stage--running::after { display: none; }
  .toolcard:hover { transform: none; }
  .btn:active { transform: none; }
  .kbd-key--active { transform: none; }
  .toast { opacity: 1; transform: none; }
  /* The width transition on a progress fill animates on every sample. */
  .bar__fill { transition: none; }
}

/* ==========================================================================
   24 — PRINT
   ========================================================================== */

@media print {
  .site-header,
  .site-footer,
  .site-nav,
  .nav-menu,
  .nav-toggle,
  .theme-toggle,
  .skip-link,
  .crumbs,
  .toast-region,
  .btnrow,
  .btn { display: none !important; }

  /* Force the light palette so results stay legible on paper. */
  :root,
  html[data-theme="light"],
  html[data-theme="dark"] {
    --bg: #ffffff;
    --bg-2: #ffffff;
    --surface: #ffffff;
    --surface-2: #ffffff;
    --border: #b8b8b8;
    --border-strong: #6b6b6b;
    --text: #000000;
    --text-2: #333333;
    --text-3: #555555;
    --accent: #0b5f76;
    --accent-2: #0b5f76;
    --amber: #7a4a06;
    --good: #0a5c42;
    --warn: #7a4a06;
    --bad: #93122f;
    --shadow: none;
    --shadow-sm: none;
    --grid-line: transparent;
  }

  html { color-scheme: light; }

  body {
    background: #fff;
    color: #000;
    font-size: 11pt;
  }

  a { color: #000; text-decoration: underline; }
  .prose a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; color: #444; }

  .card,
  .metric,
  .note,
  .chart,
  .tbl-scroll,
  .disclose { border: 1px solid #999 !important; box-shadow: none !important; break-inside: avoid; }

  .metrics { grid-template-columns: repeat(auto-fit, minmax(min(120px, 100%), 1fr)); }
  .stage { background: none; border: 1px solid #999; min-block-size: auto; }
  .tbl-scroll { overflow: visible; contain: none; }
  .tbl { min-inline-size: 0; }
  .tbl thead th { position: static; background: #eee; color: #000; }
  .tbl tbody th { position: static; background: #fff; max-inline-size: none; }

  /* Charts print as-is; the required text summary is always visible on paper. */
  .chart__canvas { max-block-size: 8cm; }
  .chart__summary.sr-only {
    position: static !important;
    inline-size: auto;
    block-size: auto;
    clip-path: none;
    margin: 0;
    white-space: normal;
  }

  .disclose:not([open]) > :not(summary) { display: block; }
  details { break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }
}

/* ==========================================================================
   24 — CONTENT-PAGE HELPERS (added during UI/UX remediation)
   ========================================================================== */

/* A reading-measure cap for a paragraph that is NOT inside .prose — e.g. the
   group intros on /guides/, which sat in a bare .wrap and ran to 166 characters
   per line on the very page whose job is helping people choose where to go. */
.measure { max-inline-size: var(--maxw-prose); }

/* Related-tool links on the guides hub. These were `.tiny.muted` — 12px grey,
   the smallest, dimmest text on the page — despite being the only in-context
   route from a guide cluster to the tool it describes. They are actions, so
   they now look like actions. */
.hub-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 0.55rem;
  font-size: 0.875rem;
}
.hub-tools__label {
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.75rem;
}
.hub-tools a {
  display: inline-block;
  padding: 0.3rem 0.7rem;
  min-block-size: 32px;
  border: 1px solid var(--border-ui);
  border-radius: 999px;
  color: var(--accent);
  text-decoration: none;
  line-height: 1.5;
}
.hub-tools a:hover { background: var(--surface-2); color: var(--text); }
@media (pointer: coarse) { .hub-tools a { min-block-size: 44px; padding-block: 0.6rem; } }

/* The dot separators between the old inline links are noise once they are chips. */
.hub-tools { --sep-hidden: 1; }

/* --------------------------------------------------------------------------
   Reading measure for ALL text blocks, not just .prose.
   Audit measurement at 1920px found helper text, callouts, chart summaries and
   card body copy running to 106-261 characters per line — the .prose cap only
   protected long-form guide copy, while the small print that explains how a tool
   works was the widest text on the page.
   `ch` resolves against each element's OWN font-size, so a 12px .tiny note gets a
   proportionally narrower cap than 16px body copy and both land near 75-85
   characters. Inside a card, tile or grid cell the parent is already narrower, so
   this is a no-op there rather than a layout change.
   -------------------------------------------------------------------------- */
main p,
main li,
main dd,
main .envline,
main .chart__summary { max-inline-size: var(--maxw-prose); }

/* Opt-outs: these are laid out, not read in lines. */
main .metrics p,
main .toolgrid p,
main .btnrow,
main table p,
main .kbd-map p { max-inline-size: none; }
