/* Theme tokens (see docs/ux-foundations.md). Alpha ships one dark theme. */
:root {
  color-scheme: dark;

  --bg: #0c1218;
  --paper: #11151d;
  --panel: #141a23;
  --ink: #eef1f6;
  --muted: #8892a4;
  --line: #222a36;
  --soft: #1a212b;
  --accent: #7da9f7;
  --accent-soft: #aec3f8;
  --accent-deep: #4a7fe6;
  --good: #22c55e;
  --error: #fb7185;
  --focus: var(--accent);
  --shadow: 0 20px 50px rgba(0, 0, 0, .5);
  --shadow-sm: 0 4px 14px rgba(0, 0, 0, .35);
  --brand-gradient: linear-gradient(90deg, var(--accent-soft), var(--accent-deep));

  --radius-card: 20px;
  --radius-control: 8px;
  --radius-pill: 999px;

  --font-display: Archivo, sans-serif;
  --font-body: "IBM Plex Sans", sans-serif;
  --font-mono: "IBM Plex Mono", monospace;

  --left-pinky: #fb7185;
  --left-ring: #f59e0b;
  --left-middle: #22c55e;
  --left-index: #22d3ee;
  --right-index: #6366f1;
  --right-middle: #a855f7;
  --right-ring: #ec4899;
  --right-pinky: #fb923c;
  --thumb: #94a3b8;
}

* { box-sizing: border-box; }
body {
  margin: 0;
  overflow-x: hidden;
  color: var(--ink);
  background: var(--bg);
  font-family: var(--font-body);
}
button { font: inherit; color: inherit; }
h1, h2, h3, p { margin: 0; }
.hidden { display: none !important; }

.app-shell { max-width: 920px; margin: auto; padding: 0 28px 64px; }

/* ---------- typography roles ---------- */

/* section heading */
.result-card h2, .device-notice-card h2, .stats-header h2 {
  font: 700 24px/1.15 var(--font-display);
  letter-spacing: -.03em;
  margin: 10px 0;
}
/* secondary copy */
.result-card p, .device-notice-card p, .stats-empty {
  color: var(--muted);
  font-size: 14px;
  line-height: 1.6;
  max-width: 460px;
}
/* captions */
.mode-caption, .stats-caption, .input-hint { color: var(--muted); font-size: 13px; }
/* small labels / kickers / metric labels */
.section-kicker, .drill-meta, .stat span, .result-number small, .hand-label, .stats-table th {
  color: var(--muted);
  font: 600 11px var(--font-mono);
  letter-spacing: .1em;
}
/* metric values */
.stat strong, .result-number { font-family: var(--font-mono); font-weight: 500; font-variant-numeric: tabular-nums; }

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

/* primary action */
.primary-button {
  display: inline-block;
  border: 0;
  background: linear-gradient(100deg, var(--accent), var(--accent-deep));
  color: #06101f;
  border-radius: var(--radius-pill);
  padding: 12px 20px;
  cursor: pointer;
  font: 700 13px var(--font-mono);
  text-decoration: none;
  transition: filter .15s ease;
}
.primary-button:hover { filter: brightness(1.08); }
.primary-button:active { transform: translateY(1px); }
/* The primary action's keyboard shortcut, quieted against the gradient fill
   by opacity rather than a colour token, which would fight it. */
.action-key { font-weight: 500; opacity: .6; }

/* quiet / secondary action */
.restart-button, .device-override-button, .secondary-button {
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--muted);
  font: 500 13px var(--font-mono);
  border-radius: var(--radius-control);
}
.restart-button:hover, .device-override-button:hover, .secondary-button:hover { color: var(--ink); }
.secondary-button { padding: 12px 14px; text-decoration: underline; text-underline-offset: 3px; }

/* outlined selectable pill: primary nav, level picker */
.nav-button, .level-button {
  border: 1px solid var(--line);
  background: var(--panel);
  border-radius: var(--radius-pill);
  cursor: pointer;
  color: var(--muted);
}
.nav-button:hover, .level-button:hover { color: var(--ink); border-color: var(--ink); }
.nav-button[aria-current="page"], .level-button.active { color: var(--ink); border-color: var(--ink); background: var(--soft); }
.nav-button { padding: 8px 16px; font: 600 13px var(--font-body); }
/* Benchmark is the product's primary action, so it is the one pill drawn in
   the brand blue -- the same blue as the mark's caret -- while every other
   section stays quiet. Still an outline, not a filled hero button; the
   current state adds a tinted fill so selection reads as more than colour. */
.nav-button-lead { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, var(--line)); }
.nav-button-lead:hover { color: var(--accent-soft); border-color: var(--accent); }
.nav-button-lead[aria-current="page"] {
  color: var(--accent-soft);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 15%, var(--panel));
}
.level-button { display: inline-flex; align-items: center; gap: 3px; padding: 7px 10px; font: 500 12px var(--font-mono); }
/* mastered stages carry a check, locked ones can't be picked */
.level-button.mastered span { color: var(--good); }
.level-button:disabled { opacity: .45; border-style: dashed; cursor: not-allowed; }
.level-button:disabled:hover { color: var(--muted); border-color: var(--line); }

/* segmented tabs: section sub-nav, Stats metric selector */
.mode-tabs {
  display: inline-flex; gap: 2px; padding: 4px;
  background: var(--soft); border-radius: var(--radius-pill);
  transition: opacity .18s ease;
}
/* A control that steps back for the duration of a run: greyed out rather than
   gone, so the row keeps both its layout space and its shape instead of
   leaving a gap above the typing line. app.js's setPhase also marks it inert,
   which is what actually stops it being operated. */
.mode-tabs.quiet { opacity: .38; }
.mode-tab {
  border: 0;
  background: transparent;
  padding: 9px 18px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  color: var(--muted);
  font: 600 14px var(--font-body);
  transition: color .15s ease, background .15s ease, box-shadow .15s ease;
}
.mode-tab:hover { color: var(--ink); }
.mode-tab.active { background: var(--panel); color: var(--ink); box-shadow: var(--shadow-sm); }
/* the standardized-mode marker: lighter and smaller than the mode
   label it trails, so it reads as a note on that tab rather than as another
   choice competing with it */
.mode-tab-tag { font-size: 12px; font-weight: 500; color: var(--muted); white-space: nowrap; }

/* ---------- topbar ---------- */
.topbar { height: 78px; display: flex; align-items: center; justify-content: space-between; }
.primary-nav { display: flex; align-items: center; gap: 8px; }
/* No box of its own, so the one semantic h1 on the page (index.html) sits in
   the topbar's flex row exactly as the bare brand button used to. */
.brand-heading { display: contents; }
.brand {
  color: var(--ink);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  font: 800 30px var(--font-display);
  letter-spacing: -.02em;
  border-radius: var(--radius-control);
}
/* The svg's own box is the lockup: its right edge is the caret, so .brand's
   gap is the space between caret and wordmark, and its height includes the
   key's base edge. Height and font-size are a matched pair -- changing one
   without the other breaks the proportion the mark was drawn at. */
.brand-mark { --mark-height: 52px; height: var(--mark-height); width: auto; flex: none; overflow: visible; }
.brand-caret { fill: var(--accent); }
.brand-caret-glow { fill: var(--accent); }
/* The key presses into its own base edge, which is the one thing a real
   keycap does; the skirt stays put so the travel is visible. The translate
   resolves in viewBox units, not CSS pixels, so 5 is half the skirt's depth
   however large the mark is drawn -- about 2px of travel at 52px. */
.brand-key { transition: transform .12s ease; }
.brand:hover .brand-key { transform: translateY(5px); }
.brand-word-lab { color: var(--accent-soft); }

/* ---------- workspace / nav ---------- */
.workspace { padding-top: 6px; }
.panel-topline { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; }
.level-picker { display: flex; gap: 5px; flex-wrap: wrap; }
.panel-subline { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin: 14px 2px 0; }
.info-button {
  flex: none; width: 22px; height: 22px; padding: 0; border-radius: 50%;
  border: 1px solid var(--line); background: none; cursor: pointer;
  color: var(--muted); font: italic 600 13px/1 var(--font-display);
}
.info-button:hover, .info-button[aria-expanded="true"] { color: var(--ink); border-color: var(--ink); }
.mode-caption#modeInfo { margin: 8px 2px 0; max-width: 620px; line-height: 1.5; }
.lesson-summary { display: flex; align-items: baseline; gap: 12px; min-width: 0; }
.lesson-label { color: var(--ink); font: 500 12px var(--font-mono); letter-spacing: .04em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---------- cards: typing, result/empty state, device notice ---------- */
.typing-card, .result-card, .device-notice-card {
  position: relative;
  overflow: hidden;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow);
  padding: 32px;
}
.typing-card::before, .result-card::before, .device-notice-card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--brand-gradient);
}
.typing-card, .result-card { min-height: 250px; }

/* ---------- typing card ---------- */
.training-panel { margin-top: 22px; }
.typing-card { padding: 30px 30px 18px; }
.drill-meta { display: flex; justify-content: space-between; }
/* A timed Benchmark's final BENCHMARK_COUNTDOWN_WARN_SECONDS (app.js): the
   countdown grows noticeably bigger and bolder so the ending doesn't feel
   surprising, without the alarm connotation a colour change would carry and
   without any motion to distract from the text being typed. */
#progressLabel.progress-label--countdown { color: var(--ink); font-size: 15px; font-weight: 700; letter-spacing: .02em; transition: font-size .15s ease; }
.drill-note { color: var(--muted); font: 500 12.5px var(--font-mono); letter-spacing: .03em; margin-top: 10px; }
/* Learn Practice's live target row. It sits above the drill rather than in
   the .stats-row the session controls dim into, because it is the one live
   readout a qualifying round is actually typed against. Nothing here
   animates or transitions: it updates four times a second, so any movement
   beyond the digits would pull the eye off the typing line. */
.target-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px; margin-top: 10px; }
.target-row-label { color: var(--muted); font: 600 11px var(--font-mono); letter-spacing: .1em; }
.target-item {
  display: flex;
  align-items: baseline;
  gap: 6px;
  color: var(--muted);
  font: 500 12.5px var(--font-mono);
  letter-spacing: .03em;
}
/* Fixed-width values, right-aligned against the target they are read
   against, so the row doesn't shuffle sideways as a number gains a digit
   mid-round. */
.target-value {
  min-width: 4ch;
  text-align: right;
  color: var(--ink);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.target-mark { min-width: 1ch; }
/* The result card's own tone colours: met is good, not-yet is the accent the
   card's needs-work mark uses (never the error red -- a round still running
   has missed nothing), and a requirement with no standing yet stays quiet. */
.target-item.met .target-value, .target-item.met .target-mark { color: var(--good); }
.target-item.short .target-mark { color: var(--accent); }
.target-item.pending .target-value { color: var(--muted); }
.drill-text {
  font: 500 clamp(19px, 2.4vw, 28px)/1.7 var(--font-mono);
  letter-spacing: .01em;
  margin: 22px 0 14px;
  min-height: 98px;
  height: 98px;
  overflow: hidden;
  white-space: nowrap;
  outline: none;
}
/* The reading position moves by sliding this box, not by scrolling the window
   above: a transform transition re-targets mid-flight, so typing faster than
   the glide redirects it instead of queueing another scroll behind it. */
.drill-viewport { position: relative; transition: transform .12s cubic-bezier(.22, .61, .36, 1); }
/* One persistent caret for the whole run, covering the current character's
   box (ui.js places and sizes it). Drills draw it as that box, a target to
   hit; Benchmark narrows it to a bar (below). Its glide matches the passage's
   exactly: on a single line the two cancel, so the box holds still while the
   text flows through it. It comes before the characters in the markup, so it
   paints under them. */
.drill-caret {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--char-width, 0);
  height: var(--char-height, 0);
  border-radius: 4px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
  transition: transform .12s cubic-bezier(.22, .61, .36, 1);
}
.drill-caret.error {
  background: color-mix(in srgb, var(--error) 16%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--error) 55%, transparent);
}
.drill-caret.off { visibility: hidden; }
/* ui.js switches the glide off for a move that isn't one: a fresh passage, a
   relayout that has already put the text somewhere else, or the caret landing
   on a new line. Important so it outranks Benchmark's own caret timing. */
.drill-viewport.instant, .drill-caret.instant { transition: none !important; }
/* No vertical padding: a taller box than the line-height would make the line
   pitch something other than the line-height the reading layout is sized and
   slid by. */
.char { position: relative; display: inline-block; padding: 0 2px; border-radius: 4px; color: var(--muted); }
.char.current { color: var(--ink); }
.char.correct { color: var(--good); }
.char.incorrect { color: var(--error); background: color-mix(in srgb, var(--error) 13%, transparent); }
.char.current.error { color: var(--error); animation: char-shake .22s ease; }
.char-cues {
  position: absolute;
  left: 0;
  top: 100%;
  margin-top: 5px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  pointer-events: none;
  z-index: 1;
}
.error-overlay {
  position: relative;
  background: var(--error);
  color: var(--panel);
  font: 700 13px var(--font-mono);
  line-height: 1;
  padding: 2px 6px;
  border-radius: 5px;
  white-space: nowrap;
}
.error-overlay::after {
  content: '';
  position: absolute;
  bottom: 100%;
  left: 6px;
  border: 4px solid transparent;
  border-bottom-color: var(--error);
}
/* Focused assist: the current character's finger/Shift, right under it */
.finger-cue {
  display: flex;
  align-items: center;
  gap: 4px;
  color: var(--muted);
  background: var(--soft);
  border-radius: 5px;
  padding: 2px 6px;
  font: 500 12px/1.2 var(--font-mono);
  white-space: nowrap;
}
.finger-cue i { width: 7px; height: 7px; border-radius: 50%; background: var(--finger); }
/* New keys leans on this cue to teach the motion, so it reads at a
   glance in the finger's own color rather than as a quiet muted chip. */
.training-panel[data-assist="teaching"] .finger-cue {
  color: var(--ink);
  background: color-mix(in srgb, var(--finger) 15%, var(--panel));
  border: 1px solid color-mix(in srgb, var(--finger) 50%, var(--line));
  padding: 3px 8px;
  font: 600 13.5px/1.2 var(--font-mono);
}
.training-panel[data-assist="teaching"] .finger-cue i { width: 9px; height: 9px; }
@keyframes char-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-3px); }
  75% { transform: translateX(3px); }
}
.input-hint { border-top: 1px solid var(--soft); padding-top: 12px; min-height: 30px; }
/* Learner escape hatch: offered on the Benchmark entry state only, and
   never inside a run -- an active attempt keeps the typing line alone. */
.starter-hint { color: var(--muted); font-size: 13px; margin-top: 3px; }
.starter-hint .secondary-button { padding: 0; font: inherit; }
.typing-card[data-phase="running"] .starter-hint { visibility: hidden; }
/* New keys' way out of its endless drill. Shaped like .starter-hint but
   deliberately not quieted while a run is on: it is offered mid-run, which is
   the only time it is any use. */
.focus-handoff { color: var(--ink); font-size: 13px; margin-top: 3px; }
.focus-handoff .secondary-button { padding: 0; font: inherit; }
/* Quiet the secondary controls while a run is on by dropping the Assist chip
   background, never their text contrast: they stay actionable and legible. */
.typing-card[data-phase="running"] .assist-tabs.mode-tabs { background: transparent; transition: background .15s ease; }
.typing-card[data-phase="running"] .assist-picker:hover .assist-tabs.mode-tabs,
.typing-card[data-phase="running"] .assist-picker:focus-within .assist-tabs.mode-tabs { background: var(--soft); }
.stats-row {
  display: flex;
  flex-wrap: wrap;
  gap: 16px 26px;
  align-items: end;
  border-top: 1px solid var(--soft);
  margin-top: 15px;
  padding-top: 14px;
}
.stat span { display: block; }
.stat strong { display: block; font-size: 21px; margin-top: 4px; }
.restart-button { margin-left: auto; padding: 8px 0; }

/* assist picker: compact, secondary to the typing line */
.assist-picker { display: flex; align-items: center; gap: 8px; }
.assist-label { color: var(--muted); font: 600 11px var(--font-mono); letter-spacing: .1em; }
.assist-tabs.mode-tabs { padding: 3px; }
.assist-tabs .mode-tab { padding: 5px 11px; font-size: 12.5px; }
/* the usual raised --panel chip would vanish on the --panel card */
.assist-tabs .mode-tab.active { background: var(--bg); }
/* while a round is under way the controls step back; Review intro returns
   once it is paused or finished */
.typing-card .stats-row { transition: opacity .2s ease; }
.typing-card[data-phase="running"] .stats-row { opacity: .55; }
.typing-card[data-phase="running"] .stats-row:is(:hover, :focus-within) { opacity: 1; }
.typing-card[data-phase="running"] .intro-button { visibility: hidden; }

/* ---------- result / empty state ---------- */
.result-grid { display: flex; gap: 32px; margin: 22px 0 26px; flex-wrap: wrap; }
.restart-button + .intro-button { margin-left: 14px; padding: 8px 0; }

/* ---------- motor-concept introduction ---------- */
.intro-keys { list-style: none; margin: 18px 0 22px; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 8px; }
.intro-keys li { display: flex; align-items: center; gap: 12px; }
.intro-keycap {
  flex: none;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 1px solid color-mix(in srgb, var(--finger) 72%, var(--line));
  background: color-mix(in srgb, var(--finger) 34%, var(--paper));
  border-radius: var(--radius-control);
  color: var(--ink);
  font: 600 15px var(--font-mono);
}
.intro-detail { color: var(--muted); font-size: 13.5px; line-height: 1.45; }
.intro-detail strong { color: var(--ink); font-weight: 600; }
/* What the intro's second action does, under the actions it explains.
   Scoped through .result-card so it outranks that card's own paragraph size. */
.result-card .intro-skip-note { color: var(--muted); font-size: 13px; line-height: 1.5; margin-top: 12px; }

.result-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
/* the quietest action on a result card: a place to go look at
   something rather than a next step, so it drops the underline the actions
   beside it carry and the card reads primary → improvement → quiet path.
   Set apart from them only while the row still fits on one line: once the
   actions wrap, a lone flush-right button reads as a layout slip. */
.result-actions .quiet-action { text-decoration: none; }
.result-actions .quiet-action:hover { text-decoration: underline; }
@media (min-width: 600px) { .result-actions .quiet-action { margin-left: auto; } }
.result-number { font-size: 25px; }
.result-number small { display: block; margin-bottom: 6px; }
/* the metric this activity leads with; supporting metrics sit beside
   it at the usual size */
.result-number.primary { font-size: 44px; line-height: 1; letter-spacing: -.02em; }
.result-grid:has(.result-number.primary) { align-items: baseline; }

/* went well (✓) / needs work (→) / context (·) */
.result-notes { list-style: none; margin: 0 0 22px; padding: 0; display: flex; flex-direction: column; gap: 8px; max-width: 560px; }
.result-notes li { display: flex; gap: 10px; font-size: 14px; line-height: 1.5; }
.note-mark { flex: none; width: 14px; font: 600 14px var(--font-mono); text-align: center; }
.result-notes li.good .note-mark { color: var(--good); }
.result-notes li.work .note-mark { color: var(--accent); }
.result-notes li.info .note-mark { color: var(--muted); }

.result-footnote { color: var(--muted); font: 500 12px var(--font-mono); margin-bottom: 22px; }

/* ---------- external speed distribution ---------- */
/* Every colour is a theme token and every stroke scales with the viewBox,
   so the plot follows whatever theme is active and needs no animation. */
.result-distribution { margin: 0 0 22px; max-width: 560px; }
.result-distribution h3 { font: 700 15px var(--font-display); letter-spacing: -.01em; margin: 0 0 4px; }
.dist-plot { display: block; width: 100%; max-width: 420px; height: auto; margin: 14px 0 2px; overflow: visible; }
.dist-area { fill: color-mix(in srgb, var(--accent) 22%, transparent); stroke: var(--accent); stroke-width: 1.5; stroke-linejoin: round; }
.dist-axis line { stroke: var(--line); stroke-width: 1; }
.dist-axis .dist-tick { fill: var(--muted); font: 500 10px var(--font-mono); text-anchor: middle; }
.dist-marker line { stroke: var(--ink); stroke-width: 1.5; stroke-dasharray: 3 3; }
.dist-marker path { fill: var(--ink); }
.dist-marker text { fill: var(--ink); font: 600 10px var(--font-mono); letter-spacing: .1em; text-anchor: middle; }
/* the comparison itself, carrying the weight the plot is drawn for */
.dist-caption { font: 600 16px/1.45 var(--font-display); letter-spacing: -.01em; }
.dist-qualifier { color: var(--muted); font-size: 13px; line-height: 1.5; margin-top: 2px; }
.result-source { margin-top: 10px; }
.result-source summary { color: var(--muted); font: 600 12px var(--font-display); letter-spacing: .02em; cursor: pointer; }
.result-source p { color: var(--muted); font-size: 13px; line-height: 1.5; margin-top: 8px; }
.result-source a { color: var(--accent); }

/* ---------- save-failure notice ----------
   Deliberately quiet: a single advisory line in the shell, not a modal or an
   error page. Typing still works when it shows -- only persistence is broken
   -- so it must not read as a failure of the app or compete with the typing
   surface for attention. */
.save-notice {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 0 0 14px;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--error);
  border-radius: var(--radius-control);
  background: var(--panel);
  font-size: 15px;
}
.save-notice p { margin: 0; flex: 1; color: var(--muted); }
.save-notice-dismiss {
  flex: none;
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: none;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
}
.save-notice-dismiss:hover { color: var(--ink); }

/* ---------- phone landing state (see docs/device-support.md) ----------
   What a touch-first visitor gets in place of the trainer: what WPM Lab is
   and where to go to use it, in as few words as that takes. It only ever
   renders under the feature query at the end of this file. */
.device-notice { display: none; }
.device-notice-card p { margin-bottom: 22px; }
.device-override-button {
  display: block;
  margin-top: 16px;
  text-align: left;
  text-decoration: underline;
  text-decoration-color: var(--line);
  padding: 0;
}
.device-override-button:hover { text-decoration-color: var(--ink); }

/* Explicit "I have a keyboard" override — the feature query below can't
   detect a keyboard case/Bluetooth keyboard, so it defaults to the notice
   but this lets a keyboard-equipped user opt back into the trainer. */
body.device-keyboard-override .workspace { display: block; }
body.device-keyboard-override .device-notice { display: none; }
body.device-keyboard-override .stats-panel, body.device-keyboard-override .train-preview { border-top: 0; }

/* ---------- keyboard ---------- */
.keyboard-wrap { margin-top: 38px; border-top: 1px solid var(--line); padding-top: 26px; }
/* One assistance surface at a time: while typing, the guide keyboard
   shows only in Full assist and the hands guide never; a lesson intro shows
   both, whatever the assist mode. */
.training-panel:not([data-assist="full"]) .keyboard-wrap, .hands-guide { display: none; }
.training-panel:has(#introCard:not(.hidden)) .keyboard-wrap { display: block; }
.training-panel:has(#introCard:not(.hidden)) .hands-guide { display: flex; }
/* New keys is the one drill that shows both at once: the guide
   keyboard for where the key is, the hands guide for which finger takes it,
   so the motion is learnable without looking down at the keyboard. */
.training-panel[data-assist="teaching"] .keyboard-wrap { display: block; }
.training-panel[data-assist="teaching"] .hands-guide { display: flex; }
.hands-guide { justify-content: center; gap: clamp(28px, 6vw, 78px); padding-bottom: 14px; }
.hand-group { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.hand { display: flex; align-items: flex-end; gap: 6px; height: 44px; }
.right-hand { transform: scaleX(-1); }
.hand i {
  display: block;
  width: 13px;
  height: 24px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--finger) 55%, var(--line));
  background: color-mix(in srgb, var(--finger) 20%, var(--paper));
  transition: transform .16s ease, translate .07s ease-out, background .16s ease, box-shadow .16s ease;
}
.hand i:nth-child(2) { height: 32px; }
.hand i:nth-child(3) { height: 40px; }
.hand i:nth-child(4) { height: 34px; }
.hand i:nth-child(5) { width: 16px; height: 19px; margin-left: 6px; }
.hand i.active {
  background: var(--finger);
  border-color: var(--finger);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--finger) 17%, transparent), 0 5px 12px color-mix(in srgb, var(--finger) 25%, transparent);
  transform: translateY(-5px);
}
/* A New keys press (ui.js's pressNewKey): the finger goes down with its key
   and stays down while the key is held. `translate` composes with the lit
   finger's raised `transform` instead of replacing it. */
.hand i.pressed { translate: 0 5px; }
.keyboard { background: var(--paper); border: 1px solid var(--line); border-radius: 16px; padding: 16px; display: grid; gap: 7px; }
.key-row { display: grid; grid-template-columns: repeat(11, 1fr); gap: 7px; }
.key {
  border: 1px solid color-mix(in srgb, var(--finger) 40%, var(--line));
  background: color-mix(in srgb, var(--finger) 14%, var(--paper));
  color: var(--ink);
  height: 37px;
  min-width: 0;
  display: grid;
  place-items: center;
  border-radius: var(--radius-control);
  font: 500 13px var(--font-mono);
  transition: opacity .2s ease, background .2s ease, border-color .2s ease, translate .07s ease-out, box-shadow .07s ease-out;
}
/* The guide keyboard's keys carry the same base edge the brand mark's keycap
   has, so the teaching surface and the logo are recognisably the same object.
   Inset rather than a border, so it costs no layout height. The Stats heatmap
   does not take it: that grid is a data surface, and a dark lip across the
   bottom of every cell would sit on top of the fill the colour has to carry. */
.keyboard.staggered .key, .intro-keycap { box-shadow: inset 0 -2px 0 rgba(0, 0, 0, .34); }
/* Held down by a New keys press, with the finger above it. */
.keyboard.staggered .key.pressed { translate: 0 2px; box-shadow: inset 0 0 0 rgba(0, 0, 0, .34); }
.key.space { grid-column: 3 / span 7; }
/* Guide keyboard: real-keyboard stagger in quarter-key columns. Gaps are key
   margins, not grid gaps, so every row lines up to the same width. */
.keyboard.staggered .key-row { grid-template-columns: repeat(60, 1fr); gap: 0; }
.keyboard.staggered .key { grid-column: span var(--width, 4); margin: 0 3.5px; }
.keyboard.staggered .key.space { grid-column: 16 / span 25; }
.key.edge-key { font-size: 11px; place-items: center start; padding: 0 8px; }
.key.off-level { opacity: .28; }
.key.new-key {
  background: color-mix(in srgb, var(--finger) 34%, var(--paper));
  border-color: color-mix(in srgb, var(--finger) 72%, var(--line));
  font-weight: 600;
}
/* the key (and Shift) to press now, filled with its finger's color */
.key.current-key { background: var(--finger); border-color: var(--finger); color: var(--bg); font-weight: 600; outline: 2px solid var(--ink); outline-offset: 2px; }
.legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 14px; justify-content: center; }
.legend-item { display: flex; align-items: center; gap: 5px; color: var(--muted); font-size: 12px; }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--finger); }

/* ---------- reading-first layout: benchmark ---------- */
/* Benchmark's continuous prose reads better wrapped in a comfortable measure
   than in the single line every curriculum-bounded drill uses, with the
   passage slid vertically (renderDrill) to keep the current line in view.
   trainingPanel's `data-reading` hook is set in app.js's begin(). */
.training-panel[data-reading="true"] .drill-text {
  white-space: normal;
  overflow: hidden;
  height: calc(3 * 1.7em);
  max-width: 68ch;
  margin-left: auto;
  margin-right: auto;
}
/* Each word, with its trailing space, is one atomic box (ui.js's
   groupIntoWords); the `<wbr>` between two of them is the only place a line
   can break. */
.training-panel[data-reading="true"] .word { display: inline-block; white-space: nowrap; }
/* The space between words is an ordinary character box like every other, so
   the line box cannot change shape as the current position crosses it. It sits
   inside the preceding word box (ui.js's groupIntoWords), where a collapsible
   space would be trimmed at the box's edge and leave the words touching. */
.training-panel[data-reading="true"] .char.space { white-space: pre; }
/* No live WPM/accuracy while Benchmark runs -- only the remaining time the
   drill-meta already shows. */
.training-panel[data-mode="benchmark"] .stats-row .stat { display: none; }
/* New keys shows none either: it's motor learning, not measurement,
   and a live accuracy percentage is exactly the pressure it shouldn't add. */
.training-panel[data-mode="focus"] .stats-row .stat { display: none; }
/* A blinking caret marks the reading position instead of the boxed
   highlight drills use for their current target -- this is a passage to
   read through, not a key to hit. */
.training-panel[data-mode="benchmark"] .drill-caret {
  width: 2px;
  height: calc(var(--char-height, 0) - 4px);
  margin: 2px 0 0 -1px;
  border-radius: 1px;
  background: var(--accent);
  box-shadow: none;
  animation: caret-blink 1s step-end infinite;
  /* Shorter than the passage glide: the caret leads the eye to the next
     character, the text follows it. */
  transition: transform .08s cubic-bezier(.22, .61, .36, 1), height .08s linear;
}
@keyframes caret-blink { 50% { background: transparent; } }

/* ---------- Train: what's coming ----------
   A single `.result-card` in its own section, outside the workspace: it needs
   no keyboard, so it shows on touch devices too, where the trainer can't run.
   Two columns -- what the feature is, and the user's own weak spots it would
   open on -- over a footer spanning both, so the card is a composition rather
   than copy in a wide box, and the stacked order stays right at 800px. The
   accent wash is a background layer on the card rather than a pseudo element,
   so it paints under the text without a stacking-context trick. */
.train-preview { padding-top: 30px; }
.train-preview .result-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(230px, 280px);
  gap: 44px;
  align-items: start;
  padding: 38px;
  background-image: radial-gradient(90% 120% at 100% 0%, color-mix(in srgb, var(--accent) 13%, transparent), transparent 70%);
}
/* the one paragraph with real weight on the page: --ink, a step above body
   copy -- qualified on the card so it outranks the shared `.result-card p` */
.train-preview .train-preview-lede {
  color: var(--ink);
  font-size: 16px;
  line-height: 1.65;
  max-width: 48ch;
  margin-top: 12px;
}
/* spans both columns, so the quiet closing line reads as the card's footer
   and still follows the data once the two columns stack */
.train-preview-footer {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px 24px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}
.train-preview .train-preview-note { max-width: 52ch; }
.train-preview .secondary-button { padding: 0; white-space: nowrap; }

/* the user's own data, set apart on the page's quietest surface */
.train-preview-targets {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  padding: 16px;
}
.train-preview .train-preview-eyebrow {
  color: var(--muted);
  font: 600 11px var(--font-mono);
  letter-spacing: .1em;
  text-transform: uppercase;
}
.train-preview .train-preview-pending { margin-top: 10px; }
.train-preview-chips { list-style: none; margin: 12px 0 0; padding: 0; }
/* one weak spot per row: the character leads, why it is listed sits beside it */
.train-preview-chip {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 0;
  border-top: 1px solid var(--line);
}
.train-preview-chip:first-child { border-top: 0; padding-top: 2px; }
.train-preview-chip .chip-label { color: var(--ink); font: 500 17px var(--font-mono); }
.train-preview-chip .chip-detail { color: var(--muted); font: 500 11px var(--font-mono); letter-spacing: .08em; text-transform: uppercase; }

@media (max-width: 800px) {
  .train-preview .result-card { grid-template-columns: 1fr; gap: 26px; padding: 28px 22px; }
}

/* ---------- About ----------
   The one surface in the Alpha allowed to be more expressive than the
   trainer, so it is the only place with entrance motion. Every moving part is
   a CSS animation whose *base* state is its finished state, which buys two
   things: the global prefers-reduced-motion rule below switches the whole
   page to its static form with nothing extra to write, and the entrance
   replays on each visit for free, because clearing `.hidden` restarts
   animations. No JS drives any of it, so Type's frame budget is untouched. */
.about-panel { padding-top: 30px; max-width: 560px; }
/* one shared entrance, staggered down the page so blocks arrive in reading
   order rather than all at once */
.about-panel > * { animation: about-rise .45s cubic-bezier(.2, .7, .3, 1) both; }
.about-panel > :nth-child(2) { animation-delay: .06s; }
.about-panel > :nth-child(3) { animation-delay: .12s; }
.about-panel > :nth-child(4) { animation-delay: .18s; }
.about-panel > :nth-child(5) { animation-delay: .24s; }
@keyframes about-rise { from { opacity: 0; transform: translateY(10px); } }

.about-hero h2 {
  font: 800 clamp(28px, 5vw, 38px)/1.12 var(--font-display);
  letter-spacing: -.035em;
  margin: 10px 0 0;
  background: var(--brand-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.about-lede { color: var(--ink); font-size: 16px; line-height: 1.65; margin-top: 16px; }

.about-block { margin-top: 44px; padding-top: 28px; border-top: 1px solid var(--line); }
.about-block h3 { font: 700 22px var(--font-display); letter-spacing: -.02em; margin: 0 0 14px; }
.about-block p { color: var(--muted); font-size: 14.5px; line-height: 1.7; }
.about-block p + p { margin-top: 12px; }
.about-block strong { color: var(--ink); font-weight: 600; }

/* The acronym typing itself out. `.about-expand-reveal` is clipped to a width
   that steps up one character at a time, and the caret is its inline sibling
   rather than a child, so it rides along the growing edge instead of being
   clipped with the text. Both sit in the mono face, which is the whole reason
   16ch is exactly "words per minute" and the caret lands on the last glyph. */
.about-expand {
  display: flex;
  align-items: baseline;
  gap: 14px;
  margin: 20px 0;
  font-family: var(--font-mono);
}
.about-expand-acronym {
  flex: none;
  color: var(--accent);
  font: 600 20px var(--font-mono);
  letter-spacing: .06em;
}
.about-expand-reveal {
  width: 16ch;
  overflow: hidden;
  white-space: nowrap;
  color: var(--ink);
  font: 500 20px var(--font-mono);
  animation: about-type 1.15s steps(16) .25s both;
}
@keyframes about-type { from { width: 0; } }
.about-expand-caret {
  flex: none;
  width: 2px;
  height: 1.15em;
  margin-left: -1px;
  background: var(--accent);
  /* a short, finite blink: the caret settles solid rather than pulsing at the
     reader for as long as the page is open */
  animation: about-caret .85s 5 both;
}
@keyframes about-caret { 50% { opacity: 0; } }

/* five mono cells standing for the fixed five-character word, drawn as keycap
   faces so the unit reads as something typed */
.about-unit { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 14px; margin: 20px 0; }
.about-unit-cells { display: flex; gap: 5px; }
.about-unit-cells i {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--soft);
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, .35);
  color: var(--ink);
  font: 500 14px var(--font-mono);
  font-style: normal;
  animation: about-pop .3s ease both;
}
.about-unit-cells i:nth-child(2) { animation-delay: .06s; }
.about-unit-cells i:nth-child(3) { animation-delay: .12s; }
.about-unit-cells i:nth-child(4) { animation-delay: .18s; }
.about-unit-cells i:nth-child(5) { animation-delay: .24s; }
@keyframes about-pop { from { opacity: 0; transform: translateY(-5px); } }
.about-unit-eq { color: var(--muted); font: 600 11px var(--font-mono); letter-spacing: .1em; text-transform: uppercase; }

/* the four surfaces as the loop they form: the numbered marker is the step,
   the name is the surface the user clicks */
.about-loop { list-style: none; margin: 20px 0 14px; padding: 0; counter-reset: about-step; }
.about-loop-step {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  gap: 4px 14px;
  padding: 14px 0;
  border-top: 1px solid var(--line);
  counter-increment: about-step;
  animation: about-rise .4s ease both;
}
.about-loop-step:first-child { border-top: 0; }
.about-loop-step:nth-child(2) { animation-delay: .05s; }
.about-loop-step:nth-child(3) { animation-delay: .1s; }
.about-loop-step:nth-child(4) { animation-delay: .15s; }
.about-loop-step::before {
  content: counter(about-step);
  grid-row: 1 / span 2;
  color: var(--accent);
  font: 600 11px var(--font-mono);
  letter-spacing: .1em;
  padding-top: 4px;
}
.about-loop-name { color: var(--ink); font: 600 15px var(--font-body); }
.about-loop-copy { color: var(--muted); font-size: 14.5px; line-height: 1.6; }

/* Two sample runs, where the full bar is raw speed and the lit part is what
   counted. The point is read off the shape: the longer bar ends shorter.
   Illustrative arithmetic, labelled as such in the figcaption. */
.about-runs {
  margin: 22px 0;
  padding: 18px 20px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--bg);
}
.about-runs-list { list-style: none; margin: 14px 0 0; padding: 0; }
.about-run {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 72px;
  align-items: center;
  gap: 6px 16px;
  padding: 10px 0;
}
.about-run + .about-run { border-top: 1px solid var(--line); }
.about-run-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
.about-run-name { color: var(--ink); font: 600 14px var(--font-body); }
.about-run-detail { color: var(--muted); font: 500 12px var(--font-mono); }
/* the unlit remainder is the speed the errors gave away */
.about-run-bar {
  grid-column: 1;
  width: var(--raw);
  height: 10px;
  border-radius: var(--radius-pill);
  background: var(--soft);
  box-shadow: inset 0 0 0 1px var(--line);
  animation: about-bar .6s cubic-bezier(.2, .7, .3, 1) .2s both;
}
@keyframes about-bar { from { width: 0; } }
.about-run-kept {
  display: block;
  width: var(--kept);
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--accent-deep), var(--accent));
}
.about-run-read {
  grid-row: 1 / span 2;
  grid-column: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  color: var(--ink);
  font: 500 24px var(--font-mono);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.about-run-read small { color: var(--muted); font: 600 11px var(--font-mono); letter-spacing: .08em; }
.about-runs-note { color: var(--muted); font-size: 13px; line-height: 1.6; margin-top: 14px; }

.about-close { margin-top: 44px; padding-top: 28px; border-top: 1px solid var(--line); }
.about-close-line { color: var(--ink); font: 700 20px/1.35 var(--font-display); letter-spacing: -.02em; max-width: 34ch; }
.about-close .primary-button { margin-top: 18px; }

@media (max-width: 480px) {
  .about-run { grid-template-columns: minmax(0, 1fr) 62px; }
  .about-run-read { font-size: 20px; }
  .about-runs { padding: 16px 14px; }
  .about-expand { gap: 10px; }
  .about-expand-acronym, .about-expand-reveal { font-size: 17px; }
}

/* ---------- stats ---------- */
/* follows the workspace's Progress sub-nav, which already draws the divider */
.stats-panel { padding-top: 30px; }
.stats-header h2 { margin: 8px 0 4px; }
.stats-empty { padding: 18px 0; }
.stats-summary { display: flex; flex-wrap: wrap; gap: 16px 26px; margin: 22px 0 8px; }
.stats-section { margin: 28px 0; }
.stats-section h3 { font: 700 18px var(--font-display); letter-spacing: -.01em; margin: 0 0 14px; }
.metric-tabs.mode-tabs { padding: 3px; }
.metric-tabs .mode-tab { padding: 6px 13px; font-size: 12.5px; }
.stats-caption { margin-top: 10px; }
/* the whole-profile totals open tier 3, so they need air before the first
   disclosure's hairline */
.stats-section[data-tier="detail"] > .stats-caption { margin-bottom: 18px; }
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 32px; }
.stats-keyboard { padding: 12px; gap: 6px; }
.stats-section h4 { font: 700 13px var(--font-display); margin: 0 0 10px; color: var(--muted); }
.detail-body h4 { margin-top: 20px; }

/* tier 1: benchmark (comparable) and curriculum sit side by side, told apart by kicker and border */
.standing-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }
.standing-card { padding: 16px 18px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.standing-card[data-card="curriculum"] { border-style: dashed; }
/* one standing per benchmark mode, switched in place; the card is
   --panel, so the selected chip lifts to --bg like the assist picker's */
.benchmark-tabs.mode-tabs { padding: 3px; flex-wrap: wrap; }
.benchmark-tabs .mode-tab { padding: 6px 11px; font-size: 12.5px; }
.benchmark-tabs .mode-tab.active { background: var(--bg); }
.standing-figure { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; }
.standing-figure strong { font: 500 34px/1 var(--font-mono); letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.standing-figure span { color: var(--muted); font-size: 13px; }
.standing-lede, .standing-note, .focus-finger, .next-action-reason { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.6; }
.standing-card .secondary-button { margin-top: auto; padding-left: 0; padding-right: 0; }
/* the one promoted action: the page's only gradient pill, so the
   recommended step is found without reading, while every other path on the
   page stays a quiet underlined link */
.next-action { margin-top: 18px; display: flex; flex-direction: column; gap: 8px; }
.next-action-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; }

.focus-finger { margin-top: 16px; }
.focus-finger strong { color: var(--ink); font-weight: 600; }

/* tier 3: native disclosures, closed until asked for */
.stats-detail { border-top: 1px solid var(--line); }
.stats-detail:last-child { border-bottom: 1px solid var(--line); }
/* a control, not a heading: body face at text weight, so a section title
   (display face, 18px) is never mistaken for one of these */
.stats-detail summary { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 12px; padding: 14px 2px; cursor: pointer; font: 600 14.5px var(--font-body); }
/* `display: flex` suppresses the native marker, so the disclosure draws its
   own chevron -- without it a quiet summary gives no sign it opens */
.stats-detail summary { list-style: none; }
.stats-detail summary::-webkit-details-marker { display: none; }
.stats-detail summary::after {
  content: ''; align-self: center; margin-left: auto; width: 7px; height: 7px;
  border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted);
  transform: translateY(-2px) rotate(45deg); transition: transform .15s ease, border-color .15s ease;
}
.stats-detail[open] summary::after { transform: translateY(2px) rotate(-135deg); }
.stats-detail summary:hover::after { border-color: var(--ink); }
.detail-hint { color: var(--muted); font: 400 13px var(--font-body); }
.detail-body { padding: 4px 2px 20px; }
.detail-body .metric-tabs { margin-bottom: 12px; }

/* heat fill is the good→error mix tinted toward the panel so the key label
   stays readable (AA) */
.heat-key {
  background: color-mix(in srgb, color-mix(in oklch, var(--good) var(--score, 50%), var(--error)) 55%, var(--panel));
  border-color: color-mix(in srgb, var(--ink) 14%, var(--line));
}
.heat-key.no-data { background: var(--soft); color: var(--muted); border-style: dashed; border-color: var(--line); }

.stats-table { width: 100%; border-collapse: collapse; font: 500 13px var(--font-mono); }
.stats-table th, .stats-table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); }
.stats-table td { color: var(--ink); }
.stats-table th { text-transform: uppercase; }
.sort-button { all: unset; cursor: pointer; user-select: none; border-radius: 3px; }
.sort-button:hover { color: var(--ink); }

.weak-key-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.weak-key-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
}
.weak-key-char { font: 700 14px var(--font-mono); color: var(--ink); }
.weak-key-detail { color: var(--muted); font: 500 12px var(--font-mono); white-space: nowrap; }

/* ---------- focus ---------- */
/* One keyboard-focus ring for every interactive element. Kept after the
   component rules so it wins over same-specificity resets (e.g. .sort-button's
   `all: unset`). */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
/* The drill surface gets focus programmatically on every start; ring the card
   instead of the text so the typing line itself isn't boxed in. */
.drill-text:focus-visible { outline: none; }
.typing-card:has(.drill-text:focus-visible) { border-color: var(--focus); }
.result-card:focus-visible { outline: none; border-color: var(--focus); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
  /* A held New keys press stays where it is rather than travelling; the lit
     key and finger are the guidance here. */
  .hand i.pressed, .keyboard.staggered .key.pressed { translate: none; }
}

@media (max-width: 800px) {
  .app-shell { padding: 0 18px 48px; }
  .panel-subline { flex-wrap: wrap; }
  .lesson-summary { width: 100%; justify-content: space-between; }
  .typing-card { padding: 22px 18px; }
  .keyboard-wrap { margin-top: 30px; padding-top: 20px; }
  .key { height: 32px; font-size: 11px; }
  .keyboard { padding: 10px; gap: 5px; }
  .key-row { gap: 4px; }
  .keyboard.staggered .key { margin: 0 2px; }
  .key.edge-key { font-size: 10px; padding: 0 4px; }
  .hands-guide { gap: 34px; }
  .hand i { width: 12px; }
  .hand i:nth-child(5) { width: 15px; }
  .stats-grid, .standing-grid { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
  .stats-row { gap: 14px; }
  .stat strong { font-size: 19px; }
  .weak-key-list li { flex-wrap: wrap; }
  .result-card, .device-notice-card { padding: 24px 20px; }
}

/* Touch-only, no-hover, phone-sized viewport: no physical keyboard to type
   the drills with, so show the landing state instead of a broken trainer. */
@media (max-width: 700px) and (pointer: coarse) and (hover: none) {
  .workspace { display: none; }
  .device-notice { display: block; }
  /* the hidden workspace normally draws the divider above Stats/Train */
  .stats-panel, .train-preview, .about-panel { border-top: 1px solid var(--line); }
  /* The landing is a product page, so it keeps the full wordmark rather than
     the mark-only topbar the narrow trainer falls back to -- the nav wraps
     under it instead of competing for the row. Not for a visitor who took the
     override: they are back in the trainer, where the narrow rules apply. */
  body:not(.device-keyboard-override) .topbar { height: auto; flex-wrap: wrap; gap: 14px; padding: 18px 0 16px; }
  body:not(.device-keyboard-override) .brand-word { display: inline; }
  body:not(.device-keyboard-override) .brand-mark { width: auto; overflow: visible; }
  body:not(.device-keyboard-override) .brand-caret,
  body:not(.device-keyboard-override) .brand-caret-glow { display: inline; }
}

/* ---------- app footer ----------
   The product loop in one line, the Alpha status, and the single quiet path
   to About. Caption type on --muted under one hairline; the loop line is the
   only thing here at --ink. */
.app-footer { margin-top: 56px; padding-top: 18px; border-top: 1px solid var(--line); }
.app-footer p { color: var(--muted); font-size: 13px; line-height: 1.6; }
.app-footer strong { color: var(--ink); font-weight: 600; }
/* The product loop in four words, set in the display face at --ink so it
   reads as the statement and the Alpha status under it stays the footnote. */
.app-footer-loop { font: 700 15px var(--font-display); letter-spacing: -.01em; }
.app-footer .app-footer-loop { color: var(--ink); }
.app-footer-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 20px;
  margin-top: 6px;
}
.app-footer .secondary-button { padding: 0; white-space: nowrap; }
.app-footer-links { display: flex; gap: 18px; }

/* Privacy reuses About's page styles; only the analytics choice is its own. */
.privacy-choice {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 16px;
  margin-top: 20px;
  padding: 12px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  background: var(--panel);
}
.about-block .privacy-choice p { color: var(--ink); margin: 0; }
.privacy-choice .secondary-button { padding: 0; }
.about-block code { font: 500 13px var(--font-mono); color: var(--ink); }

/* ---------- analytics consent banner ----------
   A full-width strip above the header. Answering collapses it rather than
   cutting it out, so the page eases up once instead of jumping. */
.consent-banner {
  display: grid;
  grid-template-rows: 1fr;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  transition: grid-template-rows .25s ease, visibility 0s;
}
.consent-banner.dismissed { grid-template-rows: 0fr; border-bottom-width: 0; visibility: hidden; transition: grid-template-rows .25s ease, visibility 0s .25s; }
.consent-inner {
  display: flex;
  align-items: center;
  gap: 12px 24px;
  min-height: 0;
  overflow: hidden;
  max-width: 920px;
  width: 100%;
  margin: 0 auto;
  padding: 12px 28px;
}
.consent-banner.dismissed .consent-inner { padding-block: 0; transition: padding .25s ease; }
.consent-banner p { flex: 1; color: var(--muted); font-size: 13.5px; line-height: 1.5; }
.consent-banner strong { color: var(--ink); font-weight: 600; }
.consent-banner .secondary-button { padding: 0; font: inherit; }
.consent-actions { display: flex; flex-shrink: 0; gap: 8px; }
.consent-button {
  padding: 8px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--soft);
  color: var(--ink);
  font: 600 13px var(--font-mono);
  white-space: nowrap;
  cursor: pointer;
}
.consent-button:hover { border-color: var(--accent); }
@media (max-width: 800px) {
  .consent-inner { padding-inline: 18px; }
}
@media (max-width: 640px) {
  .consent-inner { flex-direction: column; align-items: stretch; }
  .consent-button { flex: 1; padding: 12px 16px; }
}

/* Local-testing reset utility: deliberately quiet, set apart from the stats
   above it, and out of reach of the active typing UI. */
.progress-reset { margin-top: 32px; padding-top: 18px; border-top: 1px solid var(--line); }
.progress-reset .secondary-button { padding: 0; }
.progress-reset-note { color: var(--muted); font-size: 13.5px; line-height: 1.55; max-width: 560px; margin-top: 6px; }
@media (max-width: 480px) {
  .primary-nav { gap: 2px; }
  .primary-nav .nav-button { padding: 8px 9px; font-size: 12.5px; }
  /* Mark-only topbar. With the wordmark gone the caret would be pointing at
     nothing, so the lockup reduces to the keycap -- the same reduction the
     favicon makes below 32px. `slice` crops the caret's share of the viewBox
     rather than scaling the key down to fit the narrower box. */
  .brand-word { display: none; }
  .brand-mark { width: var(--mark-height); overflow: hidden; }
  .brand-caret, .brand-caret-glow { display: none; }
}
