:root {
  --bg: #fafafa;
  --surface: #ffffff;
  --fg: #1a1a1a;
  --muted: #6b6b6b;
  --accent: #2563eb;
  --correct: #16a34a;
  --wrong: #dc2626;
  --missed: #d97706;
  --pending: #9ca3af;
  --key-bg: #ffffff;
  --key-hover: #f1f5f9;
  --key-active: #e2e8f0;
  --border: #e5e7eb;
  --shadow: 0 1px 2px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.08);
}

[data-theme="dark"] {
  --bg: #0f1115;
  --surface: #181b22;
  --fg: #e7eaef;
  --muted: #8a8f99;
  --accent: #60a5fa;
  --correct: #4ade80;
  --wrong: #f87171;
  --missed: #fbbf24;
  --pending: #6b7280;
  --key-bg: #1f232c;
  --key-hover: #262b36;
  --key-active: #2f3542;
  --border: #2a2f3a;
  --shadow: 0 1px 2px rgba(0,0,0,0.4), 0 1px 3px rgba(0,0,0,0.3);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
}

html { height: 100%; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  background: var(--bg);
  color: var(--fg);
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transition: background 0.2s, color 0.2s;
  -webkit-tap-highlight-color: transparent;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.9rem 1.2rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}

header h1 {
  margin: 0;
  font-size: 1.35rem;
  font-weight: 600;
  letter-spacing: 0.5px;
  line-height: 1.1;
}

.byline {
  font-size: 0.65rem;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--muted);
  opacity: 0.55;
  margin-left: 0.3rem;
}

.header-titles {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.subtitle {
  margin: 0;
  font-size: 0.78rem;
  color: var(--muted);
  font-weight: 400;
}

.header-actions {
  display: flex;
  gap: 0.4rem;
}

.header-actions button {
  background: var(--key-bg);
  border: 1px solid var(--border);
  color: var(--fg);
  cursor: pointer;
  font-size: 1.1rem;
  padding: 0.4rem 0.7rem;
  border-radius: 8px;
  line-height: 1;
  min-width: 2.4rem;
}

.header-actions button:hover { background: var(--key-hover); }

main {
  max-width: 540px;
  width: 100%;
  margin: 0 auto;
  padding: 1rem;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.comp-timer {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-size: 2.6rem;
  font-weight: 600;
  text-align: center;
  color: var(--fg);
  letter-spacing: 0.08em;
  line-height: 1.1;
  margin: 0.2rem 0 0.6rem;
  cursor: pointer;
  user-select: none;
  transition: opacity 0.15s;
}

.comp-timer.dimmed { opacity: 0.18; }
.comp-timer.dimmed:hover { opacity: 0.5; }

.comp-timer.warning { color: var(--missed); }
.comp-timer.danger {
  color: var(--wrong);
  animation: pulseTimer 1s ease-in-out infinite;
}
.comp-timer.ended { color: var(--wrong); }

@keyframes pulseTimer {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

.mode-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8rem;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.3rem 0.75rem;
  margin-bottom: 0.75rem;
  font-variant-numeric: tabular-nums;
  font-family: inherit;
  cursor: pointer;
}

.mode-badge:hover { background: var(--key-hover); }

.mode-badge.locked::before {
  content: "\1F512";
  font-size: 0.8em;
}

.setting {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.setting[hidden] { display: none; }

.setting + .setting {
  margin-top: 1.1rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--border);
}

.setting-label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.9rem;
  color: var(--muted);
}

.setting-label strong {
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}

.setting-reset {
  margin-left: auto;
  background: none;
  border: 1px solid var(--border);
  color: var(--muted);
  font-family: inherit;
  font-size: 0.75rem;
  line-height: 1;
  padding: 0.15rem 0.45rem;
  border-radius: 6px;
  cursor: pointer;
}

.setting-reset:hover {
  color: var(--fg);
  background: var(--key-hover);
  border-color: var(--muted);
}

.settings-subhead {
  margin: 1.4rem 0 0.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
}

.mode-selector {
  display: flex;
  gap: 0.4rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.25rem;
}

.mode-option {
  flex: 1;
  position: relative;
  cursor: pointer;
}

.mode-option input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.mode-option span {
  display: block;
  text-align: center;
  padding: 0.45rem 0.6rem;
  border-radius: 999px;
  font-size: 0.9rem;
  color: var(--muted);
  transition: background 0.06s, color 0.06s;
  user-select: none;
}

.mode-option input:checked + span {
  background: var(--accent);
  color: white;
  font-weight: 500;
}

.mode-option input:disabled + span {
  opacity: 0.5;
  cursor: not-allowed;
}

input[type="range"] {
  width: 100%;
  accent-color: var(--accent);
}

.setting-select {
  background: var(--key-bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.45rem 0.7rem;
  font-size: 0.95rem;
  font-family: inherit;
  cursor: pointer;
  width: 100%;
  max-width: 220px;
}

.setting-action {
  background: var(--accent);
  color: white;
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 0.45rem 1.1rem;
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 500;
  cursor: pointer;
}

.setting-action:hover { filter: brightness(1.08); }

input[type="range"]:disabled {
  opacity: 0.5;
}

.hint {
  color: var(--muted);
  font-size: 0.8rem;
  min-height: 1em;
}

/* Modal */
.modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

.modal[hidden] { display: none; }

/* Bullet score modal anchors to the bottom so the pi-display digits
   above stay visible behind the dialog. */
.modal.modal-bottom {
  align-items: flex-end;
  padding-bottom: 1.5rem;
}
.modal.modal-bottom .modal-backdrop {
  background: rgba(0, 0, 0, 0.35);
}

.modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(2px);
  animation: fadeIn 0.15s ease;
}

.modal-card {
  position: relative;
  width: 100%;
  max-width: 420px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.25);
  animation: popIn 0.15s ease;
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes popIn {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to { opacity: 1; transform: none; }
}

/* Newly revealed digits fade in so the user can see them appear. The
   duration is set inline per source: ~0.8s for the Skip dialog (slow enough
   to watch the digits arrive) and ~0.14s for pasted digits and single-key
   "v" skips. The negative --fade-delay lets the animation resume mid-flight
   when a re-render recreates the element, so it never restarts or flashes. */
.digit.digit-fade {
  animation: fadeIn var(--fade-duration, 0.14s) ease both;
  animation-delay: var(--fade-delay, 0s);
}

/* Zen motion: the quick paste / single-"v" fade is just ambient motion, so
   drop it — those digits appear instantly. The slower Skip-dialog fade stays;
   it's a deliberate "watch them arrive" cue rather than incidental motion. */
:root[data-motion="zen"] .digit.digit-fade.fade-fast {
  animation: none;
}

.modal-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.9rem 1.1rem;
  border-bottom: 1px solid var(--border);
}

.modal-head h2 {
  margin: 0;
  font-size: 1.1rem;
  font-weight: 600;
}

#settings-close {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 1.2rem;
  cursor: pointer;
  padding: 0.3rem 0.5rem;
  border-radius: 6px;
  line-height: 1;
}

#settings-close:hover {
  background: var(--key-hover);
  color: var(--fg);
}

.modal-body {
  padding: 1rem 1.1rem 1.2rem;
}

.display-area {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  margin-bottom: 1rem;
}

.pi-display {
  flex: 1;
  min-height: 4rem;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 1.4rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem 1.1rem;
  line-height: 1.7;
  letter-spacing: 0.04em;
  box-shadow: var(--shadow);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  position: relative;
}

/* Bottom-anchored content via margin-top: auto. When the row is shorter
   than the display, it sits at the bottom (cursor near keypad). As it
   grows, the top edge moves up until the row fills the display, after
   which the row overflows and pi-display scrolls. */
.pi-display .pi-row {
  margin-top: auto;
  display: flex;
  align-items: flex-start;
}

.pi-display .prefix {
  flex: 0 0 auto;
}

.pi-display .pi-body {
  flex: 1;
  min-width: 0;
  word-break: break-all;
  overflow-wrap: break-word;
}

.pi-display.grouped .pi-body {
  word-break: normal;
}

.pi-display.grouped .group {
  display: inline-block;
  margin-right: 0.55ch;
}

/* Transparent spacer used inside the active (partial) group so it reserves
   the same width as a full group — partial trailing groups then wrap to a
   new line at the same column as a full group would, rather than dangling
   a stub on the previous line. inline-block so letter-spacing across an
   NBSP pad matches the letter-spacing across a real digit inline-block
   (Firefox treats the two boundaries slightly differently). */
.pi-display.grouped .group-pad {
  display: inline-block;
  color: transparent;
  user-select: none;
  pointer-events: none;
}

/* The cursor normally sits between the prefix/digits and the next typing
   slot, contributing ~1ch + margin of layout width. While it's inside a
   partial group, that extra width pushes the group past a full group's
   width — at certain zooms the partial group then wraps to a new line,
   only to pop back once enough digits are typed and the cursor moves
   out. Pull the cursor out of layout flow while it's inside a group; its
   glyph still renders via overflow: visible. */
.pi-display.grouped .group > .cursor {
  width: 0;
  margin: 0;
  overflow: visible;
}

.prefix {
  color: var(--fg);
  opacity: 0.85;
}

.digit { display: inline-block; }
.digit.correct { color: var(--correct); }
.digit.correct.skipped { color: var(--fg); opacity: 0.85; }
/* Practice-mode cue: digit was typed wrong, erased, and re-typed correctly. */
.digit.correct.corrected {
  text-decoration: underline dotted var(--missed);
  text-underline-offset: 3px;
  text-decoration-thickness: 2px;
}
.digit.wrong {
  color: var(--wrong);
  text-decoration: underline;
  text-decoration-color: var(--wrong);
  text-underline-offset: 3px;
  text-decoration-thickness: 2px;
}

.digit.wrong.masked {
  text-decoration: none;
  opacity: 0.85;
}

/* Diff view shown after a sprint session ends: typed digit struck
   through with the correct pi digit floating above it. */
.pi-display.diff-mode {
  line-height: 2.2;
}

.digit.wrong.diff {
  position: relative;
  display: inline-block;
  text-decoration: none;
}

.digit.wrong.diff .correction {
  position: absolute;
  bottom: 75%;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.55em;
  color: var(--correct);
  font-weight: 500;
  line-height: 1;
  padding-bottom: 0.15em;
  white-space: nowrap;
}

.digit.wrong.diff .typed {
  color: var(--wrong);
  text-decoration: line-through;
  text-decoration-color: var(--wrong);
  text-decoration-thickness: 2px;
}
.digit.pending { color: var(--pending); }

/* Per-digit auto-check progress cue. Two synchronised pieces:
   1. A thin accent strip below the digit that grows L→R as the
      deadline approaches.
   2. The digit's text colour eases from --pending toward --fg.
   Negative --fill-delay lets a re-rendered digit pick its animations
   up at the right point instead of restarting from zero. */
.digit.auto-filling {
  position: relative;
  animation: digitAutoColor linear forwards;
  animation-duration: var(--fill-duration, 0s);
  animation-delay: var(--fill-delay, 0s);
}

.digit.auto-filling::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0.18em;
  height: 2px;
  width: 0;
  background: var(--accent);
  opacity: 0.7;
  border-radius: 1px;
  pointer-events: none;
  animation: digitAutoStrip linear forwards;
  animation-duration: var(--fill-duration, 0s);
  animation-delay: var(--fill-delay, 0s);
}

@keyframes digitAutoStrip {
  to { width: 100%; }
}

@keyframes digitAutoColor {
  from { color: var(--pending); }
  to { color: var(--fg); }
}
.digit.missed-marker {
  color: var(--missed);
  opacity: 0.65;
  font-size: 0.85em;
  vertical-align: 0.1em;
  margin: 0 0.05em;
}

/* Visual separator injected between primes when the sequence has natural
   spacing (only relevant for the primes sequence, where the user types just
   digits and we group them visually by prime). */
.prime-space {
  display: inline-block;
  width: 0.45em;
}

/* When a user-typed entry is a space character (primes-spaced), give the
   span an explicit minimum width so the gap reads clearly — a literal " "
   inside an inline-block can otherwise render too thin to notice. */
.digit.space-char {
  min-width: 0.45em;
}

.cursor {
  display: inline-block;
  color: var(--accent);
  font-weight: bold;
  animation: blink 1.1s infinite;
  margin-left: 0.05em;
}

/* Hidden on pause / game over — the display reads as static, not "still
   typing". Applied via the .hidden class from updateUI. */
.cursor.hidden { visibility: hidden; }

/* Low-motion: solid (no blink) so it doesn't pulse. */
:root[data-motion="low"] .cursor,
:root[data-motion="zen"] .cursor {
  animation: none;
}

/* ---- Zen mode -------------------------------------------------------
   Greyscale colour tokens override the accent / status colours so every
   button, digit, and chrome element collapses to fg / muted shades.
   Wrong vs correct is still distinguishable via the wrong-underline.

   Scoped to <main> only — modals (settings / skip / missed) are body-
   level siblings, so they keep the original tokens and stay readable
   when the user opens settings from zen to switch out.
*/
:root[data-motion="zen"] main {
  --accent: var(--fg);
  --correct: var(--fg);
  --wrong: var(--fg);
  --missed: var(--muted);
  --pending: var(--muted);
}

/* Hide the surrounding chrome. The pi-display, cursor, and keypad
   (unless the user separately hid it) remain. */
:root[data-motion="zen"] header,
:root[data-motion="zen"] .mode-badge,
:root[data-motion="zen"] .comp-timer,
:root[data-motion="zen"] .actions,
:root[data-motion="zen"] .keypad-hint {
  display: none;
}

/* Pi display loses its panel chrome — the digits float on the page. */
:root[data-motion="zen"] .pi-display {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}

/* Quiet the key chrome too. */
:root[data-motion="zen"] .key,
:root[data-motion="zen"] .key-check {
  box-shadow: none;
  background: var(--key-bg);
  border-color: var(--border);
  color: var(--fg);
  opacity: 0.7;
}
:root[data-motion="zen"] .key:hover:not(:disabled),
:root[data-motion="zen"] .key-check:hover:not(:disabled) {
  opacity: 0.9;
  background: var(--key-hover);
  filter: none;
}
:root[data-motion="zen"] .key:disabled,
:root[data-motion="zen"] .key-check:disabled {
  opacity: 0.3;
}
:root[data-motion="zen"] .key-back { color: var(--fg); }

/* Primary Reset uses --accent for both background and "color: white";
   zen remaps --accent to --fg, which leaves white text on a light
   background in dark mode (unreadable). Flip text to --bg so the
   button stays a high-contrast inverted chip in either theme. */
:root[data-motion="zen"] #reset-btn.primary { color: var(--bg); }

/* Counters hidden by default; revealed only when paused or game-over
   (the JS sets .zen-reveal on <html>). */
:root[data-motion="zen"] .stats { display: none; }
:root[data-motion="zen"].zen-reveal .stats { display: grid; }

/* Actions row (Reset / Continue / stat-time) also revealed on pause or
   game-over, so the player can restart without leaving zen. */
:root[data-motion="zen"].zen-reveal .actions { display: grid; }

/* Corner ×, only shown in zen so the user can always escape without
   reaching for the keyboard. */
#zen-exit {
  display: none;
  position: fixed;
  top: 0.6rem;
  right: 0.7rem;
  z-index: 200;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 0.95rem;
  font-family: inherit;
  line-height: 1;
  cursor: pointer;
  opacity: 0.55;
  transition: opacity 0.15s;
}
:root[data-motion="zen"] #zen-exit { display: block; }
#zen-exit:hover { opacity: 1; color: var(--fg); }

@keyframes blink {
  0%, 50% { opacity: 1; }
  51%, 100% { opacity: 0; }
}

.stats {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.stats > div {
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 0.55rem 0.4rem;
  border-radius: 10px;
  text-align: center;
  box-shadow: var(--shadow);
}

.stat-label {
  font-size: 0.72rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.stat-value {
  font-size: 1.25rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  margin-top: 0.1rem;
}

#stat-correct { color: var(--correct); }
#stat-wrong { color: var(--wrong); }
#stat-fixed { color: var(--muted); }
#stat-fixed.corrected {
  text-decoration: underline dotted var(--missed);
  text-underline-offset: 3px;
  text-decoration-thickness: 2px;
}
#stat-missed { color: var(--missed); }
#stat-skipped { color: var(--muted); }

#stat-skipped-tile,
#stat-missed-tile,
#stat-correct-tile {
  cursor: pointer;
  transition: background 0.1s, border-color 0.1s;
}
#stat-skipped-tile:hover,
#stat-missed-tile:hover,
#stat-correct-tile:hover { background: var(--key-hover); border-color: var(--muted); }
#stat-skipped-tile:focus-visible,
#stat-missed-tile:focus-visible,
#stat-correct-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.skip-hint {
  margin: 0 0 0.9rem;
  font-size: 0.88rem;
  color: var(--muted);
}

.skip-presets {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.4rem;
  margin-bottom: 0.9rem;
}

/* Bullet timing settings: three short numeric fields side-by-side. */
.bullet-fields {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.45rem;
}
.bullet-field {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.78rem;
  color: var(--muted);
  background: var(--key-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.35rem 0.5rem;
}
.bullet-field > span:first-child {
  white-space: nowrap;
}
.bullet-field input[type="number"] {
  width: 100%;
  min-width: 0;
  background: transparent;
  color: var(--fg);
  border: none;
  padding: 0;
  font-size: 0.95rem;
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  -moz-appearance: textfield;
}
.bullet-field input[type="number"]::-webkit-outer-spin-button,
.bullet-field input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.bullet-field input[type="number"]:focus { outline: none; }
.bullet-suffix { opacity: 0.7; }

/* Run in progress — fields read-only. Show the lock icon on the section
   so the player knows why the controls don't respond. */
#bullet-settings.locked .bullet-field {
  opacity: 0.55;
}
#bullet-settings.locked .bullet-field input[type="number"] {
  cursor: not-allowed;
}
#bullet-settings.locked .setting-label::after {
  content: " \1F512";
  font-size: 0.85em;
  opacity: 0.7;
}

/* Bullet score modal: tighter card, grid of stats with right-aligned
   numbers in a tabular monospaced figure. */
.bullet-score-card {
  max-width: 380px;
}
.bullet-score-headline {
  margin: 0 0 0.9rem;
  font-size: 0.95rem;
  color: var(--fg);
}
.bullet-score-grid {
  display: grid;
  grid-template-columns: 1fr auto;
  row-gap: 0.4rem;
  column-gap: 1.2rem;
}
.bullet-score-row {
  display: contents;
}
.bullet-score-row > span {
  color: var(--muted);
  font-size: 0.88rem;
}
.bullet-score-row > strong {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: 0.95rem;
}

/* Bullet uses the comp-timer styling but coloured-as-accent at calm
   levels so it visually distinguishes from the sprint countdown. */
.comp-timer.bullet { color: var(--accent); }
.comp-timer.bullet.warning { color: var(--missed); }
.comp-timer.bullet.danger { color: var(--wrong); }
.comp-timer.bullet.ended { color: var(--wrong); }

/* Floating "+5" / "−30" delta near the active bullet timer.
   .large anchors below the big-top timer (high motion).
   .small anchors above the inline stat-time slot (medium motion). */
.bullet-float {
  position: fixed;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  pointer-events: none;
  white-space: nowrap;
  z-index: 100;
}
.bullet-float.large {
  font-size: 1.4rem;
  animation: bulletFloatLarge 1.1s ease-out forwards;
}
.bullet-float.small {
  font-size: 0.9rem;
  animation: bulletFloatSmall 1.1s ease-out forwards;
}
.bullet-float.positive { color: var(--correct); }
.bullet-float.negative { color: var(--wrong); }

@keyframes bulletFloatLarge {
  0%   { transform: translate(-50%, 0);       opacity: 0; }
  15%  { transform: translate(-50%, -0.4rem); opacity: 1; }
  100% { transform: translate(-50%, -3rem);   opacity: 0; }
}
@keyframes bulletFloatSmall {
  0%   { transform: translate(-50%, 0);       opacity: 0; }
  15%  { transform: translate(-50%, -0.25rem); opacity: 1; }
  100% { transform: translate(-50%, -1.8rem); opacity: 0; }
}

.skip-preset {
  background: var(--key-bg);
  border: 1px solid var(--border);
  color: var(--fg);
  border-radius: 8px;
  padding: 0.55rem 0.4rem;
  font-size: 1rem;
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background 0.08s;
}
.skip-preset:hover { background: var(--key-hover); border-color: var(--muted); }
.skip-preset:active { background: var(--key-active); }

.skip-inline {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  margin-bottom: 0.7rem;
}

.skip-inline-label {
  font-size: 0.9rem;
  color: var(--fg);
}

.skip-inline input[type="number"] {
  flex: 1;
  background: var(--key-bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.45rem 0.7rem;
  font-size: 0.95rem;
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  max-width: 8rem;
}

/* Standalone confirm button sits on its own row, full-width, with the
   live "Skip N" label. */
.skip-confirm-btn {
  display: block;
  width: 100%;
  font-variant-numeric: tabular-nums;
}

.keypad-hint {
  font-size: 0.85rem;
  font-style: italic;
  color: var(--muted);
  text-align: left;
  margin-bottom: 0.4rem;
  font-variant-numeric: tabular-nums;
}

.keypad {
  display: grid;
  gap: 0.55rem;
  margin-bottom: 1rem;
}

.keypad[hidden] { display: none; }

/* User-toggled hide (separate from the alphabet/sequence-driven [hidden]
   attribute). The keypad-hint above also collapses — without it the hint
   would float alone above the actions row. */
.keypad.user-hidden { display: none; }
:root[data-keypad="hidden"] .keypad-hint { display: none; }

/* When the on-screen keypad is hidden, signal that tapping the display
   triggers the OS keyboard (script.js focuses #mobile-input on click). */
:root[data-keypad="hidden"] .pi-display { cursor: text; }

/* Off-screen mobile-input. Visible to assistive tech / JS focus but not
   to the user — opacity 0 + 1×1px so it doesn't take layout space. Kept
   visible (not display:none) so .focus() actually surfaces the keyboard
   on iOS / Android. Positioned at the bottom of .pi-display (which is
   position:relative for this) so the browser's keep-caret-in-view scroll
   lands on the typing area rather than yanking the page to top:0 of the
   document on every keystroke. */
#mobile-input {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 1px;
  opacity: 0;
  border: 0;
  padding: 0;
  margin: 0;
  font-size: 16px; /* >=16px avoids iOS Safari's zoom-on-focus. */
  pointer-events: none;
}

.keypad-decimal {
  grid-template-columns: repeat(3, 1fr);
  grid-template-areas:
    "k1 k2 k3"
    "k4 k5 k6"
    "k7 k8 k9"
    "kc k0 kb";
}

.keypad-decimal.flipped {
  grid-template-areas:
    "k7 k8 k9"
    "k4 k5 k6"
    "k1 k2 k3"
    "kc k0 kb";
}

/* When the current sequence treats spaces as input (e.g. primes-spaced),
   show an extra full-width spacebar row below the standard 3x4 grid. */
.keypad-decimal.with-space {
  grid-template-areas:
    "k1 k2 k3"
    "k4 k5 k6"
    "k7 k8 k9"
    "kc k0 kb"
    "ksp ksp ksp";
}
.keypad-decimal.with-space.flipped {
  grid-template-areas:
    "k7 k8 k9"
    "k4 k5 k6"
    "k1 k2 k3"
    "kc k0 kb"
    "ksp ksp ksp";
}

.keypad [data-digit=" "] { grid-area: ksp; }
.keypad-decimal:not(.with-space) [data-digit=" "] { display: none; }

/* Jameco JE600-style hex keypad: A-F across the top, decimal digits in
   a 3-column phone layout below them with the trailing hex digits filling
   the right column. The bottom two rows mirror the decimal keypad
   (7 8 9 / check 0 back) with an empty 4th column. */
.keypad-hex {
  grid-template-columns: repeat(4, 1fr);
  grid-template-areas:
    "kA kB kC kD"
    "k1 k2 k3 kE"
    "k4 k5 k6 kF"
    "k7 k8 k9 . "
    "kc k0 kb . ";
}

.keypad-hex.flipped {
  grid-template-areas:
    "kA kB kC kD"
    "k7 k8 k9 kE"
    "k4 k5 k6 kF"
    "k1 k2 k3 . "
    "kc k0 kb . ";
}

.keypad [data-digit="0"] { grid-area: k0; }
.keypad [data-digit="1"] { grid-area: k1; }
.keypad [data-digit="2"] { grid-area: k2; }
.keypad [data-digit="3"] { grid-area: k3; }
.keypad [data-digit="4"] { grid-area: k4; }
.keypad [data-digit="5"] { grid-area: k5; }
.keypad [data-digit="6"] { grid-area: k6; }
.keypad [data-digit="7"] { grid-area: k7; }
.keypad [data-digit="8"] { grid-area: k8; }
.keypad [data-digit="9"] { grid-area: k9; }
.keypad [data-digit="A"] { grid-area: kA; }
.keypad [data-digit="B"] { grid-area: kB; }
.keypad [data-digit="C"] { grid-area: kC; }
.keypad [data-digit="D"] { grid-area: kD; }
.keypad [data-digit="E"] { grid-area: kE; }
.keypad [data-digit="F"] { grid-area: kF; }
.keypad [data-action="check"] { grid-area: kc; }
.keypad [data-action="back"] { grid-area: kb; }

.key {
  background: var(--key-bg);
  border: 1px solid var(--border);
  color: var(--fg);
  font-size: 1.6rem;
  font-weight: 500;
  padding: 1rem 0;
  border-radius: 12px;
  cursor: pointer;
  transition: background 0.08s, transform 0.05s;
  user-select: none;
  touch-action: manipulation;
  box-shadow: var(--shadow);
  min-height: 64px;
}

.key:hover:not(:disabled) { background: var(--key-hover); }
.key:active:not(:disabled) {
  background: var(--key-active);
  transform: scale(0.97);
}

.keypad-hex .key {
  font-size: 1.4rem;
  padding: 0.75rem 0;
  min-height: 56px;
}

.key:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.key-check {
  background: var(--correct);
  border-color: var(--correct);
  color: white;
  position: relative;
  overflow: hidden;
}
.key-check:hover:not(:disabled) { filter: brightness(1.08); background: var(--correct); }
.key-check:disabled { background: var(--correct); }

/* Medium / low motion: the check button toggles enabled/disabled
   constantly during typing; the full-green colour flashes distractingly.
   Drop the enabled-state colour to only a couple of shades brighter than
   the disabled (which already shows green at 35%). */
:root[data-motion="medium"] .key-check,
:root[data-motion="low"] .key-check {
  opacity: 0.55;
}
:root[data-motion="medium"] .key-check:disabled,
:root[data-motion="low"] .key-check:disabled {
  opacity: 0.35;
}
:root[data-motion="medium"] .key-check:hover:not(:disabled),
:root[data-motion="low"] .key-check:hover:not(:disabled) {
  opacity: 0.75;
  filter: none;
}

.key-check::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 4px;
  width: 0;
  background: rgba(255, 255, 255, 0.55);
  pointer-events: none;
}

.key-check.filling::after {
  animation: keyCheckFill linear forwards;
  animation-duration: var(--fill-duration, 0s);
  animation-delay: var(--fill-delay, 0s);
}

@keyframes keyCheckFill {
  from { width: 0; }
  to { width: 100%; }
}

/* Low / zen motion: no animated fill on the check button either. */
:root[data-motion="low"] .key-check.filling::after,
:root[data-motion="zen"] .key-check.filling::after {
  animation: none;
}

/* Medium / low / zen motion: the per-digit underline strip is
   distracting. Disable both the strip and the digit colour fade —
   pending digits stay in --pending until they're checked. */
:root[data-motion="medium"] .digit.auto-filling,
:root[data-motion="low"] .digit.auto-filling,
:root[data-motion="zen"] .digit.auto-filling {
  animation: none;
  color: var(--pending);
}
:root[data-motion="medium"] .digit.auto-filling::after,
:root[data-motion="low"] .digit.auto-filling::after,
:root[data-motion="zen"] .digit.auto-filling::after {
  display: none;
}

.key-back {
  background: var(--key-bg);
  color: var(--wrong);
}
.key-back:hover:not(:disabled) { background: var(--key-hover); }

.actions {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 1rem;
}

.actions-conditional {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
}

#stop-btn {
  background: var(--surface);
  border: 1px solid var(--wrong);
  color: var(--wrong);
  padding: 0.55rem 1.2rem;
  border-radius: 10px;
  cursor: pointer;
  font-size: 0.95rem;
  font-family: inherit;
}

#stop-btn:hover { background: var(--key-hover); }

/* In practice mode the Stop button is just a courtesy pause — keep it
   present but quiet. The Stop variant in comp/hardcore retains the
   alert-red border above. */
#stop-btn.practice-pause {
  border-color: var(--border);
  color: var(--muted);
  font-size: 0.85rem;
  padding: 0.4rem 0.9rem;
}
#stop-btn.practice-pause:hover {
  background: var(--key-hover);
  color: var(--fg);
}

#continue-btn {
  background: var(--accent);
  border: 1px solid var(--accent);
  color: white;
  padding: 0.55rem 1.2rem;
  border-radius: 10px;
  cursor: pointer;
  font-size: 0.95rem;
  font-family: inherit;
  font-weight: 500;
}

#continue-btn:hover { filter: brightness(1.08); }

.stat-time {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  font-size: 0.95rem;
  padding-right: 0.2rem;
  cursor: pointer;
  user-select: none;
  transition: opacity 0.15s;
}

.stat-time.frozen {
  color: var(--wrong);
  font-weight: 500;
}

/* Italic alone is enough to signal pause — no need for the pause-glyph
   prefix, which read as an action button (it's just a status). */
.stat-time.paused {
  color: var(--accent);
  font-style: italic;
}

/* Click-to-dim affordance. Hovering an already-dim clock raises it a bit
   so the user can read the value without un-dimming. */
.stat-time.dimmed { opacity: 0.18; }
.stat-time.dimmed:hover { opacity: 0.5; }

/* Sprint countdown folded into the stat-time slot (medium/low). Use
   the same monospace face as the big comp-timer but at the stat-time
   font size, prefixed with the outline hourglass. */
.stat-time.countdown {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  letter-spacing: 0.04em;
}
.stat-time.countdown::before {
  content: "\231B\FE0E\00A0";
  opacity: 0.7;
}

#reset-btn {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--fg);
  padding: 0.6rem 1.6rem;
  border-radius: 10px;
  cursor: pointer;
  font-size: 0.95rem;
  box-shadow: var(--shadow);
}

#reset-btn:hover { background: var(--key-hover); }

#reset-btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: 500;
}
#reset-btn.primary:hover { filter: brightness(1.08); background: var(--accent); }

#continue-btn.secondary {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--fg);
  font-weight: normal;
}
#continue-btn.secondary:hover { background: var(--key-hover); filter: none; }


@media (max-width: 480px) {
  main { padding: 0.7rem; }
  .stats { grid-template-columns: repeat(5, 1fr); gap: 0.3rem; }
  .stat-value { font-size: 1.05rem; }
  .stat-label { font-size: 0.65rem; }
  .pi-display { font-size: 1.2rem; padding: 0.8rem; }
  .key { font-size: 1.5rem; min-height: 60px; }
  header h1 { font-size: 1.2rem; }
  .mode-option span { font-size: 0.82rem; padding: 0.4rem 0.4rem; }
}

@media (max-width: 360px) {
  .pi-display { font-size: 1.05rem; }
}

/* Tight vertical viewports (zoomed-in desktop, short windows): trim the
   header and section gaps so the pi display keeps at least two or three
   lines before squishing. Covers the range above the landscape
   breakpoint where the standard layout otherwise starves the display. */
@media (max-height: 820px) {
  header { padding: 0.55rem 1.2rem; }
  .subtitle { display: none; }
  .header-actions button { padding: 0.35rem 0.6rem; font-size: 1rem; min-width: 2.1rem; }
  .mode-badge { margin-bottom: 0.4rem; }
  .display-area { margin-bottom: 0.55rem; }
  .stats { margin-bottom: 0.55rem; }
  .keypad { margin-bottom: 0.55rem; }
  .actions { margin-bottom: 0.45rem; }
}

@media (max-height: 720px) {
  header { padding: 0.4rem 1.2rem; }
  header h1 { font-size: 1.2rem; }
  .stats > div { padding: 0.4rem 0.35rem; }
  .stat-value { font-size: 1.1rem; }
  .keypad-hint { margin-bottom: 0.25rem; font-size: 0.78rem; }
  .keypad { gap: 0.4rem; margin-bottom: 0.45rem; }
  .key { min-height: 52px; padding: 0.6rem 0; font-size: 1.4rem; }
}

/* Landscape on short viewports (mobile rotated, small windows): keypad
   moves to the right column, everything else stacks in the left column;
   actions span the full width below. The header subtitle is hidden to
   reclaim vertical room. */
@media (orientation: landscape) and (max-height: 700px) {
  header { padding: 0.4rem 0.9rem; }
  header h1 { font-size: 1.1rem; letter-spacing: 0.3px; }
  .subtitle { display: none; }
  .header-actions button { padding: 0.3rem 0.55rem; font-size: 1rem; min-width: 2rem; }

  main {
    padding: 0.4rem 0.7rem;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto auto minmax(0, 1fr) auto auto;
    grid-template-areas:
      "badge   hint"
      "timer   keypad"
      "display keypad"
      "stats   keypad"
      "actions actions";
    column-gap: 0.7rem;
    row-gap: 0.35rem;
  }

  .keypad-hint { grid-area: hint; margin: 0; font-size: 0.72rem; }

  #mode-badge {
    grid-area: badge;
    justify-self: start;
    margin: 0;
    font-size: 0.72rem;
    padding: 0.2rem 0.6rem;
  }
  #comp-timer { grid-area: timer; }
  .display-area { grid-area: display; margin-bottom: 0; min-height: 0; }
  .stats { grid-area: stats; gap: 0.25rem; }
  .stats > div { padding: 0.3rem 0.25rem; border-radius: 8px; }
  .actions { grid-area: actions; margin-bottom: 0; gap: 0.4rem; }

  .keypad-decimal, .keypad-hex {
    grid-area: keypad;
    align-self: stretch;
    margin-bottom: 0;
    gap: 0.35rem;
  }
  .keypad-decimal { width: 260px; }
  .keypad-hex { width: 320px; }

  .pi-display { font-size: 1.2rem; padding: 0.6rem 0.8rem; }

  .comp-timer {
    font-size: 1.6rem;
    margin: 0;
    letter-spacing: 0.04em;
  }

  .stat-value { font-size: 0.9rem; }
  .stat-label { font-size: 0.58rem; letter-spacing: 0.03em; }

  .keypad-decimal .key { padding: 0.45rem 0; min-height: 44px; font-size: 1.3rem; }
  .keypad-hex .key { padding: 0.3rem 0; min-height: 38px; font-size: 1.15rem; }

  #reset-btn { padding: 0.35rem 1rem; font-size: 0.85rem; }
  #stop-btn, #continue-btn { padding: 0.35rem 0.8rem; font-size: 0.85rem; }
  .stat-time { font-size: 0.85rem; }
}
