/*
 * hypertype
 *
 * Two rules govern this stylesheet.
 *
 * 1. Nothing competes with the text you are typing. Chrome is dim by default and
 *    disappears entirely the moment you start. No cards, no shadows, no gradients,
 *    no borders that have not earned their keep.
 *
 * 2. The layout is anchored LEFT, not centred, and organised around a vertical
 *    progress rail that runs beside the text. Settings are a labelled row, not a
 *    floating pill, and the live readout is a corner HUD rather than a heading
 *    above the words. The palette is cold — cyan on slate — not warm.
 *
 *    That is a deliberate departure. The genre's conventions are worth learning
 *    from; wearing them wholesale is not a design.
 */

/* --------------------------------------------------------------- themes -- *
 *
 * This stylesheet knows nothing about any particular colour scheme. It only ever
 * reads the eight custom properties below, and the layout writes them from the
 * Theme row the typist has chosen (see app/models/theme.rb). Adding a scheme is a
 * database row, not an edit here.
 *
 *   --bg          the page
 *   --bg-raise    the only fill colour (cards, inputs, the rail's track)
 *   --sub         untyped text, and all secondary chrome
 *   --text        text you have typed correctly
 *   --accent      caret, progress, active state
 *   --error       text you got wrong
 *   --error-extra characters typed past the end of a word
 *
 * The values here are a FALLBACK, used only if the themes table is empty — a fresh
 * checkout before db:seed. A blank white page is not an acceptable way to report a
 * missing seed.
 */

:root {
  /* The "paper" palette, mirrored from the seed: the default a visitor sees before
     they have chosen anything. */
  --bg: #ffffff;
  --bg-raise: #f1f2f4;
  --sub: #9aa1a9;
  --text: #16191d;
  --accent: #0369a1;
  --accent-dim: #7dd3fc;
  --error: #c81e28;
  --error-extra: #e8a0a4;

  --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Roboto Mono", Menlo, Consolas, monospace;

  /* The words you are typing are the point of the screen, so they are set large:
     comfortably readable without leaning in, at four lines to a screen.
     The leading is generous on purpose — the gap between lines is where the "what
     you actually typed" mark hangs, and it needs room to be read at a glance rather
     than squinted at. */
  --type-size: 2.15rem;
  --line-height: 4.1rem;
  --visible-lines: 4;    /* the brief: never more than four lines at once */

  --rail: 2px;           /* the vertical progress line beside the text */
  --gutter: 2.25rem;     /* space between that rail and the words */

  /* Headroom above the first line, so the "what you actually typed" mark can hang
     over a mistake without being clipped by the top of the window. Without it, a typo
     on line one would show nothing at all. */
  --ghost-space: 1.9rem;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html { height: 100%; }

body {
  min-height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--sub);
  font-family: var(--mono);
  font-size: 19px;      /* everything else is relative to this */
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

a {
  color: var(--sub);
  text-decoration: none;
  transition: color 0.12s ease;
}

a:hover,
a:focus-visible { color: var(--text); }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

/* ---------------------------------------------------------------- shell -- */

.shell {
  width: 100%;
  max-width: 1320px;   /* the text is the point; give it room */
  margin: 0 auto;
  padding: 2rem 2.5rem 3rem;
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* Prose pages read badly at full width — a 1300px line of body copy is a chore to
   track — so only the typing screen takes the extra room. */
.lede,
.auth-form,
.bars,
.advice { max-width: 46rem; }

/* A hairline under the masthead, and the wordmark set flush left with the
   navigation trailing it on the same baseline - not a centred bar. */
.masthead {
  display: flex;
  align-items: baseline;
  gap: 2.5rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--bg-raise);
  transition: opacity 0.25s ease;
}

.logo {
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.03em;
}

.logo span { color: var(--accent); }

.masthead nav {
  display: flex;
  gap: 1.5rem;
  font-size: 0.78rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.masthead nav a {
  padding-bottom: 0.15rem;
  border-bottom: 1px solid transparent;
}

.masthead nav a[aria-current="page"] {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* Everything that is not the text itself gets out of the way once typing
   starts. This is the entire design philosophy in one rule. */
.typing .masthead,
.typing .config,
.typing .controls,
.typing .footnote,
.typing .page-title,
.typing .lede,
.typing .ladder,
.typing .gate-line,
.typing .crumb {
  opacity: 0;
  pointer-events: none;
}

/* --------------------------------------------------------------- config -- */

/* A row of labelled dropdowns, flush left. Every setting is the same kind of
   control, and each one names itself: a typist should not have to infer that "60"
   means seconds rather than words. */
.config {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0.5rem;
  margin: 1.75rem 0 0;
  padding: 0;
  transition: opacity 0.25s ease;
}

.config-key {
  display: block;
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--sub);
  opacity: 0.7;
}

.config-divider { display: none; }

/* <details>, not <select>: every option is a real link, so a configuration is
   shareable, bookmarkable, and survives the back button — and none of it needs
   JavaScript. */
.picker {
  position: relative;
  min-width: 8.5rem;
}

.picker summary {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.5rem 2rem 0.5rem 0.85rem;
  background: var(--bg-raise);
  border-left: 2px solid transparent;
  border-radius: 0 0.35rem 0.35rem 0;
  cursor: pointer;
  list-style: none;
}

.picker summary::-webkit-details-marker { display: none; }

/* The little chevron. */
.picker summary::after {
  content: "";
  position: absolute;
  top: 50%;
  right: 0.85rem;
  width: 0.4rem;
  height: 0.4rem;
  margin-top: -0.3rem;
  border-right: 1.5px solid var(--sub);
  border-bottom: 1.5px solid var(--sub);
  transform: rotate(45deg);
  transition: transform 0.12s ease;
}

.picker[open] summary::after {
  margin-top: -0.1rem;
  transform: rotate(-135deg);
}

.picker summary:hover,
.picker[open] summary { border-left-color: var(--accent); }

.picker-current {
  color: var(--text);
  font-size: 1rem;
  white-space: nowrap;
}

.picker[open] .picker-current { color: var(--accent); }

.picker-menu {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  min-width: 100%;
  padding: 0.4rem;
  background: var(--bg-raise);
  border-left: 2px solid var(--accent);
  border-radius: 0 0.35rem 0.35rem 0;
  box-shadow: 0 14px 36px rgb(0 0 0 / 45%);
}

/* Sixteen languages in one column would run off the screen. */
.picker.wide .picker-menu {
  display: grid;
  grid-template-columns: repeat(2, minmax(8rem, 1fr));
  gap: 0.1rem;
}

.picker-option {
  padding: 0.35rem 0.75rem;
  border-radius: 0.2rem;
  color: var(--sub);
  font-size: 0.95rem;
  white-space: nowrap;
}

.picker-option:hover {
  color: var(--text);
  background: var(--bg);
}

.picker-option.is-active { color: var(--accent); }

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

.typing-app {
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* The text and its rail. The rail is the organising element of the whole screen:
   a thin line beside the words that fills as you get through them, so progress is
   something you see in your peripheral vision rather than a number you must read. */
/* The whole stage is the click target — the text, and all the empty space around it.
   Hunting for a line of text to click on before you can start typing is a small,
   stupid friction, and it is the very first thing anyone does on this page. */
.stage {
  flex: 1;
  display: grid;
  grid-template-columns: var(--rail) 1fr;
  column-gap: var(--gutter);
  align-content: center;
  padding: 2rem 0;
  cursor: text;
}

.keycap-actions {
  margin-bottom: 2rem;
  font-size: 0.85rem;
}

.rail {
  position: relative;
  height: calc((var(--visible-lines) * var(--line-height)) + var(--ghost-space));
  background: var(--bg-raise);
  border-radius: 2px;
}

.rail-fill {
  position: absolute;
  inset: 0 0 auto 0;
  height: 0;
  background: var(--accent);
  border-radius: 2px;
  transition: height 0.15s ease;
}

.stage-body { min-width: 0; }

.words-viewport {
  position: relative;
  height: calc((var(--visible-lines) * var(--line-height)) + var(--ghost-space));
  overflow: hidden;
  cursor: text;
}

.words {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  padding-top: var(--ghost-space);   /* headroom for the typo marks - see --ghost-space */
  font-size: var(--type-size);
  line-height: var(--line-height);
  letter-spacing: 0.005em;
  transition: transform 0.15s ease;
  user-select: none;
}

.word { margin: 0 0.5ch 0 0; }

/* A word you fumbled and moved past keeps a quiet underline, so mistakes stay
   legible afterwards without shouting at you mid-test.
   text-decoration, not a border: a border adds 2px to every word's height, which
   pushes the rendered line spacing off the line-height the caret arithmetic
   depends on, and the four-line window then drifts by a line on a long test. */
.word.flawed {
  text-decoration: underline;
  text-decoration-color: var(--error);
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
}

.char {
  color: var(--sub);
  transition: color 0.05s ease;
}

.char.correct { color: var(--text); }
.char.extra { color: var(--error-extra); }

/* A mistake shows BOTH characters: the one you were meant to type, in red, in its
   proper place — and above it, small and struck through, the one you actually hit.
   Marking the target red says only that something went wrong. Which key you pressed
   instead is the part you can act on.

   Absolutely positioned, so it hangs in the leading above the line and adds nothing
   to the character's box. That matters: the four-line window and the caret both do
   arithmetic on the line height, and a glyph that grew the line would put them out
   by a row. */
.char.incorrect {
  position: relative;
  color: var(--error);
}

.char.incorrect::before {
  content: attr(data-typed);
  position: absolute;
  left: 50%;
  /* Clear of the tallest ascender, so it sits in the leading rather than on top of
     the letter it is commenting on. */
  bottom: 1.45em;
  transform: translateX(-50%);
  padding: 0 0.2em;
  background: var(--bg);   /* its own ground, so it never merges with the line above */
  font-size: 0.72em;
  line-height: 1.1;
  color: var(--error);
  pointer-events: none;
  white-space: pre;
}

/* An underline caret that sits beneath the next character, rather than a bar
   wedged between two of them. It reads as a cursor in a terminal, which is what
   this app is, and it never crowds the glyphs on either side. */
.caret {
  position: absolute;
  top: 0;
  left: 0;
  width: 1ch;
  height: 2px;
  margin-top: calc(var(--type-size) + 0.35rem);
  background: var(--accent);
  border-radius: 2px;
  transition: left 0.08s ease, top 0.08s ease;
  will-change: left, top;
}

.caret.blinking { animation: blink 1.1s infinite; }

@keyframes blink {
  0%, 45% { opacity: 1; }
  55%, 95% { opacity: 0.15; }
  100% { opacity: 1; }
}

/* The live readout: a corner HUD. It is the only thing besides the text visible
   mid-test, and putting it out at the edge of vision keeps it out of the way of
   the words in a way a heading above them never quite manages. */
.readout {
  position: fixed;
  right: 2.5rem;
  bottom: 2rem;
  z-index: 10;
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  opacity: 0;
  transition: opacity 0.2s ease;
  pointer-events: none;
}

.typing .readout,
.finished .readout { opacity: 1; }

.readout > span:first-child {
  font-size: 2.6rem;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.readout .secondary {
  font-size: 0.88rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sub);
}

.finished .readout { opacity: 0; }

/* The way out of a test in progress.
 *
 * Deliberately exempt from the rule that hides everything while you type. A test you
 * cannot stop is a trap, and it is worst in the IME section: while an input method has
 * its candidate window open it owns the keyboard, so Escape may never reach the page.
 *
 * Quiet until wanted — barely there at rest, plain on hover — so it costs the typist
 * nothing but is always where they expect it. */
.abort {
  position: fixed;
  left: 2.5rem;
  bottom: 2rem;
  z-index: 30;
  padding: 0.4rem 0.8rem;
  border-radius: 0.3rem;
  color: var(--sub);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0;
  transition: opacity 0.2s ease, color 0.12s ease;
  pointer-events: none;
}

.typing .abort,
.unfocused .abort {
  opacity: 0.45;
  pointer-events: auto;
}

.abort:hover,
.abort:focus-visible {
  opacity: 1;
  color: var(--accent);
  background: var(--bg-raise);
}

.abort kbd {
  padding: 0.05rem 0.3rem;
  border-radius: 0.2rem;
  background: var(--bg-raise);
  font-family: inherit;
  font-size: 0.62rem;
  letter-spacing: 0.05em;
  text-transform: none;
}

.veil {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: none;
  align-items: center;
  justify-content: flex-start;
  /* Derived from the theme rather than hardcoded, or the veil would stay dark on
     a white page. */
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  backdrop-filter: blur(3px);
  color: var(--text);
  font-size: 0.9rem;
  letter-spacing: 0.05em;
}

.unfocused .veil { display: flex; }
.unfocused .words { filter: blur(3px); }

/* Opening a settings dropdown legitimately takes focus off the typing input, but
   the typist has not wandered off — they are choosing a setting. Nagging them to
   click back into the text, and blurring it behind the menu they are reading, is
   just noise. */
.typing-app:has(.picker[open]) .veil { display: none; }
.typing-app:has(.picker[open]) .words { filter: none; }

.hidden-input {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  opacity: 0;
  z-index: -1;
}

/* In the IME section the input is moved under the caret (see moveInput), because an
   input method anchors its candidate window to the focused input. Giving it the height
   of a line means the candidate list opens just below the character being typed, the
   way it does in any other text field — rather than in the corner of the page. */
.ime-section .hidden-input {
  width: 2px;
  height: var(--type-size);
  z-index: 1;
  caret-color: transparent;
}

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

.controls {
  display: flex;
  align-items: center;
  gap: 1.75rem;
  margin-top: 1.75rem;
  min-height: 2rem;
  transition: opacity 0.25s ease;
}

.restart {
  padding-bottom: 0.2rem;
  border-bottom: 1px solid transparent;
  font-size: 0.82rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--sub);
}

.restart:hover,
.restart:focus-visible {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

.footnote {
  margin-top: 2rem;
  font-size: 0.8rem;
  color: var(--sub);
  transition: opacity 0.25s ease;
}

.footnote kbd {
  padding: 0.1rem 0.35rem;
  border-radius: 0.2rem;
  background: var(--bg-raise);
  font-family: inherit;
  font-size: 0.68rem;
}

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

/* A left rule and a row of figures, not a centred trophy. */
.results {
  display: none;
  padding: 1.5rem 0 1.5rem var(--gutter);
  border-left: var(--rail) solid var(--accent);
}

.finished .results {
  display: block;
  animation: rise 0.25s ease;
}

.finished .stage { display: none; }

@keyframes rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

.headline {
  display: flex;
  align-items: baseline;
  gap: 2.5rem;
  margin-bottom: 2rem;
}

.metric-label {
  font-size: 0.65rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sub);
}

.metric-value {
  font-size: 5rem;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.metric-value.small {
  font-size: 2.5rem;
  color: var(--text);
}

.detail-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 2.5rem;
}

.detail dt {
  font-size: 0.65rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sub);
}

.detail dd {
  font-size: 1.45rem;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.pb-flag {
  color: var(--accent);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  min-height: 1rem;
}

/* ----------------------------------------------------- practice + stats -- */

.page-title {
  margin: 2rem 0 0.6rem;
  color: var(--text);
  font-size: 1.2rem;
  letter-spacing: 0.02em;
  transition: opacity 0.25s ease;
}

.lede {
  max-width: 46rem;
  margin-bottom: 1.75rem;
  font-size: 0.95rem;
  line-height: 1.75;
  transition: opacity 0.25s ease;
}

/* The lesson ladder sits directly under the settings row on the practice screen, and
   without a clear gap the two read as one control block. They are not: one chooses
   the material, the other chooses the exercise. */
.ladder {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 2.75rem 0 2.25rem;
  transition: opacity 0.25s ease;
}

.rung {
  flex: 1 1 12rem;
  padding: 0.85rem 1rem;
  border-left: 2px solid var(--bg-raise);
  background: var(--bg-raise);
  border-radius: 0 0.3rem 0.3rem 0;
  text-align: left;
}

.rung:hover { color: var(--text); }
.rung.is-active { border-left-color: var(--accent); }

.rung strong {
  display: block;
  margin-bottom: 0.2rem;
  color: var(--text);
  font-size: 0.9rem;
}

.rung.is-active strong { color: var(--accent); }

.rung span { font-size: 0.75rem; }

.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 0;
  margin-bottom: 2.5rem;
  border-left: 1px solid var(--bg-raise);
}

.stat {
  padding: 0.9rem 1.25rem;
  border-right: 1px solid var(--bg-raise);
}

.stat dt {
  margin-bottom: 0.35rem;
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--sub);
}

.stat dd {
  font-size: 1.9rem;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}

caption {
  margin-bottom: 0.7rem;
  color: var(--text);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-align: left;
}

th {
  padding: 0.5rem 0.75rem 0.5rem 0;
  color: var(--sub);
  font-weight: 400;
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: left;
  border-bottom: 1px solid var(--bg-raise);
}

td {
  padding: 0.55rem 0.75rem 0.55rem 0;
  color: var(--text);
  border-bottom: 1px solid var(--bg-raise);
  font-variant-numeric: tabular-nums;
}

td.num,
th.num {
  text-align: right;
  padding-right: 0;
}

.keycaps {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 2rem;
}

.keycap {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 3rem;
  padding: 0.5rem;
  border-radius: 0.3rem;
  background: var(--bg-raise);
  border-bottom: 2px solid var(--error);
}

.keycap b {
  color: var(--error);
  font-size: 1.2rem;
}

.keycap small {
  color: var(--sub);
  font-size: 0.68rem;
}

.empty {
  padding: 2rem 0;
  font-size: 0.9rem;
}

.badge {
  padding: 0.15rem 0.5rem;
  border-radius: 0.2rem;
  background: var(--accent);
  color: var(--bg);
  font-size: 0.65rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.section { margin-bottom: 2.5rem; }

/* The coach's single next step. The one piece of chrome allowed to raise its
   voice, because it is the only thing on the page that tells you what to do. */
.advice {
  max-width: 46rem;
  margin-bottom: 2.5rem;
  padding: 1.1rem 1.5rem;
  border-left: 2px solid var(--accent);
  background: var(--bg-raise);
  border-radius: 0 0.4rem 0.4rem 0;
}

.advice h2 {
  margin-bottom: 0.5rem;
  color: var(--accent);
  font-size: 1.05rem;
}

.advice p {
  margin-bottom: 0.9rem;
  font-size: 0.92rem;
  line-height: 1.75;
}

.advice-action {
  display: inline-block;
  padding-bottom: 1px;
  border-bottom: 1px solid var(--sub);
  color: var(--text);
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.advice-action:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* ------------------------------------------------ assessment + courses -- */

.bars {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  max-width: 36rem;
  margin-bottom: 2rem;
}

.bar-row {
  display: grid;
  grid-template-columns: 6.5rem 1fr 3.5rem;
  align-items: center;
  gap: 0.9rem;
  font-size: 0.86rem;
}

.bar-label {
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.bar-track {
  height: 0.35rem;
  border-radius: 2px;
  background: var(--bg-raise);
  overflow: hidden;
}

.bar-fill {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: 2px;
}

.bar-fill.weak { background: var(--error); }

.bar-value {
  text-align: right;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.bar-value.weak { color: var(--error); }

.course-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0.5rem;
  margin-bottom: 3rem;
}

.course-card {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 1rem 1.1rem;
  background: var(--bg-raise);
  border-left: 2px solid transparent;
  border-radius: 0 0.3rem 0.3rem 0;
}

.course-card:hover { border-left-color: var(--sub); }
.course-card.complete { border-left-color: var(--accent); }

.course-card strong {
  color: var(--text);
  font-size: 1rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.course-card.complete strong { color: var(--accent); }

.course-blurb {
  font-size: 0.82rem;
  line-height: 1.5;
  min-height: 2.2rem;
}

.course-progress {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  margin-top: 0.2rem;
}

.pip {
  width: 1.4rem;
  height: 2px;
  border-radius: 2px;
  background: var(--bg);
}

.pip.lit { background: var(--accent); }

.course-count {
  margin-left: auto;
  font-size: 0.68rem;
  letter-spacing: 0.08em;
}

.lesson-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-bottom: 3rem;
}

.lesson {
  display: grid;
  grid-template-columns: 2rem 1fr 5rem;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.25rem;
  background: var(--bg-raise);
  border-left: 2px solid transparent;
  border-radius: 0 0.3rem 0.3rem 0;
}

.lesson.locked { opacity: 0.4; }
.lesson.cleared { border-left-color: var(--accent); }
.lesson.open { border-left-color: var(--sub); }

.lesson-number {
  font-size: 1rem;
  color: var(--sub);
  text-align: center;
}

.lesson.cleared .lesson-number { color: var(--accent); }

.lesson-body {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.lesson-body strong {
  color: var(--text);
  font-size: 1rem;
}

.lesson-aim {
  font-size: 0.86rem;
  line-height: 1.6;
}

.lesson-gate {
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  color: var(--accent);
}

.lesson.locked .lesson-gate { color: var(--sub); }

.lesson-action {
  text-align: right;
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.locked-note {
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sub);
}

.gate-line {
  margin-bottom: 1rem;
  font-size: 0.78rem;
  color: var(--sub);
  transition: opacity 0.25s ease;
}

.gate-line strong { color: var(--accent); }

.crumb { color: var(--sub); }

.verdict {
  min-height: 1.5rem;
  margin-top: 1.25rem;
  padding-left: var(--gutter);
  font-size: 0.95rem;
}

.verdict.pass { color: var(--accent); }
.verdict.fail { color: var(--error); }

.verdict-next {
  display: inline-block;
  margin-top: 0.4rem;
  margin-left: var(--gutter);
  padding-bottom: 1px;
  border-bottom: 1px solid var(--sub);
  color: var(--text);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.verdict-next:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* ------------------------------------------------------------------ ime -- */

/* CJK characters are dense — far more strokes in the same box than a latin letter —
   so they are set larger, and the line is taller to carry the reading underneath
   each one. */
.ime-section {
  --type-size: 2.7rem;
  --line-height: 5.4rem;
  --visible-lines: 4;
}

.words.cjk {
  display: block;
  word-break: break-all;
  line-height: var(--line-height);
  font-family: var(--mono), "PingFang SC", "Hiragino Sans", "Noto Sans CJK SC",
               "Microsoft YaHei", "Yu Gothic", sans-serif;
  letter-spacing: 0.04em;
}

.words.cjk .char {
  display: inline-block;
  position: relative;
  /* The character's own box is exactly one glyph tall, so "just below the glyph" is a
     position we can actually address. Row spacing still comes from the parent's
     line-height; without this, the box inherits that full height and the reading ends
     up floating somewhere above the character it belongs to. */
  line-height: 1;
  height: 1.15em;   /* a box of known height, so "under the glyph" is addressable */
  vertical-align: baseline;
}

/* The latin keys you press to produce this character: pinyin under a hanzi, romaji
   under a kana or kanji. Without it, a learner who cannot yet read the character has
   no way in at all, and the app would be testing their vocabulary, not their typing.

   Absolutely positioned, so it hangs in the leading below the line and adds nothing
   to the character's box — the caret and the four-line window both do arithmetic on
   the line height, and a glyph that grew the line would put them out by a row. */
.words.cjk .char[data-reading]::after {
  content: attr(data-reading);
  position: absolute;
  left: 50%;
  top: calc(100% + 0.15em);   /* immediately under the glyph's box */
  transform: translateX(-50%);
  font-family: var(--mono);
  font-size: 0.3em;
  line-height: 1;
  letter-spacing: 0;
  color: var(--sub);
  white-space: nowrap;
  pointer-events: none;
}

/* The reading follows the state of its character, so a correctly typed character's
   hint recedes and a mistake's stands out. */
.words.cjk .char.correct[data-reading]::after { opacity: 0.45; }
.words.cjk .char.incorrect[data-reading]::after { color: var(--error); }

/* The IME's provisional buffer: the romaji or pinyin entered but not yet
   converted. It sits under the caret and is never scored. */
.composition {
  position: absolute;
  z-index: 4;
  padding: 0.05rem 0.3rem;
  transform: translateY(calc(var(--line-height) * 0.95));
  color: var(--bg);
  background: var(--accent);
  border-radius: 0.2rem;
  font-size: 0.85rem;
  line-height: 1.4;
  white-space: nowrap;
  pointer-events: none;
}

.composition:empty { display: none; }

/* --------------------------------------------------------------- account -- */

.masthead nav.account {
  margin-left: auto;
  align-items: center;
  gap: 1.25rem;
}

/* The theme picker sits in the masthead, so it is smaller and quieter than the
   settings dropdowns on the typing screen. */
.theme-picker {
  min-width: 0;
}

.theme-picker summary {
  padding: 0.3rem 1.6rem 0.3rem 0.7rem;
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.theme-picker summary::after { right: 0.6rem; }

.theme-picker .picker-current {
  font-size: 0.72rem;
  letter-spacing: 0.1em;
}

.theme-picker .picker-menu {
  left: auto;
  right: 0;
  border-left: none;
  border-right: 2px solid var(--accent);
  border-radius: 0.35rem 0 0 0.35rem;
}

/* button_to renders a form, so the option is a <button>: make it lie flat. */
.theme-picker form { display: block; }

.picker-option.swatch {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  text-align: left;
  letter-spacing: 0.04em;
  text-transform: none;
}

/* A miniature of the theme: its page colour, with its caret sitting on it. Far
   more use than the name alone when you are choosing between six of them. */
.swatch-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1rem;
  border: 1px solid;
  border-radius: 0.15rem;
  flex: none;
}

.swatch-chip span {
  display: block;
  width: 2px;
  height: 0.6rem;
  border-radius: 1px;
}

.masthead .cta {
  color: var(--accent);
  border-bottom: 1px solid var(--accent-dim);
}

.masthead .cta:hover { border-bottom-color: var(--accent); }

/* button_to renders a form; make its button read as the link it is. */
.masthead form {
  display: inline;
}

.linkish {
  padding: 0;
  font-size: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--sub);
}

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

.auth-form {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  max-width: 22rem;
  margin-bottom: 2rem;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.field label {
  font-size: 0.65rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--sub);
}

.field input {
  padding: 0.6rem 0.75rem;
  background: var(--bg-raise);
  border: 1px solid transparent;
  border-left: 2px solid var(--bg-raise);
  border-radius: 0 0.3rem 0.3rem 0;
  color: var(--text);
  font: inherit;
  font-size: 1rem;
}

.field input:focus {
  outline: none;
  border-left-color: var(--accent);
}

.hint {
  font-size: 0.7rem;
  color: var(--sub);
}

.checkbox {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.78rem;
  color: var(--sub);
  cursor: pointer;
}

.checkbox input { accent-color: var(--accent); }

.submit {
  align-self: flex-start;
  margin-top: 0.5rem;
  padding: 0.6rem 1.5rem;
  background: var(--accent);
  border-radius: 0.3rem;
  color: var(--bg);
  font-size: 0.78rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 700;
}

.submit:hover { background: var(--text); }

.danger {
  padding: 0.55rem 1.25rem;
  border: 1px solid var(--error);
  border-radius: 0.3rem;
  color: var(--error);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.danger:hover {
  background: var(--error);
  color: var(--bg);
}

.auth-errors {
  padding: 0.75rem 1rem;
  border-left: 2px solid var(--error);
  background: var(--bg-raise);
  border-radius: 0 0.3rem 0.3rem 0;
  color: var(--error);
  font-size: 0.8rem;
}

.flash {
  margin-top: 1.5rem;
  padding: 0.7rem 1rem;
  border-left: 2px solid var(--accent);
  background: var(--bg-raise);
  border-radius: 0 0.3rem 0.3rem 0;
  color: var(--text);
  font-size: 0.82rem;
}

.flash.alert {
  border-left-color: var(--error);
  color: var(--error);
}

/* ----------------------------------------------------------- responsive -- */

@media (max-width: 720px) {
  :root {
    --type-size: 1.2rem;
    --line-height: 2rem;
    --gutter: 1.25rem;
  }

  .shell { padding: 1.25rem 1.25rem 2rem; }

  .masthead {
    flex-wrap: wrap;
    gap: 0.75rem 1.5rem;
  }

  .readout {
    right: 1.25rem;
    bottom: 1rem;
  }

  .readout > span:first-child { font-size: 1.6rem; }

  .metric-value { font-size: 3rem; }

  .headline { gap: 1.5rem; }

  .lesson { grid-template-columns: 1.5rem 1fr; }
  .lesson-action { grid-column: 2; text-align: left; }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
  }
}
