/* ============================================================
   THEME — "Field Notes"
   Warm paper and slate ink, with one hue per meaning:
     teal   = action and focus (buttons, links, focus ring, current step)
     violet = earned progress (streaks, mastery)
     amber  = unsaved state (progress saved on this device only)
     red    = a wrong answer
   Nothing is ever two of these. Light is the primary mode; dark is a
   deliberate second palette rather than a filter over the first.

   The older --bg/--accent/--text names are kept as aliases so the rest of
   this file (and anything reading them in JS) keeps working; new work
   should reach for the semantic names below them.
   ============================================================ */
:root {
  color-scheme: light;

  /* -- Paper and ink -- */
  --paper: oklch(0.96 0.012 85);
  --surface: oklch(0.985 0.008 85);
  --surface-tint: oklch(0.975 0.01 85);
  --surface-input: oklch(0.99 0.006 85);
  --hairline: oklch(0.88 0.014 85);
  --hairline-strong: oklch(0.82 0.014 85);
  --track: oklch(0.9 0.012 85);
  --ink: oklch(0.22 0.015 240);
  --ink-heading: oklch(0.24 0.015 240);
  --body: oklch(0.36 0.015 240);
  --muted: oklch(0.5 0.015 240);

  /* -- Action -- */
  --teal: oklch(0.5 0.09 200);
  --teal-hover: oklch(0.43 0.08 200);
  --teal-text: oklch(0.45 0.07 200);
  --teal-tint: oklch(0.96 0.03 200);
  --teal-tint-border: oklch(0.82 0.05 200);
  --on-teal: oklch(0.99 0.01 200);

  /* -- Earned progress -- */
  --violet: oklch(0.55 0.09 300);
  --violet-text: oklch(0.42 0.09 300);
  --violet-tint: oklch(0.94 0.03 300);
  --violet-tint-border: oklch(0.78 0.05 300);
  --violet-track: oklch(0.88 0.02 300);

  /* -- Unsaved -- */
  --amber: oklch(0.62 0.11 60);
  --amber-tint: oklch(0.95 0.04 60);
  --amber-tint-border: oklch(0.84 0.07 60);
  --amber-text: oklch(0.42 0.08 60);

  /* -- Right --
     The fifth hue, and the one the handoff didn't name. It sits on a
     different axis from violet: green is the verdict on *this answer*,
     violet is progress you have *kept*. Both can be on screen at once —
     an answer flashes green as the violet streak segment it just filled
     lights up. Hue 145 is 55 deg off the teal so it can't be mistaken for
     an action.

     The lightness is tuned by measured contrast rather than by matching
     the red's OKLCH numbers: relative luminance weights green far more
     heavily than red, so a green set to the same perceptual lightness
     lands around 3.8:1 on its own tint — under AA, and visibly fainter
     than the wrong state beside it. L 0.51 puts it at 4.88:1 against the
     red's 4.83:1, so right and wrong read at the same strength. */
  --correct: oklch(0.51 0.13 145);
  --correct-bg: oklch(0.96 0.035 145);
  --correct-border: oklch(0.8 0.07 145);
  --correct-ink: oklch(0.42 0.13 145);

  /* -- Wrong -- */
  --error: oklch(0.55 0.14 25);
  --error-bg: oklch(0.98 0.02 25);
  --error-ink: oklch(0.46 0.14 25);

  /* The -ink pair is for *text sitting on its own tint* — a graded button,
     a marked choice, the input after a verdict. --correct and --error stay
     the saturated versions and keep doing borders, marks and bars, where
     the shape carries the meaning and the colour only has to be
     recognisable. Text has to be read, so it goes deeper: both -ink values
     are tuned to ~7.2:1 on their tint (AAA) and to within 0.15 of each
     other, so "I got it" and "I missed it" are equally legible. */

  /* -- Type -- */
  --font-display: "Crimson Pro", Georgia, "Times New Roman", serif;
  --font-ui: "Assistant", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: "Space Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* The sitelen pona face. Falls back to the display serif, which matters:
     the fallback is a *readable romanised sentence*, not tofu. See the
     @font-face note below. */
  --font-sitelen: "nasin nanpa", var(--font-display);

  /* -- Shape. No shadows anywhere except the focus ring. -- */
  --radius: 3px;
  --radius-pill: 20px;
  --focus-ring: 0 0 0 3px oklch(0.55 0.09 200 / 0.14);
  --shadow: none;

  /* -- Aliases for the pre-rebrand variable names -- */
  --bg: var(--paper);
  --bg-elevated: var(--surface);
  --border: var(--hairline);
  --text: var(--ink);
  --text-dim: var(--muted);
  --accent: var(--teal);
  --accent-strong: var(--teal-hover);
  --wrong: var(--error);
  --wrong-bg: var(--error-bg);
}

/* Dark mode. Applied when the user has picked dark, or when they have made
   no choice at all and the OS prefers it — hence the two selectors sharing
   one body. theme.js stamps data-theme on <html> before first paint. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --paper: oklch(0.2 0.012 240);
  --surface: oklch(0.25 0.014 240);
  --surface-tint: oklch(0.235 0.012 240);
  --surface-input: oklch(0.25 0.014 240);
  --hairline: oklch(0.32 0.014 240);
  --hairline-strong: oklch(0.38 0.014 240);
  --track: oklch(0.3 0.014 240);
  --ink: oklch(0.96 0.008 240);
  --ink-heading: oklch(0.96 0.008 240);
  --body: oklch(0.82 0.012 240);
  --muted: oklch(0.7 0.012 240);

  --teal: oklch(0.7 0.09 200);
  --teal-hover: oklch(0.78 0.1 200);
  --teal-text: oklch(0.74 0.08 200);
  --teal-tint: oklch(0.28 0.035 200);
  --teal-tint-border: oklch(0.42 0.05 200);
  --on-teal: oklch(0.18 0.03 200);

  --violet: oklch(0.72 0.08 300);
  --violet-text: oklch(0.78 0.07 300);
  --violet-tint: oklch(0.3 0.035 300);
  --violet-tint-border: oklch(0.45 0.05 300);
  --violet-track: oklch(0.36 0.03 300);

  --amber: oklch(0.72 0.11 60);
  --amber-tint: oklch(0.3 0.04 60);
  --amber-tint-border: oklch(0.45 0.06 60);
  --amber-text: oklch(0.86 0.07 60);

  /* Same measured match in dark: 4.70:1 against the red's 4.69:1. */
  --correct: oklch(0.67 0.14 145);
  --correct-bg: oklch(0.3 0.045 145);
  --correct-border: oklch(0.48 0.07 145);
  --correct-ink: oklch(0.8 0.12 145);

  --error: oklch(0.68 0.14 25);
  --error-bg: oklch(0.29 0.05 25);
  --error-ink: oklch(0.8 0.12 25);

  --focus-ring: 0 0 0 3px oklch(0.7 0.09 200 / 0.22);
}

* { box-sizing: border-box; }
button, input, select, textarea { font-family: inherit; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.6;
  display: flex;
  flex-direction: column;
}

/* The page proper, under the header bar. Wider than the pre-rebrand 640px:
   the picker rows and the drill's progress strip both need the room. */
.app {
  width: 100%;
  max-width: 900px;
  margin: 0 auto;
  padding: 44px 32px 40px;
  display: flex;
  flex-direction: column;
  gap: 30px;
  flex: 1;
}

/* ---- Site header ----
   A full-bleed surface strip with a hairline under it, its contents held to
   the same 900px column as the page body. Brand lockup left; identity,
   theme toggle and sign-out right. */
header.site-header {
  background: var(--surface);
  border-bottom: 1px solid var(--hairline);
}
.site-header-inner {
  width: 100%;
  max-width: 900px;
  margin: 0 auto;
  padding: 12px 28px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}
.site-header-left {
  display: flex;
  align-items: center;
  gap: 14px;
  min-width: 0;
}
.site-header-right {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* ---- Brand lockup ----
   Mark plus wordmark, always lowercase. The mark is an aperture: a ring
   with an off-centre pupil, reading as "seeking". Two CSS circles, no
   asset. --mark-size scales both parts together. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  text-decoration: none;
  color: inherit;
  flex-shrink: 0;
}
/* The mark: the site's name written in sitelen pona, in place of the teal
   ring that used to sit here. It says the same thing as the wordmark
   beside it, in the other script — which is the whole subject of the site,
   so the pair reads as a bilingual lockup rather than as a logo with a
   caption.

   Drawn from paths inlined in the markup, not set in the webfont. The
   drills falling back to romanised text is desirable; a mark that can turn
   into a serif word is a mark with two identities, so this one carries its
   own outlines and cannot degrade: no fetch, nothing to fail. It also has
   to survive on the three pages that never load the font at all.

   Sized by height, since the viewBox is the glyphs' own ink box. It takes
   the ink colour rather than the old mark's teal: teal means action
   everywhere else on the site, and a logo is not one. */
.brand-lockup {
  height: 21px;
  width: auto;
  display: block;
  flex: none;
  color: var(--ink);
}

/* The wordmark, in the one typeface used nowhere else on the site — which
   is the point: in Crimson Pro it was the same face as every heading, so
   it read as a line of text that happened to be in the corner rather than
   as a logo.

   Baloo 2 rather than something louder because of what it sits next to.
   The sitelen pona glyphs are monoline with rounded joins, and Baloo's
   even strokes and round terminals are built the same way, so the mark
   and the word read as one object instead of two. A hand-drawn face would
   have been funnier and would have fought them.

   It is loaded &text=-subsetted to just these characters (see the <head>
   of each page), so the whole face costs a few hundred bytes. Nothing
   else may use it without widening that subset. */
.brand-word {
  font-family: "Baloo 2", var(--font-display);
  font-size: 21px;
  font-weight: 600;
  /* Baloo sits high in its em box and carries generous built-in line
     spacing; 1 leaves it visibly above the glyphs' centre. Nudged rather
     than nudged with margins so the flex row keeps doing the aligning. */
  line-height: 1.35;
  color: var(--ink);
  white-space: nowrap;
}

/* ---- Breadcrumb in the header ----
   Where a page sits below the picker, the trail lives beside the brand.
   The ↑ key box inside the link *is* the shortcut, not decoration. */
.crumbs {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--muted);
  min-width: 0;
}
.crumbs-sep {
  color: var(--hairline-strong);
}
.crumbs a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--teal-text);
  font-weight: 600;
  text-decoration: none;
  border-bottom: 1px solid var(--teal-tint-border);
}
.crumbs a:hover { color: var(--teal-hover); }
.crumbs-current {
  color: var(--ink-heading);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- Key box ----
   The shortcut indicators. Textual on purpose: they should match what is
   printed on a keyboard. */
.key {
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.6;
  border: 1px solid var(--hairline-strong);
  border-radius: 2px;
  padding: 0 5px;
  background: var(--surface-input);
  color: var(--muted);
}

/* ---- Theme toggle ----
   A two-state segmented control, persisted by theme.js. */
.theme-toggle {
  display: flex;
  align-items: center;
  gap: 2px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 2px;
  background: var(--surface-input);
  flex: none;
}
.theme-toggle button {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.4;
  padding: 3px 8px;
  border: none;
  border-radius: 2px;
  background: none;
  color: var(--muted);
  cursor: pointer;
}
.theme-toggle button[aria-pressed="true"] {
  background: var(--track);
  color: var(--ink-heading);
}

/* ============================================================
   SITELEN PONA
   ============================================================
   Toki Pona's logographic script, drawn by nasin nanpa 4.0.2 (MIT,
   vendored under shared/fonts/ — see the LICENSE beside it).

   The mechanism, and why the data never changes: the font carries
   OpenType ligature rules that map each romanised word onto its glyph —
   "j","a","n" become the jan glyph. So switching scripts is a
   font-family swap and nothing else. Every sentence in tokipona/*.js
   stays romanised, the typed-answer input still holds "jan li sona e
   lipu", and grading never learns that any of this happened.

   It also means the failure mode is benign. If the font doesn't load,
   the fallback in --font-sitelen renders the same string as ordinary
   romanised Toki Pona; nothing becomes unreadable. (The font's
   UCSUR-only variant would have degraded to tofu boxes instead — which
   is why it isn't the one vendored here.)
   ============================================================ */
@font-face {
  font-family: "nasin nanpa";
  src: url("/shared/fonts/nasin-nanpa-4.0.2.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  /* swap, not block: the romanised text is a perfectly good thing to be
     looking at while 87KB arrives. */
  font-display: swap;
}

/* Elements the drills stamp with data-target-lang hold target-language
   text — Toki Pona here, but the attribute is language-agnostic and the
   other three languages stamp it too. Only this rule, gated on the Toki
   Pona page's data-script, does anything with it. */
/* The face reaches descendants too, not just the stamped element: a
   stamped container's children often set their own font-family (the
   choice buttons do), and a declaration beats plain inheritance. The
   line-height below is kept off this rule, since it only makes sense
   applied once. */
:root[data-script="sitelen"] [data-target-lang],
:root[data-script="sitelen"] [data-target-lang] *,
:root[data-script="sitelen"] .ref-example .tl,
:root[data-script="sitelen"] .ref-irregular .wrong-form,
:root[data-script="sitelen"] .ref-irregular .correct-form,
.sitelen-sample {
  font-family: var(--font-sitelen);
  /* Two traps, both load-bearing:

     letter-spacing disables ligatures outright — the text silently falls
     back to Latin letters. Nothing here sets any, but the eyebrows on
     this page use 0.14em, so zero it rather than trust inheritance.

     text-transform is what lets the sentences stay capitalised. The
     ligatures are lowercase-only (the font defines ~3250 of them and
     exactly one — "Linku" — starts with a capital), so "Jan li moku."
     would otherwise draw a Latin "J" next to three glyphs. Lowercasing
     in CSS fixes the rendering without touching the string: input.value
     still reads back exactly what the learner typed. Sitelen pona has no
     case to lose. */
  letter-spacing: 0;
  text-transform: lowercase;
  font-variant-ligatures: common-ligatures contextual;
}

/* Line height only, on the stamped element alone. Square glyphs sitting
   on the baseline crowd each other at the serif's ratios.

   Deliberately no font-size here. The glyphs do read a touch small at the
   size of the serif they replace, but there is no way to say "12% larger
   than whatever this element already was": an em font-size resolves
   against the *parent*, so the rule that looks like a nudge in fact reset
   the 40px prompt to 18px. The sizes stay exactly as the Latin design
   sets them, and every layout below keeps working. */
:root[data-script="sitelen"] [data-target-lang],
:root[data-script="sitelen"] .ref-example .tl {
  line-height: 1.35;
}

/* The romanised reading of the typed box, shown under it in sitelen pona
   mode — see updateTypedEcho in grammar-engine.js for why it has to
   exist. Deliberately NOT stamped data-target-lang: being Latin is the
   entire point, so it must stay Latin whatever the page is set to.
   Monospace so the word gaps are wide and countable. */
.typed-echo {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
  min-height: 1.5em;
  margin: -2px 0 8px;
  word-spacing: 0.2em;
}

/* The placeholder is English instructions sitting in a box that is set in
   the target script, so it opts back out of all of the above. */
:root[data-script="sitelen"] .typed-input[data-target-lang]::placeholder {
  font-family: var(--font-ui);
  font-size: 15px;
  text-transform: none;
}

/* ---- Script toggle ----
   The Latin/sitelen control, persisted by script-toggle.js and mounted
   only on the Toki Pona page. Each half is set in the script it selects.

   It sits on the drill cards rather than in the site header. In the
   header it was a pair of small glyphs among five other bits of chrome,
   and read as another theme switch; on the card it is next to the
   sentence it redraws, which is the only place the choice means
   anything. */
.card-tools {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 16px;
}
.script-toggle {
  display: flex;
  align-items: center;
  gap: 2px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 2px;
  background: var(--surface-input);
  flex: none;
}
.script-toggle button {
  font-family: var(--font-display);
  font-size: 14px;
  line-height: 1.4;
  padding: 1px 8px;
  border: none;
  border-radius: 2px;
  background: none;
  color: var(--muted);
  cursor: pointer;
}
.script-toggle button[aria-pressed="true"] {
  background: var(--track);
  color: var(--ink-heading);
}
/* Named, unlike the theme control: two renderings of the same word are
   only self-explanatory once you already know what the control does. */
.script-toggle-label {
  display: flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 0 6px 0 4px;
}
/* Smaller than the key caps on the buttons: this one labels a control
   that is itself chrome, so it shouldn't outweigh the choice beside it. */
.script-toggle-label .key {
  font-size: 10px;
  line-height: 1.5;
  padding: 0 4px;
  letter-spacing: 0;
}
/* The sitelen half draws its own label in the font whatever the page is
   currently set to — it is a sample, not a piece of the page's text. */
.sitelen-sample {
  font-size: 15px;
}

/* ---- Page headings ---- */
.page-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 40px;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.page-subtitle {
  margin: 8px 0 0;
  font-size: 16px;
  line-height: 1.55;
  color: var(--muted);
}
.page-head { max-width: 560px; }

.eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.back-link {
  display: inline-block;
  margin-bottom: 10px;
  color: var(--muted);
  text-decoration: none;
  font-size: 13px;
}
.back-link:hover { color: var(--teal); }

/* ---- Identity bar (sign in / sign out) ----
   Auth state is load-bearing chrome: signed in, progress follows the
   account; signed out, it lives in this browser only and the amber chip
   has to say so. Amber is used here and nowhere else. */
.identity-bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  flex-wrap: wrap;
  min-width: 0;
  font-size: 13px;
  color: var(--muted);
}
.identity-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border-radius: var(--radius-pill);
  padding: 5px 12px 5px 9px;
  font-size: 12.5px;
  line-height: 1.4;
  background: var(--teal-tint);
  border: 1px solid var(--teal-tint-border);
  color: var(--teal-text);
  max-width: 100%;
}
.identity-chip::before {
  content: "";
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: var(--teal);
}
.identity-chip .identity-email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.identity-chip.unsaved {
  background: var(--amber-tint);
  border-color: var(--amber-tint-border);
  color: var(--amber-text);
}
.identity-chip.unsaved::before { background: var(--amber); }
.identity-btn {
  font-family: inherit;
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  color: var(--body);
  border-radius: var(--radius);
  padding: 6px 13px;
  font-size: 13px;
  cursor: pointer;
}
.identity-btn:hover { color: var(--ink); border-color: var(--muted); }
.identity-form {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.identity-form input {
  font-family: inherit;
  background: var(--surface-input);
  border: 1px solid var(--hairline-strong);
  color: var(--ink);
  border-radius: var(--radius);
  padding: 6px 10px;
  font-size: 13px;
  min-width: 200px;
}
.identity-hint {
  color: var(--muted);
  font-size: 13px;
}

/* One focus treatment everywhere: a 3px teal halo. */
:where(a, button, input, select, summary, [tabindex]):focus-visible {
  outline: none;
  border-color: var(--teal);
  box-shadow: var(--focus-ring);
}

@media (max-width: 700px) {
  .app { padding: 28px 18px 32px; }
  .site-header-inner { padding: 10px 18px; flex-wrap: wrap; }
  .page-title { font-size: 32px; }
}

/* Landing page: Vocab Drill entry + Grammar Drill tracks, one list */
/* "Vocab Drill" / "Grammar Drill" — eyebrows rather than headings. They
   label two groups of cards; the cards carry the weight. */
.section-heading {
  margin: 0 0 12px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
.section-heading:not(:first-child) {
  margin-top: 32px;
}

/* Shared card look */
.card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: 28px 24px;
}

.reset-btn {
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  color: var(--body);
  border-radius: var(--radius);
  padding: 7px 14px;
  font-size: 13px;
  cursor: pointer;
}
.reset-btn:hover { color: var(--ink); border-color: var(--muted); }

/* ---- Vocab drill: practice set ----
   Collapsed by default, like the progress panel. The selection is a thing
   you set once and then leave alone, so it should not take up room above
   every question — but a collapsed panel hides its own state, hence the
   summary line in the header. */
.scope-summary {
  /* Takes the slack so the show/hide control's own margin-left:auto has
     nothing left to claim and stays put on the right. */
  flex: 1 1 auto;
  text-align: right;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--muted);
  margin-right: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.scope-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 18px;
}
.scope-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.scope-label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
/* The native arrow is drawn in the OS's own blue on some platforms, which
   is the one colour this palette has no room for — so the control is
   stripped back and given a chevron of its own in the text colour. */
.scope-select {
  flex: 1;
  min-width: 0;
  appearance: none;
  background-color: var(--surface-input);
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 17px center, right 12px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  color: var(--body);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius);
  padding: 9px 34px 9px 12px;
  font-size: 14px;
  cursor: pointer;
}
.scope-select:hover { border-color: var(--muted); color: var(--ink); }
.pos-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.pos-chip {
  background: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--muted);
  border-radius: var(--radius-pill);
  padding: 5px 13px;
  font-size: 12.5px;
  cursor: pointer;
}
.pos-chip:hover { border-color: var(--hairline-strong); color: var(--body); }
.pos-chip.active {
  background: var(--teal);
  color: var(--on-teal);
  border-color: var(--teal);
  font-weight: 600;
}

/* ---- Vocab drill: progress panel ----
   Violet throughout: every number in here is something already earned. */
.tier-progress {
  margin-top: 0;
  display: grid;
  gap: 18px;
}
.tier-row-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 14px;
  margin-bottom: 6px;
  gap: 8px;
}
.tier-row-name { font-weight: 600; color: var(--ink); }
.tier-row-name.locked { color: var(--muted); font-weight: 400; }
.tier-row-count {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--violet-text);
  white-space: nowrap;
}
.tier-bar {
  height: 6px;
  border-radius: 1px;
  background: var(--track);
  overflow: hidden;
}
.tier-bar-fill {
  height: 100%;
  background: var(--violet);
}
.tier-lock-note {
  margin-top: 5px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}
.progress-actions {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--hairline);
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.word-list {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid var(--hairline);
  display: grid;
  gap: 18px;
}
.word-list-tier-name {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 8px;
}
.word-list-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  font-size: 14px;
  padding: 6px 0;
  border-top: 1px solid var(--hairline);
}
.word-list-row:first-of-type { border-top: none; }
/* Target-language text is set in the display face, like the drill prompt
   and the reference examples — not tinted teal, which would read as a
   link in a list of plain words. */
.word-list-tl {
  font-family: var(--font-display);
  font-size: 17px;
  color: var(--ink);
}
.word-list-en { color: var(--muted); text-align: right; }

/* ---- Vocab drill card ----
   The word being drilled leads, the way the grammar prompt does. Centred
   here rather than left-aligned: this prompt is one word, not a sentence
   that grows, so there is no ragged left edge to hunt for. */
.vocab-category,
.vocab-direction {
  text-align: center;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
.vocab-category { margin-bottom: 6px; }
.vocab-direction { margin-bottom: 18px; }
.vocab-prompt {
  text-align: center;
  font-family: var(--font-display);
  font-size: 40px;
  font-weight: 400;
  line-height: 1.2;
  color: var(--ink);
  padding: 20px 8px 12px;
  min-height: 1.4em;
  text-wrap: pretty;
}
.vocab-prompt.empty-state {
  font-family: var(--font-ui);
  font-size: 15px;
  color: var(--muted);
  line-height: 1.6;
  padding: 28px 8px;
}
/* The revealed answer. Ink, not teal: it is the thing you are reading, and
   nothing about it is clickable. */
.vocab-answer {
  text-align: center;
  font-family: var(--font-display);
  font-size: 26px;
  line-height: 1.3;
  color: var(--body);
  padding-bottom: 16px;
  min-height: 1.4em;
}

/* The romanised reading of the word, revealed under the answer in the
   sitelen pona deck only — see vocabReveal in vocab-engine.js. A glyph
   card asks two things at once ("which word is this, and what does it
   mean"), so the reveal has to answer both or it only half-marks itself.

   Deliberately NOT stamped data-target-lang, and a sibling of the answer
   rather than a child of it: being Latin is the entire point, and either
   one would hand it straight back to the sitelen rule above. Monospace
   and muted for the same reason .typed-echo is — it is a reading aid
   sitting under the real answer, not a second answer. */
.vocab-echo {
  text-align: center;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted);
  margin-top: -10px;
  padding-bottom: 12px;
}

.btn-row {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-top: 16px;
}
button.primary {
  background: var(--teal);
  color: var(--on-teal);
  border: none;
  border-radius: var(--radius);
  padding: 12px 24px;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
button.primary:hover { background: var(--teal-hover); }

button.grade-good,
button.grade-bad {
  border-radius: var(--radius);
  padding: 11px 20px;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
button.grade-good {
  background: var(--correct-bg);
  color: var(--correct-ink);
  border: 1px solid var(--correct);
}
button.grade-bad {
  background: var(--wrong-bg);
  color: var(--error-ink);
  border: 1px solid var(--wrong);
}
button.grade-good:hover { border-color: var(--correct-ink); }
button.grade-bad:hover { border-color: var(--error-ink); }

/* ---- Session complete card (both modes) ----
   The score is the one thing on this card, so it gets the display face at
   size. Violet: a finished session is earned, and the only teal here
   should be the button that starts the next one. */
.session-complete { text-align: center; }
.session-complete-title {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 6px;
}
.session-complete-score {
  font-family: var(--font-mono);
  font-size: 40px;
  font-weight: 700;
  color: var(--violet-text);
  margin-bottom: 20px;
}

/* ---- Drill progress strip ----
   Board 4a's three panels, divided by hairlines and reading left to right
   as: where am I in this session, what is the gate I am working against,
   and which of the concept's two levels am I on. Teal for the session bar
   (it is the current step), violet for the streak (it is earned), and
   nothing else. */
.progress-strip {
  display: flex;
  align-items: stretch;
  flex-wrap: wrap;
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  background: var(--surface);
  margin-bottom: 22px;
  overflow: hidden;
}
.strip-panel {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px 18px;
  border-left: 1px solid var(--hairline);
  min-width: 0;
}
.strip-panel:first-child { border-left: none; }
.strip-session { flex: 1 1 240px; }
.strip-gate {
  flex: 0 1 250px;
  background: var(--surface-tint);
}
.strip-levels { flex: 0 1 200px; }

.strip-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.strip-eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
.strip-eyebrow-earned { color: var(--violet-text); }
.strip-count {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}
.strip-count strong {
  font-weight: 700;
  color: var(--ink);
}
.strip-caption {
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted);
}

/* Session: one continuous bar. No tick at 12 — see renderProgressStrip,
   the 12 is a per-level attempt count and belongs to the streak panel. */
.strip-bar {
  height: 6px;
  border-radius: 1px;
  background: var(--track);
  overflow: hidden;
}
.strip-bar span {
  display: block;
  height: 100%;
  background: var(--teal);
}
/* The vocab drill's second panel measures what has been learned, not where
   you are in the session, so it fills violet like every other earned
   number on the site. */
.strip-bar-earned span { background: var(--violet); }

/* Streak: one segment per answer needed, so the run reads as discrete
   steps — which is what a streak is. */
.strip-seg {
  display: flex;
  gap: 3px;
  height: 8px;
}
.strip-seg i {
  flex: 1;
  border-radius: 1px;
  background: var(--violet-track);
}
.strip-seg i.on { background: var(--violet); }
.strip-gate-muted .strip-caption { color: var(--muted); }

/* Levels: a filled violet disc for a level already cleared, a teal ring
   for the one in play, an empty outline for one not reached. */
.strip-level {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--muted);
}
.strip-disc {
  width: 11px;
  height: 11px;
  flex: none;
  border-radius: 50%;
  border: 1px solid var(--hairline-strong);
  background: var(--track);
}
.strip-level.done { color: var(--violet-text); }
.strip-level.done .strip-disc {
  background: var(--violet);
  border-color: var(--violet);
}
.strip-level.current {
  color: var(--ink);
  font-weight: 700;
}
.strip-level.current .strip-disc {
  background: none;
  border: 2px solid var(--teal);
}

/* ---- Level switch ----
   Deliberately quiet, and below the drill rather than beside the score:
   dropping to multiple choice is a step down, and restarting is a thing
   you do rarely. */
.level-switch {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--hairline);
}

@media (max-width: 620px) {
  .strip-panel {
    flex: 1 1 100%;
    border-left: none;
    border-top: 1px solid var(--hairline);
  }
  .strip-panel:first-child { border-top: none; }
}

/* ---- Reference panel ----
   Board 4a's bordered box: a header strip with the concept named and a
   show/hide control, a grid of worked examples, and a footer carrying the
   one-sentence rule. Above the prompt rather than below it — the prompt
   still leads visually at 40px, but the reference is meant to be open
   beside the question you are answering, not found after it.

   No key box on the toggle: nothing is bound to it. 4a shows ⌃/⌄ in one,
   and a key box that doesn't match a key is worse than none. */
details.reference-panel {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  margin-bottom: 22px;
  padding: 0;
  overflow: hidden;
}
details.reference-panel summary {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 13px 20px;
  cursor: pointer;
  list-style: none;
  font-weight: 400;
  color: var(--ink);
}
details.reference-panel summary::-webkit-details-marker { display: none; }
details[open].reference-panel summary { border-bottom: 1px solid var(--hairline); }
.ref-eyebrow {
  flex: none;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
/* The control is generated rather than marked up, so its label can never
   disagree with whether the panel is actually open. */
details.reference-panel summary::after {
  content: "Show ⌄";
  margin-left: auto;
  font-size: 13px;
  font-weight: 600;
  color: var(--teal-text);
  white-space: nowrap;
}
details[open].reference-panel summary::after { content: "Hide ⌃"; }
details.reference-panel summary:hover::after { color: var(--teal-hover); }

.reference-body { padding: 20px; }

/* Worked examples, three across where there is room. auto-fit rather than
   a fixed three: concepts ship two to four examples, and a fixed grid
   leaves a hole under the short ones. */
.ref-table {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 22px;
  margin: 0;
}
.ref-row {
  border-top: none;
  padding-top: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.ref-focus-label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--teal-text);
  margin-bottom: 2px;
}
.ref-form {
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 700;
  color: var(--muted);
}
.ref-example {
  margin-top: 2px;
  font-size: 13px;
  line-height: 1.5;
}
/* The example itself is the target language, set in the display face like
   the prompt — not in the action colour, which it was before. */
.ref-example .tl {
  font-family: var(--font-display);
  font-size: 19px;
  line-height: 1.3;
  color: var(--ink);
}
.ref-example .en { color: var(--muted); }

/* The footer rule, hairline-separated and full width under the grid. */
.ref-note {
  margin: 20px -20px -20px;
  padding: 14px 20px;
  border-top: 1px solid var(--hairline);
  background: var(--surface-tint);
  font-size: 13px;
  line-height: 1.6;
  color: var(--body);
}
.ref-note strong { color: var(--ink); font-weight: 700; }
.ref-note code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: var(--paper);
  border: 1px solid var(--hairline);
  border-radius: 2px;
  padding: 1px 5px;
}

/* The exception callout. Red, because an exception is the thing that makes
   an otherwise-correct answer wrong. */
.ref-irregular {
  margin-top: 20px;
  padding: 14px 16px;
  border: 1px solid var(--error);
  background: var(--error-bg);
  border-radius: var(--radius);
  font-size: 13px;
  line-height: 1.6;
  color: var(--body);
}
.ref-irregular .ref-focus-label {
  color: var(--error-ink);
  margin-bottom: 6px;
}
.ref-irregular .irregular-example { margin-top: 8px; font-size: 15px; }
.ref-irregular .wrong-form { color: var(--error-ink); text-decoration: line-through; }
.ref-irregular .correct-form { color: var(--correct-ink); font-weight: 700; }
.ref-irregular .irregular-more { margin-top: 6px; color: var(--muted); }

.grammar-english {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 40px;
  line-height: 1.15;
  color: var(--ink);
  max-width: 640px;
  margin-bottom: 24px;
  text-wrap: pretty;
}
/* The stretch of the sentence this concept is about. Underlined in teal as
   the focus of the question, but left in ink — teal *text* here would read
   as a link in the middle of a sentence. */
.grammar-english strong {
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--teal);
  text-underline-offset: 4px;
  text-decoration-thickness: 2px;
}
.grammar-tagalog {
  font-family: var(--font-display);
  font-size: 30px;
  line-height: 1.2;
  color: var(--ink);
  max-width: 640px;
  margin-bottom: 24px;
}
@media (max-width: 620px) {
  .grammar-english { font-size: 28px; }
  .grammar-tagalog { font-size: 24px; }
}
.blank-slot {
  display: inline-block;
  min-width: 90px;
  border-bottom: 2px solid var(--text-dim);
  color: var(--accent);
}

.choices {
  display: grid;
  gap: 10px;
  margin-bottom: 8px;
}
.choice-btn {
  /* The key cap is positioned rather than laid out as a sibling: a choice's
     text is HTML (a <strong> span plus a trailing "."), so making this a
     flex row turned the period into its own flex item and opened a gap
     before it. */
  position: relative;
  background: var(--surface-input);
  border: 1px solid var(--hairline);
  color: var(--ink);
  font-family: var(--font-display);
  border-radius: var(--radius);
  padding: 13px 16px 13px 46px;
  font-size: 19px;
  line-height: 1.35;
  text-align: left;
  cursor: pointer;
}
.choice-btn:hover:not(:disabled) { border-color: var(--teal); }
.choice-btn:disabled { cursor: default; }
.choice-btn.correct { background: var(--correct-bg); border-color: var(--correct); color: var(--correct-ink); }
.choice-btn.wrong { background: var(--wrong-bg); border-color: var(--wrong); color: var(--error-ink); }

.feedback {
  margin-top: 22px;
  padding: 16px 18px;
  border-radius: var(--radius);
  background: var(--surface-tint);
  border: 1px solid var(--hairline);
  display: none;
}
.feedback.show { display: block; }
.feedback .full-sentence {
  font-family: var(--font-display);
  font-size: 24px;
  line-height: 1.3;
  color: var(--ink);
  margin-bottom: 4px;
}
.feedback .full-sentence-en {
  font-size: 14px;
  color: var(--muted);
  margin-bottom: 10px;
}
.feedback .explanation {
  font-size: 14.5px;
  color: var(--body);
  line-height: 1.6;
}

.next-row {
  text-align: center;
  margin-top: 18px;
}

/* ---- Language standing ----
   Board 6b's stats strip, reduced to the part we can honestly fill: how
   much of this language is learned, summed from the same per-track counts
   the cards below show. Violet — earned progress — and the one thing on
   the page that isn't a link.

   One line, and every part of it at the same small size. It is context for
   the cards below, not the headline: the page's job is picking a track,
   and a large number here competes with that. The label and the count are
   told apart by letter-spacing and weight rather than by size, so neither
   half of the fraction reads as the fine print of the other. */
.language-stats {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  padding: 0 0 20px;
  border-bottom: 1px solid var(--hairline);
}
.language-stats:empty {
  display: none;
}
.language-stats-label {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--violet-text);
}
.language-stats-value {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  color: var(--violet-text);
}

/* ---- Track and concept cards ----
   Board 6b sets these as bare rows with a hairline between them, and
   collapses the tail of the list behind a "Show all tracks" link. Kept as
   cards here, and all of them on screen: the whole point of this page is
   picking, and a picker that hides half its options to look tidy is making
   the user work for the tidiness. What is worth taking from 6b is the
   typography and the mastery readout — the serif track name, the violet
   count, and the segment bar that shows the shape of the progress rather
   than only its size. */
.concept-selector-intro {
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--muted);
  margin: 0 0 14px;
}
.concept-grid {
  display: grid;
  gap: 10px;
}
.concept-card {
  position: relative;
  width: 100%;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  color: var(--ink);
  font-family: inherit;
  cursor: pointer;

  /* Title, teaser and progress stack down column 1; the mastery readout
     spans them all in column 2. Laid out by explicit placement rather than
     a wrapper element, so a card that has no mastery block (vocab, review,
     a locked concept) needs no different markup — its column 2 just
     collapses. */
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  column-gap: 24px;
  padding: 16px 22px 16px 56px;
}
.concept-card > * { grid-column: 1; }
.concept-card.active:hover {
  border-color: var(--teal);
  background: var(--surface-input);
}
.concept-card.coming-soon,
.concept-card.locked {
  cursor: default;
  opacity: 0.55;
}
.concept-card-badge {
  justify-self: start;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  padding: 2px 9px;
  margin-bottom: 8px;
}
/* Mixed Review: the same card shape as a track, marked in the action hue
   so it reads as the odd one out at the foot of the list rather than as a
   seventh track. */
.concept-card-badge.review {
  color: var(--teal-text);
  border-color: var(--teal-tint-border);
  background: var(--teal-tint);
}
.concept-card.review-card { border-color: var(--teal-tint-border); }
.concept-card-title {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--ink-heading);
}
.concept-card-teaser {
  margin-top: 4px;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--body);
}
/* The text-only progress line, for cards with no concepts to count
   (the vocab drill, Mixed Review, a concept's own rung and streak). */
.concept-card-progress {
  margin-top: 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--violet-text);
}

/* ---- Track mastery readout ----
   Violet throughout: this is earned progress, and it sits on a card that
   is itself the action, so any teal here would be a second call to click. */
.track-mastery {
  grid-column: 2;
  grid-row: 1 / -1;
  align-self: center;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  width: 108px;
}
.track-mastery-count {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  color: var(--violet-text);
}
.track-mastery-count span {
  font-weight: 400;
  opacity: 0.7;
}
.track-mastery-label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
/* One segment per concept, so the bar shows how long the track is as well
   as how far in you are — four fat segments and twelve thin ones are
   different propositions even at the same percentage. */
.track-seg {
  display: flex;
  gap: 3px;
  width: 100%;
  height: 6px;
}
.track-seg i {
  flex: 1;
  border-radius: 1px;
  background: var(--track);
}
.track-seg i.done { background: var(--violet); }
.track-seg i.started { background: var(--violet-track); }

@media (max-width: 560px) {
  .concept-card {
    grid-template-columns: 1fr;
    row-gap: 12px;
  }
  .track-mastery {
    grid-column: 1;
    grid-row: auto;
    align-items: stretch;
    width: 100%;
  }
  .track-mastery-count, .track-mastery-label { align-self: flex-start; }
}

/* ---- Grammar drill: dev unlock toggle ----
   Only ever mounted on localhost or with ?unlock=1 (see grammar-engine.js).
   Parked in the corner over the page rather than in the flow, so it can sit
   on the landing page, the concept list, and a running session alike
   without any of them having to make room for it. */
.dev-unlock-bar {
  position: fixed;
  right: 12px;
  bottom: 12px;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px 6px 12px;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow);
  font-size: 0.72rem;
  color: var(--text-dim);
  opacity: 0.75;
}
.dev-unlock-bar:hover { opacity: 1; }
.dev-unlock-bar.on {
  border-color: var(--accent-strong);
  opacity: 1;
}
.dev-unlock-label {
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.dev-unlock-bar.on .dev-unlock-label { color: var(--accent); }
.dev-unlock-btn {
  font-family: inherit;
  font-size: 0.72rem;
  padding: 4px 10px;
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: var(--text);
  cursor: pointer;
}
.dev-unlock-btn:hover { border-color: var(--accent); }

/* ---- Grammar drill: concept practice view ---- */
.concept-back-link {
  background: none;
  border: none;
  padding: 0;
  margin-bottom: 10px;
  color: var(--text-dim);
  text-decoration: none;
  font-size: 0.85rem;
  cursor: pointer;
}
.concept-back-link:hover { color: var(--accent); }
.concept-view-title {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: 28px;
  font-weight: 600;
  line-height: 1.2;
  color: var(--ink-heading);
}
.typed-hint {
  font-size: 14px;
  color: var(--text-dim);
  margin-bottom: 6px;
}

.typed-row {
  display: flex;
  gap: 10px;
  margin-bottom: 8px;
}
.typed-input {
  flex: 1;
  background: var(--surface-input);
  border: 1px solid var(--hairline-strong);
  color: var(--ink);
  border-radius: var(--radius);
  padding: 14px 16px;
  font-size: 19px;
}
.typed-input:focus { outline: none; border-color: var(--teal); box-shadow: var(--focus-ring); }
/* The sentence the learner just typed, now carrying a verdict — it is the
   thing they are reading, so it takes the deeper ink like any other
   text-on-a-verdict. The border keeps the saturated colour. */
.typed-input.correct { border-color: var(--correct); color: var(--correct-ink); background: var(--correct-bg); }
.typed-input.wrong { border-color: var(--wrong); color: var(--error-ink); background: var(--wrong-bg); }

/* ---- Language picker ----
   Board 1a's cards: a fixed name column beside a flowing description, on a
   raised surface. Deliberately carries no progress or mastery — that
   belongs on the language page, not in the choice of language. */
.language-grid {
  display: grid;
  gap: 14px;
}
.language-link {
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: 24px;
  padding: 22px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
}
.language-link:hover {
  border-color: var(--teal);
  background: var(--surface-input);
}
.language-link .lang-name {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 600;
  line-height: 1.15;
  color: var(--ink-heading);
}
.language-link .lang-family {
  margin-top: 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--teal-text);
}
.language-link .lang-note {
  font-size: 15px;
  line-height: 1.55;
  color: var(--body);
}
.language-link .lang-meta {
  margin-top: 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--muted);
}
.language-link[aria-disabled="true"] {
  opacity: 0.5;
  pointer-events: none;
}
@media (max-width: 640px) {
  .language-link {
    grid-template-columns: 1fr;
    gap: 12px;
  }
}

/* ---- Site footer: quiet link out ---- */
.site-footer {
  margin-top: auto;
  padding-top: 18px;
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: flex-end;
  font-size: 13px;
  color: var(--text-dim);
}
.site-footer a {
  color: var(--text-dim);
  text-decoration: none;
}
.site-footer a:hover { color: var(--accent); }

/* ---- Keyboard shortcut hints ----
   Every element a keyboard shortcut targets carries a data-shortcut label
   (stamped in shortcuts.js) that renders here as a small key cap. Kept
   deliberately quiet — dim, thin-bordered, and it only picks up the accent
   colour on hover — so the hints read as an aside for anyone who goes
   looking rather than as chrome competing with the content. Hidden on
   touch-primary devices, where there's no keyboard to press. */
.concept-card[data-shortcut]::after,
.choice-btn[data-shortcut]::before,
.concept-back-link[data-shortcut]::after,
button.primary[data-shortcut]::after,
button.grade-good[data-shortcut]::after,
button.grade-bad[data-shortcut]::after {
  content: attr(data-shortcut);
  display: inline-block;
  min-width: 1.55em;
  padding: 1px 0;
  border: 1px solid var(--border);
  border-radius: 2px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.5;
  text-align: center;
  color: var(--text-dim);
  opacity: 0.75;
}
/* In the left gutter, not the top-right corner: the right side of a track
   card is the mastery readout now, and down the left the caps read as the
   numbered list the digit keys actually address. Every card carries the
   gutter as padding whether or not it has a cap, so numbered and
   unnumbered cards still line up. */
.concept-card[data-shortcut]::after {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
}
/* Pinned to the first line rather than vertically centred, so it stays put
   on a choice long enough to wrap. */
.choice-btn[data-shortcut]::before {
  position: absolute;
  top: 14px;
  left: 16px;
}
.concept-card.active:hover::after,
.choice-btn:hover:not(:disabled)::before {
  color: var(--accent);
  border-color: var(--accent);
  opacity: 1;
}
.choice-btn:disabled::before { opacity: 0.4; }
.concept-back-link[data-shortcut]::after,
button.primary[data-shortcut]::after,
button.grade-good[data-shortcut]::after,
button.grade-bad[data-shortcut]::after {
  margin-left: 8px;
  vertical-align: middle;
}
/* On a filled or tinted button the cap borrows the button's own text
   colour — a neutral dim grey reads as disabled against them. */
button.primary[data-shortcut]::after,
button.grade-good[data-shortcut]::after,
button.grade-bad[data-shortcut]::after {
  border-color: currentColor;
  color: inherit;
}
/* Knocked well back on a solid fill, where the cap is white on teal and
   would otherwise shout as loudly as the label. */
button.primary[data-shortcut]::after { opacity: 0.55; }
/* But the graded buttons are a tint, not a fill: the same 0.55 against a
   pale green or pink leaves the cap looking disabled. */
button.grade-good[data-shortcut]::after,
button.grade-bad[data-shortcut]::after { opacity: 0.8; }
@media (hover: none) {
  .concept-card[data-shortcut]::after,
  .choice-btn[data-shortcut]::before,
  .concept-back-link[data-shortcut]::after,
  button.primary[data-shortcut]::after,
  button.grade-good[data-shortcut]::after,
  button.grade-bad[data-shortcut]::after { display: none; }
  .concept-card { padding-left: 22px; }
  .choice-btn { padding-left: 14px; }
}

/* Visually hidden, still read aloud. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
