/* --------------------------------------------------------------------------
   Components.
   -------------------------------------------------------------------------- */

/* --- buttons -------------------------------------------------------------- */

.icon-btn {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  color: var(--text-muted);
  transition: color var(--dur) var(--ease),
              background-color var(--dur) var(--ease);
}

.icon-btn:hover {
  color: var(--text);
  background-color: var(--bg-elev);
}

.icon {
  width: 18px;
  height: 18px;
}

/* One icon per theme; the other is hidden. */
.icon--moon { display: none; }
:root[data-theme="dark"] .icon--sun { display: none; }
:root[data-theme="dark"] .icon--moon { display: block; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .icon--sun { display: none; }
  :root:not([data-theme="light"]) .icon--moon { display: block; }
  :root[data-theme="light"] .icon--sun { display: block; }
  :root[data-theme="light"] .icon--moon { display: none; }
}

.segmented {
  display: flex;
  padding: 3px;
  gap: 2px;
  background-color: var(--bg-elev);
}

.segmented__btn {
  padding: var(--sp-1) var(--sp-3);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  transition: color var(--dur) var(--ease),
              background-color var(--dur) var(--ease);
}

.segmented__btn:hover {
  color: var(--text);
}

.segmented__btn[aria-pressed="true"] {
  color: var(--bg);
  background-color: var(--accent);
}

.pill {
  padding: var(--sp-1) var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  transition: color var(--dur) var(--ease);
}

.pill:hover {
  color: var(--text);
}

.pill[aria-pressed="true"] {
  color: var(--accent);
  font-weight: 500;
}

.ghost-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  transition: color var(--dur) var(--ease),
              background-color var(--dur) var(--ease);
}

.ghost-btn:hover {
  color: var(--text);
  background-color: var(--bg-elev);
}

.ghost-btn--small {
  padding: var(--sp-1) var(--sp-3);
  font-size: var(--fs-xs);
}

.solid-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-5);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--bg);
  background-color: var(--accent);
  transition: opacity var(--dur) var(--ease), transform var(--dur-fast) var(--ease);
}

.solid-btn:hover {
  opacity: 0.86;
}

.solid-btn:active {
  transform: translateY(1px);
}

.solid-btn kbd {
  color: var(--bg);
  background-color: transparent;
  border-color: color-mix(in srgb, var(--bg) 45%, transparent);
}

/* --- mode bar ------------------------------------------------------------- */

/* The mode buttons must never move when the mode changes. That rules out
   putting the length options beside them: quote and zen have none, so the row
   would change width and shift everything. Stacking the two rows keeps the
   mode row's width fixed by its own four labels, and the slot reserves the
   height of both rows so the test below stays put as well. */
.modebar-slot {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  /* Derived from the bar's own tokens rather than eyeballed, so it keeps
     matching if the type scale changes: padding + mode row + gap + option row. */
  min-height: calc(
    var(--sp-2) * 2 +
    (var(--fs-sm) * 1.5 + var(--sp-1) * 2) +
    var(--sp-1) +
    (var(--fs-xs) * 1.5 + var(--sp-1) * 2)
  );
}

.modebar {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-4);
  background-color: var(--bg-elev);
}

.modebar__row {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
}

.modebar__row:empty {
  display: none;
}

.modebar__row--options .pill {
  font-size: var(--fs-xs);
}

/* --- heads-up display ----------------------------------------------------- */

.hud {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  min-height: 2.5rem;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.hud__value {
  font-size: var(--fs-stat);
  font-weight: 500;
  line-height: 1;
}

.hud__label {
  font-size: var(--fs-sm);
  color: var(--text-muted);
}

/* --- typing area ---------------------------------------------------------- */

.typing-area {
  position: relative;
  width: 100%;
  /* Exactly three lines: three line boxes plus the 2px underline each word
     reserves for the "you left mistakes here" marker. */
  height: calc((var(--fs-type) * var(--lh-type) + 2px) * 3);
  overflow: hidden;
  outline: none;
}

.words {
  position: relative;
  transition: transform var(--dur-slow) var(--ease);
}

.word-list {
  display: flex;
  flex-wrap: wrap;
  font-size: var(--fs-type);
  line-height: var(--lh-type);
  user-select: none;
}

.word {
  /* One monospace character of gap stands in for the space between words. */
  margin-right: 1ch;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
}

/* A word left behind with mistakes stays flagged. */
.word.error {
  border-bottom-color: color-mix(in srgb, var(--error) 55%, transparent);
}

/* The one place a lower contrast is the right call: this text is 20-28px, so
   3:1 clears WCAG, and staying quiet is what makes the typed text read as
   progress rather than as more of the same. */
.char {
  color: var(--text-untyped);
  transition: color var(--dur-fast) linear;
}

/* Colour alone separates typed from untyped only weakly in the light theme,
   where the untyped text has to stay dark enough to read comfortably. The
   extra weight carries the distinction without costing contrast — and in a
   monospace face it shifts nothing, because every weight shares one advance
   width. */
.char.correct {
  color: var(--text);
  font-weight: 500;
}

.char.incorrect {
  color: var(--error);
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

.char.extra {
  color: var(--error);
  opacity: 0.55;
}

.caret {
  position: absolute;
  top: 0;
  left: 0;
  width: 2px;
  /* Replaced with the exact glyph-box height by the renderer on first move. */
  height: calc(var(--fs-type) * 1.3);
  background-color: var(--accent);
  /* Locks to the character grid, but glides between positions. Long enough to
     read as movement, short enough that it is not still catching up a couple
     of characters later at speed. */
  transition: transform var(--dur-glide) var(--ease);
  will-change: transform;
}

/* Typing feedback outlives "reduce motion".

   The blanket reset in base.css is aimed at animation that plays by itself
   while you watch it. These two do not: they fire only in response to a
   keypress, the caret travels exactly one character, and together they are how
   the interface says "that key landed". Snapped to zero they read as a stutter,
   which is the one thing this screen must never do. The blink is a different
   matter and stays switched off — it repeats on its own, forever.

   `:not(.no-transition)` leaves the renderer's deliberate jump back to the
   first character on a new test alone. */
@media (prefers-reduced-motion: reduce) {
  .caret:not(.no-transition) {
    transition-duration: var(--dur-glide) !important;
  }

  .char {
    transition-duration: var(--dur-fast) !important;
  }
}

/* Blink only while the typist is idle, so it never fights the typing. */
.caret.idle {
  animation: caret-blink 1s steps(1, end) infinite;
}

@keyframes caret-blink {
  0%, 50% { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

.focus-overlay {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  cursor: pointer;
  color: var(--text-muted);
  font-size: var(--fs-sm);
  background-color: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: blur(3px);
}

.quote-meta {
  min-height: 1.25rem;
  color: var(--text-muted);
  font-size: var(--fs-sm);
  font-style: italic;
}

.test-actions {
  display: flex;
  justify-content: center;
}

/* --- results -------------------------------------------------------------- */

.results {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}

.results__hero {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-7);
  flex-wrap: wrap;
}

.stat {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

.stat__label {
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

.stat__value {
  font-size: var(--fs-stat);
  font-weight: 500;
  line-height: 1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.stat--hero .stat__value {
  font-size: var(--fs-hero);
  font-weight: 700;
  letter-spacing: -0.03em;
  color: var(--accent);
}

.results__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--sp-5);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--border);
}

.results__grid .stat__value {
  font-size: var(--fs-lg);
}

.results__note {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  font-style: italic;
}

.results__actions {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

/* --- chart ---------------------------------------------------------------- */

.chart {
  /* The readout is positioned against this box. */
  position: relative;
  padding: var(--sp-4);
  background-color: var(--bg-elev);
}

.chart svg {
  width: 100%;
  height: auto;
  cursor: crosshair;
}

.chart__caption {
  margin-top: var(--sp-3);
  color: var(--text-muted);
  font-size: var(--fs-xs);
}

.chart__grid {
  stroke: var(--border);
  stroke-width: 1;
}

.chart__tick {
  fill: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 11px;
}

.chart__area {
  fill: var(--accent);
  opacity: 0.08;
}

.chart__wpm {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* A supporting line, not text — it should sit behind the wpm line. */
.chart__raw {
  fill: none;
  stroke: var(--text-untyped);
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 3 4;
}

.chart__error {
  fill: var(--error);
}

.chart__empty {
  fill: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 13px;
}

/* --- chart readout -------------------------------------------------------- */

/* The readout locks to whole seconds, but slides between them rather than
   jumping — same feel as the caret between characters. */
.chart__guide,
.chart__hover-wpm,
.chart__hover-raw {
  transition: transform var(--dur-glide) var(--ease);
}

/* Suppressed for the frame the readout appears on, so it does not slide in
   from the position it was last left at. */
.chart__hover--instant .chart__guide,
.chart__hover--instant .chart__hover-wpm,
.chart__hover--instant .chart__hover-raw,
.readout--instant {
  transition: none;
}

/* The caret's exception to "reduce motion", for the same reason — see the note
   in base.css. This slide happens because the pointer moved, so it is an answer
   to an input rather than something that plays by itself while you watch.

   The `:not()` guards leave the two deliberate jumps above untouched: the frame
   the readout first appears on, and the moment it flips to the other side of
   the point. */
@media (prefers-reduced-motion: reduce) {
  .chart__hover:not(.chart__hover--instant) .chart__guide,
  .chart__hover:not(.chart__hover--instant) .chart__hover-wpm,
  .chart__hover:not(.chart__hover--instant) .chart__hover-raw {
    transition-duration: var(--dur-glide) !important;
  }

  .readout:not(.readout--instant) {
    transition-duration: var(--dur-glide) !important;
  }
}

.chart__guide {
  stroke: var(--text-untyped);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}

.chart__hover-wpm {
  fill: var(--accent);
}

.chart__hover-raw {
  fill: var(--text-untyped);
}

.readout {
  position: absolute;
  z-index: 2;
  /* Anchored above the point it describes, centred on it. */
  transform: translate(-50%, calc(-100% - 10px));
  /* The 10px here is graph.js's GAP — keep the two in step. */
  transition: left var(--dur-glide) var(--ease), top var(--dur-glide) var(--ease);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border);
  background-color: var(--bg);
  box-shadow: 0 2px 10px var(--shadow);
  /* Never eat the pointer — that would make it flicker as it follows. */
  pointer-events: none;
  white-space: nowrap;
}

/* Flipped under the point when the top of the plot leaves no room above. */
.readout--below {
  transform: translate(-50%, 10px);
}

.readout__time {
  margin-bottom: var(--sp-1);
  color: var(--text);
  font-size: var(--fs-xs);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.readout__rows {
  display: grid;
  gap: 2px;
  font-size: var(--fs-xs);
}

.readout__rows > div {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-4);
}

.readout__rows dt {
  color: var(--text-muted);
}

.readout__rows dd {
  margin: 0;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* --- stats page ----------------------------------------------------------- */

.stats {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}

.stats__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--border);
}

.stats__title {
  color: var(--text);
  font-size: var(--fs-lg);
  font-weight: 500;
  letter-spacing: 0.08em;
}

.stats__empty {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  line-height: 1.6;
}

.stats__body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}

/* Counted, not auto-fitted. There are exactly six figures here, so the only
   layouts that come out even are 6, 3, 2 and 1 across — auto-fit happily picked
   five and left "avg consistency" stranded on a row of its own. Six across is
   out too: at the measure that leaves ~145px a column, and "14m 12s" at this
   size needs more. So three, in two rows. */
.stats__overview {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--sp-5);
}

@media (min-width: 40rem) {
  .stats__overview {
    grid-template-columns: repeat(3, 1fr);
  }
}

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

/* Its own scroll container on both axes: sideways because nine columns do not
   fit a phone, and downwards so a few hundred runs cannot push the rest of the
   page out of reach. Being the scroller is also what lets the header stick. */
.history {
  max-height: min(70vh, 32rem);
  overflow: auto;
  border: 1px solid var(--border);
  background-color: var(--bg-elev);
}

.history__table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

.history__table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--sp-3);
  border-bottom: 1px solid var(--border);
  /* Opaque, or the rows would read straight through it as they pass under. */
  background-color: var(--bg-elev);
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-weight: 400;
  letter-spacing: 0.08em;
  text-align: left;
  white-space: nowrap;
}

.history__table td,
.history__table tbody th {
  padding: var(--sp-2) var(--sp-3);
  /* Half-strength: with this many rows a full-contrast rule between each one
     turns the table into a grid of boxes. */
  border-bottom: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
  color: var(--text);
  font-weight: 400;
  text-align: left;
  white-space: nowrap;
}

.history__table tbody tr:last-child td,
.history__table tbody tr:last-child th {
  border-bottom: none;
}

.history__table tbody tr:hover td,
.history__table tbody tr:hover th {
  background-color: var(--bg-sunken);
}

/* Right-aligned so each column of numbers can be read as a column. Two
   classes, which outranks the `.history__table td` above without !important. */
.history__table .history__num {
  text-align: right;
}

.history__table .history__index {
  color: var(--text-muted);
}

/* --- on-screen keyboard --------------------------------------------------- */

.keyboard {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: center;
  padding: var(--sp-3);
  background-color: var(--bg-elev);
  user-select: none;
}

.keyboard[hidden] {
  display: none;
}

/* The keyboard needs room a phone does not have. This breakpoint lived in
   layout.css with the other responsive rules and silently lost: both selectors
   are a single class, a media query adds no specificity, and components.css is
   read last. So the keyboard stayed on phones — 642px of it, dragging the page
   sideways. It has to sit after `.keyboard` above, which means it has to sit
   here. */
@media (max-width: 900px) {
  .keyboard {
    display: none;
  }
}

.keyboard__row {
  display: flex;
  gap: 6px;
}

.key {
  display: grid;
  place-items: center;
  min-width: 2.25rem;
  height: 2.25rem;
  padding-inline: 0.4rem;
  border: 1px solid var(--border);
  background-color: var(--bg-sunken);
  color: var(--text-muted);
  font-size: var(--fs-xs);
  line-height: 1;
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease);
}

.key--wide { min-width: 3.5rem; }
.key--wider { min-width: 4.5rem; }
.key--space { min-width: 14rem; }

/* The key the typist should hit next. */
.key.next {
  color: var(--accent);
  border-color: var(--accent);
}

/* The key actually hit, flashed for a moment. */
.key.pressed {
  color: var(--bg);
  background-color: var(--accent);
  border-color: var(--accent);
}
