/* ═══════════════════════════════════════════════════════════════════════════
   LE CODEX · CODEX-COMPONENTS.CSS
   The objects of the design system, built from codex-tokens.css. Link order on
   every page: codex-tokens.css → codex-components.css → (page-specific styles).

   Namespacing: every class is prefixed `cx-`. This is the shared vocabulary a
   page composes from. A page may add its own layout classes, but a button, a
   card, an answer option, a badge, a section — those come from HERE so an exam
   card and a stats card are literally the same object, product-wide.

   Principle carried throughout:
     · Hierarchy by weight & space, not by loud colour.
     · Separation by hairline --rule and surface lift (--white on --cream).
     · State by subtle wash + border, paired with position/label (never hue
       alone) — a student always reads correct vs wrong two ways.
     · One mode colour visible at a time, via body.cx-mode-*.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Reset & base ─────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
  background: var(--cream);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--fs-body-lg);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Visible, on-brand focus ring for every interactive element (accessibility:
   never suppress this without an equal replacement). */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--mode-accent);
  outline-offset: 2px;
}

/* ── Layout: page, container, sections ────────────────────────────────────── */
.cx-container { max-width: var(--container); margin: 0 auto; padding: 0 var(--sp-8); }
.cx-container--narrow { max-width: var(--container-narrow); }
@media (max-width: 768px) { .cx-container { padding: 0 var(--sp-5); } }

/* ── THE CANVAS (RUN 65) ──────────────────────────────────────────────────
   The destination's own reading area, and the half of the one-surface shell
   that was never built.

   `.cx-has-nav` clears the fixed bar by exactly its own height and stops
   there, and `.cx-container` above carries no vertical padding at all — so a
   page joining the shell inherited a bar and NOTHING ELSE, and had to invent
   its own reading width and its own top air. Five of them did, differently:
   820px on /dashboard, /progress, /study-plan and /goals, 940px on /session,
   48px of top air on four and 56px on one, against the approved mockup's
   1040px and 60px. Each of those `.container` rules also SHADOWS the shared
   one it sits inside, which is why the drift was invisible from the shared
   sheet.

   `.cx-container` itself is left exactly as it was: it is consumed by
   ui/style_showcase.html and by nothing else in the product, so re-pointing it
   would have changed one page nobody navigates to and none of the five that
   needed it. This is the class the destinations adopt.

   The numbers are the mockup's own, not new ones. */
.cx-canvas {
  max-width: var(--canvas-w);
  margin: 0 auto;
  padding: var(--canvas-pad-y) var(--canvas-pad-x) var(--sp-11);
}
/* A reading column, for a destination whose content is prose or a single
   focal card rather than a grid. */
.cx-canvas--narrow { max-width: var(--container-narrow); }
@media (max-width: 900px) {
  .cx-canvas { padding: var(--sp-6) var(--sp-5) var(--sp-9); }
}

/* Sections carry the alternating-contrast rhythm. Compose a page by stacking
   these: white → cream → ink → cream-d → white. That rhythm is the product's
   signature — see the design reference. */
.cx-section        { padding: var(--sp-11) var(--sp-8); }
.cx-section--white { background: var(--white); }
.cx-section--cream { background: var(--cream); }
.cx-section--cream-d { background: var(--cream-d); }
.cx-section--ink   { background: var(--command); color: var(--on-command); }
@media (max-width: 768px) { .cx-section { padding: var(--sp-10) var(--sp-5); } }

/* ── Typography helpers ───────────────────────────────────────────────────── */
/* Eyebrow: the tiny uppercase structural label. Sans, letter-spaced, muted.
   Takes the mode accent when a mode is active so the label itself signals mode. */
.cx-eyebrow {
  display: block;
  font-family: var(--sans);
  font-size: var(--fs-eyebrow);
  font-weight: 700;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: var(--sp-4);
}
.cx-section--ink .cx-eyebrow { color: var(--on-command-rule); }
.cx-eyebrow--mode { color: var(--mode-accent); }

/* Short rule mark under a heading. */
.cx-rule { display: block; width: 48px; height: 1px; background: var(--ink-3); margin: var(--sp-5) 0; }

/* ── THE HERO ROLES — THE ONLY SERIF IN THE PRODUCT ───────────────────────────
   UI STANDARD v2's ONE RULE: "EB Garamond = greeting + page/card titles ONLY."
   This block IS that list. After RUN 71 the root default is --sans, so serif
   appears nowhere except where a selector below puts it, and adding a name here
   is the only way to make anything serif again.

   IT IS PINNED HERE, IN THE SHARED SHEET, AND THAT ORDER IS LOAD-BEARING. Type
   inherits: before this run every page/card title was serif purely because
   `html, body` was, and not one of them said so. Inverting the root first would
   have turned the greeting, every page title and every card title into Inter in
   one commit, with no declaration changed and nothing to see in a diff — which
   is precisely the failure tests/test_type_law.py cannot detect, because it
   reads declarations and inheritance is not one. scripts/type_census.py is the
   instrument that can, and it is what verified this list.

   WHY BY NAME AND NOT BY SIZE. A rule like "anything above 20px is a hero word"
   would have swept in the stat numerals — the 52px grade, the 44px readiness
   score, the 32px session counts — and those are not titles. The type scale
   names them separately (`--fs-stat`, "big single-number displays") and the ONE
   RULE does not list them, so they are functional and they are now Inter. That
   is the single judgement call in this run and it is flagged for review rather
   than buried: it is visible on /dashboard, /session-summary and /report.

   THE LIST MAY ONLY SHRINK. A new entry widens what the product calls a hero
   word, which is a design decision and not a CSS one.

   `h1` IS THE LIST, almost. Measured with scripts/type_census.py: .ph-title,
   .page-h1, .step-h1, .report-title, .hero-h1, .gm-h1 and #now-title are all
   carried ON h1 elements, so naming the element once covers every page title in
   the product and cannot go stale when a class is renamed. The seven names
   beside it are the ones that are NOT an h1 and would otherwise be missed:

     .dash-greet   a <p>    the greeting the standard names first
     .final-h2     an <h2>  the landing page's closing hero line
     .lobby-h1     a <div>  Le Controle's lobby title, an h1 in everything but tag
     .r-title      a <div>  the diagnostic report's page title
     .empty-h      —        an empty-state card title
     .paper-title  a <div>  the paper chooser's card titles
     .cp-name      a <span> a concept card's name, i.e. that card's title

   Each of those was found by reading the DOM, not by grepping for "title". */
h1, .dash-greet, .final-h2, .lobby-h1, .r-title, .empty-h, .paper-title, .cp-name {
  font-family: var(--serif);
}

/* Headings: weight 400, emphasis via italic <em>, never bold. */
.cx-h1, .cx-h2, .cx-h3, .cx-title {
  font-weight: 400; font-family: var(--serif); line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight); color: var(--ink);
}
.cx-h1 { font-size: var(--fs-h1); line-height: 1.06; }
.cx-h2 { font-size: var(--fs-h2); margin-bottom: var(--sp-4); }
.cx-h3 { font-size: var(--fs-h3); }
.cx-title { font-size: var(--fs-title); font-weight: 500; line-height: 1.2; letter-spacing: 0; }
.cx-h1 em, .cx-h2 em, .cx-h3 em { font-style: italic; }
.cx-section--ink .cx-h1, .cx-section--ink .cx-h2, .cx-section--ink .cx-h3 { color: var(--on-command); }
.cx-section--ink .cx-h1 em, .cx-section--ink .cx-h2 em, .cx-section--ink .cx-h3 em { color: var(--on-command-2); }

/* Deck / lead: the italic sub-heading line under an H2. */
.cx-deck { font-size: var(--fs-body); font-style: italic; color: var(--ink-2); margin-bottom: var(--sp-9); max-width: 640px; }
.cx-body { font-size: var(--fs-body); color: var(--ink-2); line-height: 1.85; }
.cx-body p + p { margin-top: var(--sp-4); }
.cx-body strong { color: var(--ink); font-weight: 500; }
.cx-muted { color: var(--ink-3); }
.cx-mono  { font-family: var(--mono); }

/* SCREEN-READER-ONLY. The standard clip-rect utility: removed from the visual
   page, kept in the accessibility tree and in the tab/heading order. It is NOT
   `display:none` and it is NOT `visibility:hidden` — both of those remove the
   element from the accessibility tree too, which would defeat the entire point.

   WHY IT EXISTS: three student screens rendered with no top-level heading at
   all (RUN 72 measured it in the DOM — /questions, /session-summary, /learn).
   A screen-reader user navigates a page by its headings; with zero, the core
   screen of the product presents as an undifferentiated run of text. WCAG 2.4.6
   (AA) and 1.3.1 (A). The remediation is one hidden h1 per screen, and it
   changes nothing on screen.

   The 1x1 box is deliberate rather than 0x0: a zero-area element is dropped
   from the accessibility tree by some engines, and tests/test_type_law_rendered
   .py judges an element visible by its bounding rect — so this h1 IS measured
   by the rendered type gate, and it is permitted serif for the ordinary reason
   that every other h1 in the product is: `h1` is a hero role. */
.cx-sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Buttons ──────────────────────────────────────────────────────────────── */
.cx-btn {
  display: inline-block; text-align: center; text-decoration: none;
  font-family:var(--sans); font-size: 16px; font-weight: 500;
  padding: 14px 28px; border: none; border-radius: var(--radius);
  cursor: pointer; transition: opacity var(--t-fast), border-color var(--t-fast), color var(--t-fast);
  letter-spacing: 0.01em;
}
.cx-btn:active { transform: translateY(0.5px); }
/* Primary = ink fill. Hover shifts opacity only, never hue. */
.cx-btn--primary { background: var(--command); color: var(--on-command); }
.cx-btn--primary:hover { opacity: 0.85; }
/* Secondary / ghost = outline, fills tone within the ink scale on hover. */
.cx-btn--ghost { background: transparent; color: var(--ink-2); border: var(--bw-hair) solid var(--rule); }
.cx-btn--ghost:hover { border-color: var(--ink); color: var(--ink); }
.cx-btn--block { display: block; width: 100%; }
.cx-btn[disabled], .cx-btn.is-disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
/* On dark surfaces, invert. */
.cx-section--ink .cx-btn--primary,
.cx-on-dark .cx-btn--primary { background: var(--white); color: var(--ink); }
.cx-section--ink .cx-btn--ghost,
.cx-on-dark .cx-btn--ghost { color: var(--on-command-2); border-color: var(--on-command-rule); }
.cx-section--ink .cx-btn--ghost:hover,
.cx-on-dark .cx-btn--ghost:hover { color: var(--on-command); border-color: var(--on-command-2); }

/* ── Cards & panels ───────────────────────────────────────────────────────── */
.cx-card {
  background: var(--white); border: var(--bw-hair) solid var(--rule);
  border-radius: var(--radius); padding: var(--sp-6);
}
.cx-card--pad-lg { padding: var(--sp-7); }
.cx-card--flush  { padding: 0; overflow: hidden; }     /* for cards that frame a mockup */
/* Dark "command" card — hero stat blocks, engine-recommendation banners. */
.cx-card--dark { background: var(--command); border-color: var(--ink); color: var(--on-command); }
.cx-card--dark .cx-eyebrow { color: var(--on-command-2); }

/* ── Stat tile ────────────────────────────────────────────────────────────── */
.cx-stat__num   { font-size: var(--fs-stat); font-weight: 400; line-height: 1; letter-spacing: var(--ls-tight); color: var(--ink); }
.cx-stat__label { font-family: var(--sans); font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-3); margin-top: var(--sp-1); }
.cx-stat__body  { font-size: var(--fs-small); font-style: italic; color: var(--ink-2); margin-top: var(--sp-2); line-height: 1.6; }
.cx-section--ink .cx-stat__num   { color: var(--on-command); }
.cx-section--ink .cx-stat__label { color: var(--on-command-rule); }
.cx-section--ink .cx-stat__body  { color: var(--on-command-2); }

/* A row of stat tiles separated by hairline gaps (the 1px-background trick:
   the grid gap shows --rule through opaque cells). */
.cx-stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--rule); border: var(--bw-hair) solid var(--rule); }
.cx-stat-grid > * { background: var(--white); padding: var(--sp-6); }
.cx-section--ink .cx-stat-grid { background: var(--on-command-rule); border-color: var(--on-command-rule); }
.cx-section--ink .cx-stat-grid > * { background: var(--on-command-rule); }
@media (max-width: 768px) { .cx-stat-grid { grid-template-columns: 1fr; } }

/* ── Tier / price cards ───────────────────────────────────────────────────── */
.cx-tier-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--rule); border: var(--bw-hair) solid var(--rule); }
@media (max-width: 768px) { .cx-tier-grid { grid-template-columns: 1fr; } }
.cx-tier { background: var(--cream); padding: var(--sp-7) var(--sp-6); }
.cx-tier--featured { background: var(--command); }        /* the highlighted plan inverts */
.cx-tier__label  { display: block; font-family: var(--sans); font-size: 8px; font-weight: 700; letter-spacing: var(--ls-eyebrow); text-transform: uppercase; color: var(--ink-3); margin-bottom: var(--sp-4); }
.cx-tier__price  { font-size: var(--fs-stat); font-weight: 400; line-height: 1; letter-spacing: -0.03em; color: var(--ink); margin-bottom: var(--sp-1); }
.cx-tier__period { font-size: 15px; font-style: italic; color: var(--ink-2); margin-bottom: var(--sp-5); }
.cx-tier__sep    { height: 1px; background: var(--rule); margin-bottom: var(--sp-5); }
.cx-tier__list   { list-style: none; margin-bottom: var(--sp-6); }
.cx-tier__list li { font-size: 14.5px; color: var(--ink-2); padding: 7px 0; border-bottom: var(--bw-hair) solid var(--rule); display: flex; align-items: center; gap: var(--sp-3); }
.cx-tier__list li:last-child { border-bottom: none; }
.cx-tier__check  { color: var(--correct); font-style: normal; flex-shrink: 0; }
.cx-tier--featured .cx-tier__label  { color: var(--on-command-2); }
.cx-tier--featured .cx-tier__price  { color: var(--on-command); }
.cx-tier--featured .cx-tier__period { color: var(--on-command-2); }
.cx-tier--featured .cx-tier__sep    { background: var(--on-command-rule); }
.cx-tier--featured .cx-tier__list li { color: var(--on-command-2); border-color: var(--on-command-rule); }
.cx-tier--featured .cx-tier__check  { color: var(--correct-on-dark); }

/* ── Chips (filters / tags) ───────────────────────────────────────────────── */
.cx-chip {
  font-family: var(--sans); font-size: 9px; font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; padding: 4px 10px; border: var(--bw-hair) solid var(--rule);
  background: var(--white); color: var(--ink-3); border-radius: var(--radius); cursor: pointer;
  transition: border-color var(--t-fast), color var(--t-fast);
}
.cx-chip:hover { border-color: var(--ink-3); color: var(--ink-2); }
.cx-chip.is-active { background: var(--command); border-color: var(--ink); color: var(--on-command); }

/* ── Mastery bands (status badges) ────────────────────────────────────────── */
/* Five visual tiers (the engine's 10 mastery bands collapse to these). Each is
   a low-opacity tint of a semantic colour, never a solid fill. */
.cx-band { display: inline-block; font-family: var(--sans); font-size: 8px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; padding: 2px 6px; border-radius: var(--radius); }
.cx-band--new        { color: var(--ink-3); border: var(--bw-hair) solid var(--rule); }
/* THE LOWEST BAND IS NOT AN ERROR. This was var(--wrong-bg)/var(--wrong), so the
   mastery ladder ran new -> RED -> amber -> green -> green, and a student who had
   just started a topic was labelled in the colour this product uses to mean "you
   got this wrong". Nothing was wrong; they had only begun.

   Neutral now, which is what 44c settled for Progress: mastered sage, developing
   NEUTRAL, needs-work amber, not-yet grey. "Building" already owns amber one line
   below, so red was never carrying a distinction the ladder needed. Found by
   tests/test_accent_law.py — the fourth instance of a defect fixed by eye three
   times in RUN 44, and the first found in the shared component sheet rather than
   on one screen. */
.cx-band--developing { background: var(--cream-d);    color: var(--ink-2); }
.cx-band--building   { background: var(--amber-bg);   color: var(--amber); }
.cx-band--confident  { background: var(--correct-bg); color: var(--correct); }
.cx-band--mastered   { background: var(--correct-bg-2); color: var(--correct); }

/* ── List rows ────────────────────────────────────────────────────────────── */
.cx-row { display: flex; align-items: center; gap: var(--sp-3); padding: 11px 16px; border-bottom: var(--bw-hair) solid var(--rule); cursor: pointer; transition: background var(--t-fast); }
.cx-row:last-child { border-bottom: none; }
.cx-row:hover { background: var(--cream); }
.cx-row.is-selected { background: var(--cream-d); }

/* ── Question card + answer options (the core learning surface) ───────────── */
/* The question is the thing the student reads; give it a bordered, lifted
   surface with generous line-height so math breathes. */
.cx-question { background: var(--white); border: var(--bw-hair) solid var(--rule); border-radius: var(--radius); padding: 24px 28px; }
.cx-question__meta { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: var(--sp-4); }
.cx-question__text { font-size: var(--fs-body); line-height: var(--lh-loose); color: var(--ink); }
.cx-question__text em { font-style: italic; }

/* Answer option: a full-width selectable button. Default is quiet. Selection,
   correct, and incorrect are each a SUBTLE wash + border shift + a leading key
   letter — three signals, so meaning never rests on colour alone. Use real
   <button> elements so keyboard/AT get them for free. */
.cx-option {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  font-family:var(--sans); font-size: 15px; color: var(--ink-1);
  background: var(--cream); border: var(--bw-hair) solid var(--rule); border-radius: var(--radius);
  padding: 12px 16px; margin-bottom: 6px; cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.cx-option:hover { border-color: var(--ink-3); }
.cx-option__key { font-family: var(--sans); font-size: 8px; font-weight: 800; letter-spacing: 0.1em; color: var(--ink-3); flex-shrink: 0; }
/* Selected but not yet graded — ink outline, cream-d fill. */
.cx-option.is-selected { border-color: var(--ink); background: var(--cream-d); }
/* Graded states — SUBTLE green / red. */
.cx-option.is-correct   { border-color: var(--correct); background: var(--correct-bg); color: var(--correct); }
.cx-option.is-correct   .cx-option__key { color: var(--correct); }
.cx-option.is-incorrect { border-color: var(--wrong);   background: var(--wrong-bg);   color: var(--wrong); }
.cx-option.is-incorrect .cx-option__key { color: var(--wrong); }
.cx-option.is-dimmed { opacity: 0.4; }

/* A short inline feedback line under the options (correct-approach note, etc.) */
.cx-feedback { font-size: 13px; font-style: italic; line-height: 1.6; padding: 10px 14px; border-radius: var(--radius); margin-top: var(--sp-2); }
.cx-feedback--correct { background: var(--correct-bg); border: var(--bw-hair) solid var(--sage-rule); color: var(--correct); }
.cx-feedback--wrong   { background: var(--wrong-bg);   border: var(--bw-hair) solid var(--sienna-rule); color: var(--wrong); }

/* ── Form controls (settings / profile / onboarding) ─────────────────────── */
.cx-field { margin-bottom: var(--sp-5); }
.cx-label { display: block; font-family: var(--sans); font-size: var(--fs-eyebrow); font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3); margin-bottom: var(--sp-2); }
.cx-input, .cx-select, .cx-textarea {
  width: 100%; padding: 11px 14px; font-family:var(--sans); font-size: var(--fs-body);
  color: var(--ink); background: var(--white); border: var(--bw-hair) solid var(--rule); border-radius: var(--radius);
  transition: border-color var(--t-fast);
}
.cx-input::placeholder, .cx-textarea::placeholder { color: var(--ink-3); }
.cx-input:focus, .cx-select:focus, .cx-textarea:focus { border-color: var(--ink); outline: none; }

/* ── Brand lockup (the real Atelier de Maths mark + wordmark) ─────────────── */
/* The mark is static/img/codex-mark.svg — the BRAND's mark, byte-identical to
   08_brand/atelier-brand-assets/mark/mark.svg since RUN 51 (it was a hand trace
   before, on every migrated page). The wordmark is "ATELIER" over "de MATHS",
   the "de" in serif italic.

   This used to say the mark was "inlined so it inherits currentColor — ink on
   light surfaces, white on the dark nav". Neither half held: it is referenced
   with <img>, not inlined, and currentColor does not cross an <img> boundary —
   see the long note in static/js/cx_nav.js. On-dark surfaces use the brand's
   own codex-mark-cream.svg. */
.cx-brand { display: flex; align-items: center; gap: 16px; text-decoration: none; }
.cx-mark { flex-shrink: 0; line-height: 0; color: var(--ink); }   /* set color on the parent to theme it */
.cx-mark svg { display: block; height: auto; }
/* ── THE BRAND LOCKUP ────────────────────────────────────────────────────────
   One component, two variants, every screen. .cx-lockup is the mark plus the
   wordmark. The WORDMARK inherits `color`, so the on-ink and on-cream variants
   are the same markup in a different context. The MARK does not — it is an
   <img>, and the sentence that used to be here claimed otherwise (see the
   cx_nav.js note); the on-dark surfaces point their <img> at
   codex-mark-cream.svg, which is the brand's own variant.

   LETTER-SPACING IS FIXED AT 0.04em, per the design brief. It was 0.14em here,
   0.3em on .cx-nav__wm-top and 0.02em on the italic — three wordmarks with
   three different tracking values, which is what the brief means by retiring
   the text-only lockups. The case goes with it: Cinzel small-caps at 0.14em was
   a label; "Atelier / de Maths" set in title case is a wordmark. */
/* min-height 44: the lockup is a LINK to /dashboard on every app surface,
   and it measured 99x34 at 390px — the same undersized target repeated on
   every screen. The nav is taller than 44 already, so this changes no
   layout; it only stops the hit area being smaller than the thing it
   looks like. RUN 72. */
.cx-lockup { display: inline-flex; align-items: center; gap: 11px; text-decoration: none; color: var(--ink); min-height: 44px; }
.cx-lockup__mark { flex-shrink: 0; display: block; width: 26px; height: auto; }
/* THE MARK FLIPS WITH THE THEME — ink on light, cream on dark, per v2's brand
   line. An <img> cannot inherit `color` (see the note above), so the lockup
   ships both assets and exactly one is displayed. Keyed on [data-theme] rather
   than prefers-color-scheme because cx_prefs.js writes that attribute and it is
   the only thing the rest of this sheet keys on; two mechanisms for one theme
   is how a light mark ends up on a dark bar. */
.cx-lockup__mark--cream { display: none; }
[data-theme="dark"] .cx-lockup__mark--ink { display: none; }
[data-theme="dark"] .cx-lockup__mark--cream { display: block; }
.cx-wordmark { display: flex; flex-direction: column; line-height: 1.1; text-align: left; }
.cx-wordmark__1,
.cx-wordmark__2 { font-family: var(--font-wordmark); font-weight: 600; font-size: 15px; letter-spacing: 0.04em; color: inherit; }
.cx-wordmark__2 { font-weight: 400; font-size: 11.5px; font-style: italic; opacity: 0.72; }
.cx-wordmark__2 em { font-family: var(--serif); font-style: italic; }
/* The on-ink variant: set the colour once on the lockup and the mark, the
   wordmark and the italic all follow it, because every one of them inherits.
   This replaced three separate colour rules and an rgba() that could not
   re-theme. */
/* THE BAR IS PART OF THE PAGE, NOT A LID ON IT (Elite Design Standard).
   This rule used to force the lockup to --on-command because the bar was a
   near-black slab. The standard's frame is one continuous cream surface
   separated from the canvas by a single hairline and nothing else — no fill,
   no shadow — so the lockup takes the page's own ink and this override goes.
   Kept as a no-op-shaped rule rather than deleted so the intent is legible:
   the lockup inherits, and the bar decides. */
.cx-nav .cx-lockup, .cx-nav .cx-mark { color: var(--ink); }

/* ── Top navigation (canonical app + marketing chrome) ──────────────────────
   60px · lockup left · destinations CENTRED · meta and identity right · one
   1px hairline underneath and no shadow. The centred group is what makes it
   read as a frame rather than a toolbar: `margin: 0 auto` on the links, with
   the two end clusters sized by their content. */
.cx-nav { position: fixed; top: 0; left: 0; right: 0; z-index: 200; height: var(--nav-h); background: var(--cream); border-bottom: var(--bw-hair) solid var(--rule); display: flex; align-items: center; gap: var(--sp-5); padding: 0 var(--sp-6); }
.cx-nav__brand { display: flex; align-items: center; gap: 16px; text-decoration: none; }
/* .cx-nav__wm-top / .cx-nav__wm-bot are GONE. They were a second text-only
   wordmark — Garamond 800 at 0.3em tracking — used by style_showcase alone,
   and they are two of the three clashing lockups the brief calls out. The
   showcase now renders .cx-lockup like every other surface, which is the point
   of a showcase. */
/* CENTRED, and centred by `auto` margins rather than by justify-content, so
   the group stays in the middle of the BAR however wide the two end clusters
   grow.

   THE FACE HAS MOVED TWICE AND v2 SETTLES IT — INTER, 13.5px, SENTENCE CASE.
   Before RUN 44 these were 9px letter-spaced uppercase system-ui labels, "the
   loudest remaining piece of web-app chrome in the app"; RUN 44 moved them to
   EB Garamond 14.5px to make type structure the page. UI STANDARD v2's ONE
   RULE reverses the DESTINATION of that move while keeping its reasoning: the
   serif is for hero words — the greeting, the page title, the card title — and
   a nav tab is functional chrome however quietly it is set. So it goes to the
   named sans rather than back to the system stack, which is the part RUN 44
   was right about.

   WEIGHT CARRIES THE ACTIVE STATE ALONGSIDE THE UNDERLINE: 500 resting, 600
   active. That is a FOURTH signal on the active tab, after the 2px accent, the
   darker ink and aria-current — and it is the one that survives both a
   colourblind reader and a monochrome print. Sentence case is kept from 44;
   v2's type scale asks for it and uppercase tracking is what made the old bar
   read as an app. */
.cx-nav__links { display: flex; align-items: center; gap: 28px; margin: 0 auto; height: 100%; }
.cx-nav__link { position: relative; display: inline-flex; align-items: center; height: 100%; font-family: var(--sans); font-size: 13.5px; font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--ink-2); text-decoration: none; background: none; border: none; cursor: pointer; padding: 0; transition: color var(--t-fast); }
.cx-nav__link:hover { color: var(--ink); }
/* "YOU ARE HERE" IS A 2px ACCENT UNDERLINE ON THE BAR'S OWN EDGE — not a pill,
   not a fill. It sits at the bottom of the full-height link so it lands
   exactly on the hairline, replacing it for the width of the active word.
   Three signals still, never colour alone: the rule, the darker label, and
   aria-current in the markup. */
.cx-nav__link.is-active { color: var(--ink); font-weight: 600; }
.cx-nav__link.is-active::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: var(--nav-accent); }
.cx-nav__cta { padding: 9px 24px; background: var(--white); color: var(--ink); font-family: var(--sans); font-size: 9px; font-weight: 800; letter-spacing: 0.18em; text-transform: uppercase; text-decoration: none; border-radius: var(--radius); white-space: nowrap; transition: opacity var(--t-fast); }
.cx-nav__cta:hover { opacity: 0.82; }
/* ── The nav spine (Wave 2 surface #1) ─────────────────────────────────────
   One stable top-level nav, identical on every screen, rendered from ONE source
   (static/js/cx_nav.js) so no page hand-maintains a copy that can drift.

   COLOUR IS NEVER ALONE. Every item carries an icon AND a text label, and the
   active item is marked three ways at once: the mode-accent underline, a
   brighter colour, and aria-current="page". A student who cannot distinguish
   the accent still knows where they are, and so does a screen reader. */
/* NO ICON LIBRARY ON THE DESKTOP BAR. The six line glyphs are gone from it —
   the standard structures a page with type, Cinzel labels and 1px hairlines,
   and a row of little strokes beside four words is decoration standing in for
   hierarchy. They are KEPT on the mobile bottom bar, where the label is 9px
   and genuinely needs the help; that is a different object with a different
   constraint, not an exception smuggled back in.

   This does not weaken "colour is never the only signal". The icon was never
   what distinguished the destinations — the words are, and they are always
   there. The ACTIVE state is what colour could have carried alone, and it is
   still marked three ways: the accent rule, the darker label, aria-current. */
.cx-nav__icon { width: 13px; height: 13px; flex-shrink: 0; display: block; }
.cx-nav__icon svg { width: 100%; height: 100%; display: block;
                    stroke: currentColor; fill: none; stroke-width: 1.6;
                    stroke-linecap: round; stroke-linejoin: round; }

/* The right-hand cluster: a meta slot, You, and the way out. */
.cx-nav__end { display: flex; align-items: center; gap: var(--sp-4); }

/* THE META SLOT IS EMPTY UNTIL A PAGE HAS SOMETHING TRUE TO PUT IN IT.
   The reference shows "7-day streak" here. Two things make that exact string
   wrong for this product: docs/STYLE_GUIDE.md and the voice canon rule out
   gamified framing, and session_server already computes the honest version
   (`sessions_this_week` — distinct days studied in the last seven). So the
   spine renders an EMPTY element and a page fills it from its own fetched
   data; a page with no data renders no chip rather than a placeholder. */
.cx-nav__meta:empty { display: none; }
/* THE CHIP IS INTER 500 ON --chip, not a serif line inside a ring. v2 lists
   chips under the sans face and gives the chip its own surface token, so the
   pill now reads as a chip rather than as body text someone drew a border
   round. The hairline stays: --chip and --navbg are close enough in light that
   the pill would otherwise lose its edge against the bar. */
.cx-nav__meta {
  font-family: var(--sans); font-size: 12px; font-weight: 500;
  color: var(--ink-3); background: var(--chip);
  border: var(--bw-hair) solid var(--rule); border-radius: 999px; padding: 5px 12px;
  white-space: nowrap;
}
/* Identity still reads as a name rather than a destination — but it does it in
   the FUNCTIONAL face now. v2's ONE RULE gives the serif to hero words only,
   and a nav item is chrome however personal its label. */
.cx-nav__link--you {
  font-family: var(--sans); font-weight: 500;
  font-size: 12px; letter-spacing: 1px; text-transform: uppercase;
}

/* SIGN OUT. Quieter than a destination — it is an action you take once, not a
   place you go — but never hidden behind a hover menu: a student on a shared
   school computer must be able to SEE how to leave. Reaches 44px through
   padding while reading as a small label. */
.cx-nav__signout {
  font-family: var(--sans); font-size: 12.5px; font-weight: 500;
  letter-spacing: 0; text-transform: none;
  color: var(--ink-3); background: none; border: none;
  cursor: pointer; padding: 15px 0; transition: color var(--t-fast);
}
.cx-nav__signout:hover { color: var(--ink); }

/* ── FOCUS STRIP ─────────────────────────────────────────────────────────────
   In practice and exam the question owns the screen. The bar does not vanish —
   a student who cannot leave a paper feels trapped, and on mobile the bottom
   bar is the only way out — it collapses: destinations drop away, the lockup
   and the way back remain, and the height comes down to the point where the
   chrome stops competing with the mathematics.

   Set by adding .cx-focus to <body>. Deliberately a class a page opts into
   rather than something derived from the mode, because "in a question" is not
   the same as "in a mode": the session chooser is study mode too, and it needs
   its navigation. */
/* --nav-h COMES DOWN WITH THE BAR. It is the token any page uses to sit
   something under the nav, and leaving it at 72px while the bar collapsed to
   52 meant a 20px band of page showing through above every sticky element on a
   focus surface — exam_mode.html's chrome strip is `top: var(--nav-h)` and hung
   exactly that far below the bar it is supposed to touch. One declaration, and
   it fixes every present and future consumer rather than the one found. */
.cx-focus { --nav-h: 52px; }
.cx-focus .cx-nav { height: 52px; }
.cx-focus .cx-nav__links { display: none; }
.cx-focus .cx-nav .cx-wordmark { display: none; }
.cx-focus.cx-has-nav { padding-top: 52px; }
@media (max-width: 768px) {
  /* On a phone the bottom bar keeps only Home and You — enough to get out,
     not enough to invite wandering mid-question. */
  .cx-focus .cx-nav-mobile__item { display: none; }
  .cx-focus .cx-nav-mobile__item[href="/dashboard"],
  .cx-focus .cx-nav-mobile__item[href="/profile"] { display: flex; }
}

/* MOBILE. The old rule simply hid every link at <=768px, which left no
   navigation at all on a phone — the device most students revise on. A bottom
   bar replaces it: same destinations, same icons, same active logic. */
.cx-nav-mobile { display: none; }
@media (max-width: 768px) {
  .cx-nav { padding: 0 var(--sp-5); }
  .cx-nav__links { display: none; }
  /* Profile lives in the bottom bar on a phone, so the top bar is the brand
     alone. Keeping the end cluster clipped "Profile & Settings" off the right
     edge — measured at a 390px viewport, not assumed. */
  .cx-nav__end { display: none; }

  .cx-nav-mobile {
    position: fixed; bottom: 0; left: 0; right: 0; z-index: 200;
    display: flex; align-items: stretch;
    background: var(--cream); border-top: var(--bw-hair) solid var(--rule);
    padding: 6px 0 4px;
  }
  .cx-nav-mobile__item {
    /* Six destinations have to fit the narrowest phone in use. Equal flex
       basis with min-width:0 divides whatever there is; the earlier
       min-width:56px summed past 390px and pushed Profile off the screen
       entirely — a destination you cannot reach is worse than a cramped one. */
    flex: 1 1 0; min-width: 0; overflow: hidden;
    /* 44px MINIMUM, MEASURED (Phase E #13). These were 78x41 — three pixels
       under the house rule, on every authenticated screen in the product, and
       nothing had ever measured them because the render harness cannot go
       below a 500px viewport. `min-height` rather than more padding: the
       column is centred, so the extra height distributes around the icon and
       label instead of pushing the label toward the edge. */
    min-height: 44px;
    display: flex; flex-direction: column; align-items: center;
    justify-content: center; gap: 3px;
    text-decoration: none; padding: 4px 2px;
    /* Inter at 9px under v2's ONE RULE. Two faces have stood here: 7.5px
       system-ui bold, then the display face when the bar went parchment. v2
       settles it — a nav label is functional, so it is the named sans, which is
       also the face that survives being this small. */
    font-family: var(--sans); font-size: 9px; font-weight: 500;
    letter-spacing: 0.06em; text-transform: uppercase; text-align: center;
    white-space: nowrap;
    color: var(--ink-3); transition: color var(--t-fast);
  }
  .cx-nav-mobile__item .cx-nav__icon { width: 17px; height: 17px; }
  /* THREE signals on the active tab, never colour alone: the accent rule, the
     darker label, and aria-current in the markup. */
  .cx-nav-mobile__item.is-active {
    color: var(--ink);
    box-shadow: inset 0 2px 0 var(--nav-accent);
  }
  /* THE BRAND IS A LINK, and on a phone it was the only one in the top bar —
     104x33, so the one control up there was the hardest to hit. Centring it
     in a 44px box changes nothing visually (the lockup is already vertically
     centred in a 72px bar) and makes the target the size the rule asks for. */
  .cx-nav__brand { min-height: 44px; }

  /* Keep the last screenful clear of the bar. Raised with the items: the bar
     is 6px + 44px + 4px = 54px now, and this held 62px against a 51px bar. */
  .cx-has-nav { padding-bottom: 66px; }
}

/* Push page content below the fixed nav. */
.cx-has-nav { padding-top: var(--nav-h); }

/* ── The bridge out of the old left sidebar ────────────────────────────────
   The legacy layer offsets .lc-main-content by `margin-left: 240px` to clear
   the .lc-nav-desktop SIDEBAR. A page carrying the top spine has no sidebar,
   and the leftover margin pushes its whole content column 120px off centre —
   measured on student_session, where the question card sat at x=500 in a 1440
   viewport while the card above it started at 288. Keyed on .cx-has-nav so a
   surface gets the right layout the moment it adopts the spine.

   STATUS: no page renders .lc-nav-desktop any more, and the only page wearing
   .lc-main-content is student_session, which has .cx-has-nav — so this rule
   wins on specificity (two classes to one) regardless of source order, and the
   240px it cancels now reaches nothing. Both are kept for one more phase rather
   than removed here: Phase B replaces this nav layer wholesale, and unpicking a
   provably inert rule mid-migration is risk without a reader-visible gain. */
.cx-has-nav .lc-main-content { margin-left: 0; }

/* ── The one surface: the canvas cross-fade (cx_shell.js) ──────────────────
   The standard's destination transition — ~220ms, opacity only. The frame does
   not take part: the bar, the lockup and the accent underline are outside
   #cx-main and never repaint, which is the whole point of the shell.

   OPACITY AND NOTHING ELSE. No slide, no scale. A canvas that travels tells the
   student the two destinations are laid out side by side in some space, which
   is a claim the product does not otherwise make and cannot honour when they
   arrive by the back button.

   220ms here MUST equal FADE_MS in cx_shell.js — the script waits out the fade
   before swapping, so a stylesheet that disagreed would either swap mid-fade
   (a visible tear) or hold a blank canvas after the fade finished.
   tests/test_shell_spa.py reads both numbers and fails if they drift apart. */
#cx-main { transition: opacity 220ms ease; }
body.cx-shell-busy #cx-main { opacity: 0; }

/* The canvas takes focus after a swap so the change is announced; it is not an
   interactive control and must never draw a focus ring for having been moved
   to programmatically. Keyboard focus on real controls inside it is untouched. */
#cx-main:focus { outline: none; }

/* Motion is a preference, and this one is honoured on BOTH sides: the script
   skips its wait, and the transition is removed here, so the swap is instant
   rather than instant-but-still-transitioning. */
@media (prefers-reduced-motion: reduce) {
  #cx-main { transition: none; }
  body.cx-shell-busy #cx-main { opacity: 1; }
}

/* ── Accessibility preferences (Wave 2 CHUNK 6) ────────────────────────────
   These are SETTINGS THAT DO SOMETHING. A toggle that stores a flag and changes
   nothing on screen is worse than no toggle: a student who needs larger text
   turns it on, sees no difference, and concludes the product does not work for
   them.

   Applied as attributes on <html> by static/js/cx_prefs.js, which every page
   loads, so a preference set in settings holds on every surface including the
   session. Attributes on the root rather than a body class because the text-size
   scale has to reach the root font size, which is what rem units read.
   ───────────────────────────────────────────────────────────────────────── */

/* TEXT SIZE. Scales the root, so every rem-based size moves together and the
   layout reflows rather than text overflowing its container. Values are modest
   on purpose — this is a reading-comfort control, not a zoom substitute. */
html[data-cx-text="large"]  { font-size: 112.5%; }
html[data-cx-text="larger"] { font-size: 125%; }

/* HIGHER CONTRAST. Darkens the muted ink tiers and firms up every hairline.
   It does NOT switch to black-on-white: the parchment system is the product's
   identity, and the students who need more contrast should not be the only ones
   who lose it. --ink-3 and --rule are the two tokens that carry almost all of
   the product's low-contrast surface area. */
html[data-cx-contrast="high"] {
  --ink-2:  #2C2318;
  --ink-3:  #4A3F32;
  --ink-4:  #5C4E3E;
  --rule:   #9A8E80;
}

/* DYSLEXIA-FRIENDLY. The serif is the brand and it is also the hardest thing on
   the page for a dyslexic reader, so this swaps the body face for the sans,
   loosens tracking, and opens the line height. No webfont is fetched — a
   dyslexia setting that depends on a font download fails exactly when the
   network does. Maths stays in KaTeX either way. */
html[data-cx-dyslexia="on"] body,
html[data-cx-dyslexia="on"] .cx-body,
html[data-cx-dyslexia="on"] p,
html[data-cx-dyslexia="on"] li,
html[data-cx-dyslexia="on"] label,
html[data-cx-dyslexia="on"] input,
html[data-cx-dyslexia="on"] select,
html[data-cx-dyslexia="on"] textarea,
html[data-cx-dyslexia="on"] button {
  font-family: var(--sans);
  letter-spacing: 0.012em;
  word-spacing: 0.06em;
  line-height: 1.85;
}
/* Italic is a readability cost for dyslexic readers and the product uses it
   heavily for asides. Kept as a distinct weight instead of removing emphasis. */
html[data-cx-dyslexia="on"] em,
html[data-cx-dyslexia="on"] i { font-style: normal; font-weight: 600; }

/* REDUCED MOTION. Honours the OS setting by default AND an explicit in-app
   choice, because a student on a shared or school machine often cannot change
   the OS one. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}
html[data-cx-motion="reduced"] *,
html[data-cx-motion="reduced"] *::before,
html[data-cx-motion="reduced"] *::after {
  animation-duration: 0.001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
  scroll-behavior: auto !important;
}

/* ── Dividers & utility ───────────────────────────────────────────────────── */
.cx-divider { border: none; border-top: var(--bw-hair) solid var(--rule); margin: var(--sp-8) 0; }
.cx-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-6); }
.cx-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--sp-6); }
@media (max-width: 768px) { .cx-grid-2, .cx-grid-3 { grid-template-columns: 1fr; } }

/* ── Mode recognition ─────────────────────────────────────────────────────── */
/* Set ONE of these on <body>. It swaps --mode-accent, which the eyebrow, the
   active nav underline, focus rings, and any .cx-accent element read from — so
   the whole page quietly signals which mode the student is in, without a second
   theming system and without shouting in bright colour. */
/* One mode class sets all three tiers, so a component reads --mode-accent /
   --mode-rule / --mode-wash and never names a mode. That is what keeps "one
   mode colour on screen at a time" true by construction rather than by
   discipline: there is no way for a component to reach a mode it is not in. */
body.cx-mode-study      { --mode-accent: var(--mode-study);
                          --mode-rule: var(--mode-study-rule);
                          --mode-wash: var(--mode-study-wash); }
body.cx-mode-diagnostic { --mode-accent: var(--mode-diagnostic);
                          --mode-rule: var(--mode-diagnostic-rule);
                          --mode-wash: var(--mode-diagnostic-wash); }
body.cx-mode-drill      { --mode-accent: var(--mode-drill);
                          --mode-rule: var(--mode-drill-rule);
                          --mode-wash: var(--mode-drill-wash); }
body.cx-mode-exam       { --mode-accent: var(--mode-exam);
                          --mode-rule: var(--mode-exam-rule);
                          --mode-wash: var(--mode-exam-wash); }
/* Free Study is deliberately NEUTRAL — it is the mode with no agenda, and
   giving it an accent would imply one. Declared explicitly so its absence
   reads as a decision rather than an omission. */
body.cx-mode-free       { --mode-accent: var(--ink);
                          --mode-rule: var(--rule);
                          --mode-wash: var(--white); }
.cx-accent { color: var(--mode-accent); }
.cx-accent-bar { height: 2px; background: var(--mode-accent); }

/* ── The mode chooser (Phase C.5) ──────────────────────────────────────────
   THE ONE PLACE FOUR ACCENTS APPEAR AT ONCE, and it is deliberate rather than
   an exception carved out of the rule. "One mode colour on screen at a time"
   protects a student who is INSIDE a mode from a second mode's colour claiming
   to mean something. The hub is the screen where they are inside none of them
   and are being asked to choose, so the four accents are the content: each card
   teaches the colour it will wear for the rest of that mode.

   The card takes its accent from data-mode, NOT from an inline style and NOT
   from --mode-accent. Two reasons, both load-bearing:
     · body.cx-mode-* would give every card the same colour — the body can only
       be in one mode, which is exactly the invariant that makes the hub hard.
     · A page that wrote #2A6B40 into a style attribute has defined a colour,
       which the directive forbids product-wide. Here the mapping lives in the
       one sheet that is allowed to know a mode's colour, so dark mode reaches
       these cards for free (the tokens flip under [data-theme="dark"]).

   THE ACCENT'S FOUR PLACES, and no fifth: the 2px bar, the eyebrow, the focus
   ring, and — on the surfaces the card leads into — the active nav indicator.
   Not the border, not the background, not the button. The card's hover and
   selected states move INK and surface, never hue, so the accent keeps meaning
   "which mode is this" instead of drifting into "which card is under the
   cursor". */
/* RUN 44 — the mode card to the Elite Design Standard.
   THE ACCENT MOVED FROM THE TOP EDGE TO THE LEFT ONE, and shrank. It was a
   full-width 3px bar across the head of the card: a saturated band on a
   resting surface, which the standard forbids, and four of them side by side
   read as a colour-coded toolbar rather than four pieces of paper. A ≤4px LEFT
   border is the one wide edge the colour law allows — the same mark the Now
   card wears on Home, so the two mean the same thing product-wide.
   The eyebrow keeps the accent at full strength, because a LABEL is the other
   permitted job. */
.cx-mode-card {
  display: flex; flex-direction: column; align-items: stretch;
  background: var(--white); border: var(--bw-hair) solid var(--rule);
  border-left: var(--bw-edge) solid var(--card-accent, var(--ink-3));
  border-radius: var(--radius-card); padding: 0; cursor: pointer;
  /* INTER, not the serif. v2 names "mode labels" in the sans list beside
     eyebrows and chips — a mode card is a control you pick, not a hero word.
     Set on the container so the eyebrow, name and hint all inherit one face
     and cannot drift apart. */
  font-family: var(--sans); text-align: left; width: 100%;
  min-height: 44px;                      /* touch target — house rule */
  transition: border-color var(--t-fast), background var(--t-fast);
}
.cx-mode-card:hover { border-color: var(--ink-2); border-left-color: var(--card-accent, var(--ink-3)); background: var(--cream); }
/* The bar element stays in the markup on every page that renders one; it is
   collapsed rather than removed so no page has to change to lose it. */
.cx-mode-card__bar  { display: none; }
.cx-mode-card__body { padding: var(--sp-5) var(--sp-5) var(--sp-5); }
.cx-mode-card__eyebrow {
  display: block; font-family: var(--sans); font-size: 10px;
  font-weight: 600; letter-spacing: 1.6px; text-transform: uppercase;
  color: var(--card-accent, var(--ink-3)); margin-bottom: var(--sp-2);
}
/* 15px, not --fs-title's 21px. That token is the SERIF card-title size and
   reads a full step louder in Inter, which would put four competing headings
   in the mode grid. v2 gives this role its own line: Inter 500, 15px. */
.cx-mode-card__name {
  display: block; font-size: 15px; font-weight: 500;
  color: var(--ink); line-height: var(--lh-snug);
}
.cx-mode-card__what {
  display: block; font-size: var(--fs-small); font-style: italic;
  color: var(--ink-2); line-height: var(--lh-snug); margin-top: var(--sp-1);
}
.cx-mode-card:focus-visible {
  outline: 2px solid var(--card-accent, var(--mode-accent));
  outline-offset: 2px;
}
/* The mapping. Four lines, and the only place a mode card learns its colour. */
.cx-mode-card[data-mode="diagnostic"] { --card-accent: var(--mode-diagnostic); }
.cx-mode-card[data-mode="study"]      { --card-accent: var(--mode-study); }
.cx-mode-card[data-mode="drill"]      { --card-accent: var(--mode-drill); }
.cx-mode-card[data-mode="exam"]       { --card-accent: var(--mode-exam); }

/* ── The model explainer (Phase D #9, UI_BRIEF_copy §6) ────────────────────
   "Make the model legible." The product tells a student a mastery score and a
   predicted grade and, until now, never said what either one was. A number a
   student cannot interrogate is asking for trust it has not earned — and this
   product's whole claim is that its reading of them is honest.

   A <details>/<summary> pair, deliberately, rather than a JS popover: it is
   keyboard-operable, screen-reader-announced and Escape-dismissable for free,
   it needs no script on a page that may not have one, and it degrades to plain
   visible text if CSS fails. The marker is a "?" because that is the question
   the student is asking.

   Quiet by construction: no shadow, no animation beyond the native disclosure,
   and the panel is a hairline-bordered note in the same voice as everything
   else. */
.cx-explain { display: inline; position: relative; }
.cx-explain > summary {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; margin-left: 6px; vertical-align: middle;
  border: var(--bw-hair) solid var(--rule); border-radius: 50%;
  font-family: var(--sans); font-size: 10px; font-weight: 700;
  color: var(--ink-3); cursor: pointer; list-style: none;
  transition: border-color var(--t-fast), color var(--t-fast);
}
.cx-explain > summary::-webkit-details-marker { display: none; }
.cx-explain > summary:hover { border-color: var(--ink-2); color: var(--ink-2); }
.cx-explain > summary:focus-visible {
  outline: 2px solid var(--mode-accent); outline-offset: 2px;
}
.cx-explain__body {
  position: absolute; z-index: 60; left: 0; top: calc(100% + 8px);
  width: max(260px, 22em); max-width: 84vw;
  background: var(--white); border: var(--bw-hair) solid var(--rule);
  border-left: var(--bw-edge) solid var(--ink-4); border-radius: var(--radius);
  padding: var(--sp-4) var(--sp-4);
  font-family:var(--sans); font-size: var(--fs-small); font-style: normal;
  line-height: var(--lh-body); color: var(--ink-2); text-transform: none;
  letter-spacing: 0; font-weight: 400;
}
/* The question the panel answers. A heading, so it is a labelled element with
   its own weight rather than <b> — which the canon reserves for nothing at all
   in body copy, and which scripts/voice_canon_audit.py caught here on the first
   run of this very component. */
.cx-explain__q { display: block; color: var(--ink); font-weight: 500;
                 margin-bottom: var(--sp-2); }
@media (max-width: 520px) {
  /* On a phone an absolutely-positioned panel hanging off a label lands
     half-off the screen. It becomes a block note under the label instead. */
  .cx-explain__body { position: static; width: auto; max-width: none;
                      margin-top: var(--sp-2); }
}

.cx-mode-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-3); }
@media (max-width: 900px) { .cx-mode-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 520px) { .cx-mode-grid { grid-template-columns: 1fr; } }

/* ── --nav-accent: the accent as it reads ON THE DARK BAR ──────────────────
   `--mode-accent` defaults to --ink, and Free Study sets it to --ink on purpose.
   Both are correct on a parchment page and both are INVISIBLE on the nav, which
   is --ink itself — so on Home, Progress, Profile, Free Study and every other
   no-mode surface the active item's underline vanished and its active state
   rested on brightness alone. Measured on /session: accent #1A1410 on a
   #1A1410 bar.

   Colour is never the only signal, so the bar carries its own accent slot: a
   light neutral rule by default, replaced by the real mode colour on the five
   surfaces that have one. The active item then always shows an underline, plus a
   brighter label, plus aria-current, plus its icon. */
/* THE BAR IS CREAM NOW, and that changes this token's whole reason for
   existing. The paragraph above is kept because it is the record of why the
   slot is here at all, but its measurement — "accent #1A1410 on a #1A1410 bar"
   — describes a bar that no longer exists. On parchment the default accent can
   simply BE the accent: sage is 5.50:1 on cream, so the underline is visible
   on every no-mode surface without a special neutral. The four mode overrides
   below are unchanged and still win where a mode is set. */
:root, body.cx-mode-free { --nav-accent: var(--sage); }
body.cx-mode-study      { --nav-accent: var(--mode-study); }
body.cx-mode-diagnostic { --nav-accent: var(--mode-diagnostic); }
body.cx-mode-drill      { --nav-accent: var(--mode-drill); }
body.cx-mode-exam       { --nav-accent: var(--mode-exam); }

/* Fade-in reveal (opacity + small rise only — the one motion we allow). */
@media (prefers-reduced-motion: no-preference) {
  .cx-reveal { opacity: 0; transform: translateY(16px); transition: opacity var(--t-slow), transform var(--t-slow); }
  .cx-reveal.is-in { opacity: 1; transform: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   LEGACY .lc-* LAYER — ported from design-system/COMPONENTS.css, which is
   now deleted.

   WHY THIS EXISTS AND WHY IT IS NOT THE DESIGN SYSTEM
     COMPONENTS.css was the last competing component library. Four pages still
     consume it — dev_status, profile, skill_map, student_session — so it could
     not simply be removed. Every OTHER page was already on the cx- layer: the
     UI directive's premise that "the codex library renders only in
     style_showcase.html" was not true at 4468194, and is recorded as such in
     the Phase A report.

   WHAT WAS PORTED: exactly the 23 classes those four pages actually use, with
     their state and dark variants. The other ~18 classes in that file — the
     desktop sidebar (.lc-nav-desktop and its wordmark/item/path), .lc-page,
     .lc-card, .lc-h1/h2/h4, .lc-body, .lc-italic, .lc-divider, .lc-section,
     .lc-grid-2/3, .lc-textarea (referenced by one dark rule, worn by no
     element) and the .lc-home-* pre-login nav — had no remaining consumer and
     were dropped rather than carried forward.
     .lc-note and .lc-question-enter are referenced by student_session and were
     ALREADY undefined in COMPONENTS.css; they stay undefined here, because
     inventing styling for them now would be a render change disguised as a port.

   HOW "NO REMAINING CONSUMER" WAS ESTABLISHED, since getting it wrong is how a
     port breaks a screen: class="" attributes, className assignments and
     classList calls, across all four pages. Two scans in a row cleared the
     answer-reveal pair before a third caught them — see the note on them below.
     A class is only dropped when all three say nothing uses it.

   RENDER FIDELITY: this is byte-faithful, deliberately. All four pages linked
     COMPONENTS.css BEFORE codex-tokens.css, so the canonical palette already
     won every shared token name (--cream, --rule, --ink-3 …) and these rules
     already resolved to canonical values. The only COMPONENTS.css-exclusive
     colours were its selected-row wash and dot-done sepia, which moved to
     --lc-cream-dark / --lc-sepia at their shipped values. Its --space-* scale
     maps 1:1 onto the canonical --sp-*. Nothing here changes a pixel.

   THIS LAYER IS TRANSITIONAL. Phase B replaces the mobile nav, Phase C the
   learning surface. Each class deleted here is progress; none should gain a
   new consumer.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Typography ── */
.lc-h3    { font-size: 24px; font-weight: 400; line-height: 1.4; margin-bottom: var(--sp-3); }
.lc-small { font-size: 14px; line-height: 1.6; margin-bottom: var(--sp-2); }
.lc-label { font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; }
.lc-muted { color: var(--ink-3); }

/* ── Cards ── */
.lc-card-sm       { background: var(--cream); border: var(--bw-hair) solid var(--rule); border-radius: 2px; padding: 20px; }
.lc-card-question { max-width: 680px; margin: 0 auto; background: var(--cream);
                    border: var(--bw-hair) solid var(--rule); border-radius: 2px; padding: 30px 36px; }

/* ── Buttons ── */
.lc-btn {
  font-family:var(--sans); font-size: 16px; padding: 12px 28px;
  border-radius: 2px; cursor: pointer; transition: all 120ms ease;
  display: inline-block; text-decoration: none;
}
.lc-btn:active         { transform: scale(0.98); }
.lc-btn-primary        { background: var(--command); color: var(--on-command); border: none; }
.lc-btn-primary:hover  { background: var(--ink-2); }
.lc-btn-secondary      { background: transparent; color: var(--ink); border: var(--bw-hair) solid var(--ink); }
.lc-btn-ghost          { background: transparent; color: var(--ink-3); border: none; }

/* ── Form controls. .lc-select carried COMPONENTS.css's note that it is not a
   spec class but exists so profile.html's dropdowns are not native-unstyled. ── */
.lc-input, .lc-select {
  width: 100%; padding: 10px 14px; border: var(--bw-hair) solid var(--rule);
  background: var(--cream); font-family:var(--sans);
  font-size: 17px; border-radius: 2px; color: var(--ink);
}
.lc-input:focus, .lc-select:focus { border-color: var(--ink); outline: none; }

/* ── Answer option (student_session) ── */
/* A FLEX ROW as of C.6, so the key-letter ring, the value and the state mark
   sit on one baseline — the design brief's option object. It was a plain block,
   which put the (previously bare-letter) key and the value on separate lines the
   moment either became anything other than inline text. student_session.html is
   this class's only consumer, so the change reaches exactly one surface.
   The surface is --white, not --cream: an option is a card lifted off the page,
   and on the reveal the state wash replaces it. */
.lc-answer-option {
  display: flex; align-items: center;
  border: var(--bw-hair) solid var(--rule); background: var(--white); padding: 15px 18px;
  min-height: 48px; margin-bottom: 12px; border-radius: var(--radius-card);
  cursor: pointer;
  transition: border-color 80ms ease; font-family:var(--sans);
  font-size: 17px; width: 100%; text-align: left; color: var(--ink);
}
.lc-answer-option:hover    { border-color: var(--ink-3); }
.lc-answer-option.selected { border-color: var(--ink); background: var(--lc-cream-dark); }

/* The answer reveal — the single most important visual state in the product.
   These were nearly dropped as unused: they are never written in a class="",
   only assigned through `el.className = 'lc-answer-option ' + (ok ? ... : ...)`
   at five sites in student_session.html, so a scan of class attributes says
   they have no consumer.

   RESOLVED TO TOKENS IN PHASE C.6, which is what Phase A deferred to here.
   They used to be COMPONENTS.css's own hexes, kept verbatim through the
   migration so that commit changed no pixel:

       correct   border #4a7c59  background #f0f7f2
       incorrect border #8b3a3a  background #fdf2f2

   Four literals, none of them canonical (--correct is #1A4A28, considerably
   darker than #4a7c59), and — the reason this mattered more than tidiness —
   **none of them had a dark value.** Every other state in the product flips
   under [data-theme="dark"]; this one painted a near-white #f0f7f2 panel on an
   espresso page, on the single most-repeated screen in the product. The tokens
   below already carry their dark values (--correct-bg #1E2C22, --wrong-bg
   #2E1E1B, both contrast-measured in Phase B.0), so resolving the literals IS
   the dark-mode fix — there is no second edit.

   WHY --wrong AND NOT THE BRIEF'S --maroon. UI_BRIEF_design.html renders the
   incorrect option with its local `--maroon`. In THIS repo maroon is
   `--mode-drill`, the Drill mode's accent. Painting "this answer was wrong" in
   a mode colour would make that colour mean two different things on the same
   screen — and a student in Drill would see the mode's own colour used to mark
   them wrong. `--wrong` is the semantic token the design system already
   defines for exactly this, and it is the one used.

   Colour is never the only signal: the option also carries the key letter's
   ring, a "your answer" / "✓ correct" mark, and the border weight.

   ── RUN 44: SAGE AND SIENNA, AND THE SATURATION MOVED OFF THE PANEL ────────
   Two changes, both from the Elite Design Standard's colour law.

   FIRST, THE HUES. Correct was --correct (#1A4A28, a deep forest) and wrong
   was --wrong (#6B1818, oxblood). The standard reserves SAGE for the
   correct-answer outline and quarantines SIENNA to exam mode and the outline
   on an answer the student chose and got wrong. Oxblood is the colour of a
   mistake; sienna is the colour of a mark on a paper — and this is the screen
   where the difference is the whole product. The semantic tokens --correct and
   --wrong are untouched and still carry every other state in the app; only the
   OUTLINE on these two objects moves.

   SECOND, AND THIS IS THE BIGGER ONE: a 2px saturated border with a tinted
   panel behind it is a resting surface shouting. The standard forbids borders
   of 2px or more on a resting surface and caps a tint at a whisper. So the
   border becomes 1.5px of the SOFT tier (--sage-rule / --sienna-rule, both
   already defined as ~22% of the accent over the card), the correct option
   keeps a 5% wash and the chosen-wrong option gets NO wash at all, and the
   full-strength accent moves to the key ring and the mark — the two small
   elements that are meant to be read, not the whole panel.

   The chosen-wrong option losing its wash is deliberate: a student's own
   answer should not be highlighted more loudly than the right one. */
/* THE CORRECT OPTION LOSES ITS GREEN WASH (v2 law 1). It is now marked the same
   way its incorrect twin below already is — a coloured 1.5px boundary on the
   card surface — so the reveal is a pair of outlines rather than one filled
   block beside one outlined one.
   NOTHING IS CARRIED BY COLOUR ALONE, unchanged: .opt-mark writes the word
   beside the option, which is what the house rule requires and what makes
   dropping the fill safe here. */
.lc-answer-correct-reveal {
  border: 1.5px solid var(--sage) !important;
  background: var(--white) !important;
}
.lc-answer-incorrect-reveal {
  border: 1.5px solid var(--sienna-rule) !important;
  background: var(--white) !important;
}

/* ── Progress dots ── */
.lc-progress-dots { display: flex; gap: 8px; justify-content: center; margin-bottom: 16px; }
.lc-dot           { width: 8px; height: 8px; border-radius: 50%; background: var(--rule);
                    transition: background 200ms ease; }
.lc-dot.active    { background: var(--command); }
.lc-dot.done      { background: var(--lc-sepia); }

/* ── Break / fatigue toast (student_session) ── */
.lc-toast {
  position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
  background: var(--cream); border: var(--bw-hair) solid var(--rule); border-radius: 2px;
  padding: 20px 24px; max-width: 400px; width: 90%; text-align: center;
  box-shadow: 0 4px 16px rgba(26,20,16,0.12); z-index: 100;
}
.lc-toast p        { font-size: 14px; line-height: 1.6; color: var(--ink); margin: 0; }
.lc-toast-actions  { display: flex; gap: 12px; justify-content: center; margin-top: 16px; }

/* ── Mobile bottom nav. Phase B replaces this with the one shared tab bar;
   ported unchanged so dev_status and skill_map keep working until it does. ── */
.lc-nav-mobile {
  position: fixed; bottom: 0; left: 0; right: 0; height: 64px;
  background: var(--cream); border-top: var(--bw-hair) solid var(--rule);
  display: none; justify-content: space-around; align-items: center; z-index: 100;
}
.lc-nav-mobile-item {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  color: var(--ink-3); text-decoration: none; font-size: 11px;
  font-family:var(--sans); padding: 8px 16px;
}
.lc-nav-mobile-item.active { color: var(--ink); }
.lc-nav-mobile-item svg    { width: 20px; height: 20px; }

/* .lc-main-content's 240px offset cleared the .lc-nav-desktop sidebar, which no
   surviving page uses. The offset is kept ONLY below, under the same media
   query it always had, because student_session still carries the class. */
.lc-main-content { min-height: 100vh; padding: 48px; margin-left: 240px; }

@media (max-width: 768px) {
  .lc-nav-mobile   { display: flex; }
  .lc-main-content { margin-left: 0; padding: 24px 20px 80px; }
}
@media (min-width: 769px) { body.lc-has-mobile-nav { padding-left: 240px; } }
@media (max-width: 768px) { body.lc-has-mobile-nav { padding-bottom: 64px; } }

/* ── Dark preference, legacy layer only ──────────────────────────────────
   The `.lc-theme-dark` scoping this block carried through Phase A is GONE, and
   so are two of its rules. Dark mode is now a token override in
   codex-tokens.css, so `body` re-themes itself through --cream/--ink, and
   .lc-h3 (which sets no colour of its own) simply inherits. Both rules were
   restating what the cascade already does.

   What survives is only what the token override CANNOT reach: these components
   hardcode `background: var(--cream)`, which is the PAGE colour. In light that
   was a harmless quirk — cream cards on a cream page. In dark it would make
   every card, input and answer option vanish into the background. The rules
   below lift them onto the card surface. Correct behaviour would be for the
   components to use --white in the first place; that is Phase C's rebuild of
   this surface, not a change to make inside a theming commit. ── */
[data-theme="dark"] .lc-card-sm,
[data-theme="dark"] .lc-card-question,
[data-theme="dark"] .lc-nav-mobile      { background: var(--white); border-color: var(--rule); }
[data-theme="dark"] .lc-input,
[data-theme="dark"] .lc-select,
[data-theme="dark"] .lc-answer-option   { background: var(--white); color: var(--ink); border-color: var(--rule); }

/* ── KaTeX on dark ───────────────────────────────────────────────────────────
   KaTeX sets no colour on .katex, so rendered maths inherits from its container
   and follows --ink for free — that is why nothing here recolours the glyphs.
   What does NOT come free: KaTeX draws fraction bars, radical rules, overlines
   and \hline as BORDERS, and its own stylesheet gives several of them explicit
   currentColor-independent handling. Pinning the whole subtree to currentColor
   keeps a fraction bar the same colour as the numerator above it on either
   theme; without it a dark-mode fraction can render its bar in a stale dark
   value and read as a missing line. Costs nothing in light mode, where
   currentColor is already the ink. */
.katex, .katex * { border-color: currentColor; }

/* ── WIDE MATHEMATICS ON A 390-px PHONE (RUN 39) ────────────────────────────
   MEASURED, not guessed. `scripts/render_qa.py --only CUMFREQ-001
   --viewport 390x844` in headless Chrome, before this rule:

       .katex-display   width 254px, scrollWidth 515px, overflow-x: visible
       .q-preamble      width 300px, scrollWidth 537px
       window.innerWidth 583  ← the 390-px viewport, blown out by 193px

   CUMFREQ-001's stem IS a table — a `\begin{array}{|c|c|c|}` of five class
   intervals with their frequencies and cumulative frequencies. KaTeX renders
   an array as one unbreakable inline box, and `.katex-display` ships with
   `overflow: visible`, so the box did not clip and did not scroll: it pushed
   the DOCUMENT wider than the phone. Half the cumulative-frequency column sat
   off the right-hand edge, and the whole page — stem, parts, options, buttons
   — scrolled sideways underneath it. That is the failure mode the published
   Accessibility Statement's "works down to a 390-px phone" claim denies.

   A SCROLL CONTAINER, NOT A RESPONSIVE STACK. Restacking is the usual answer
   for a wide table and it is the wrong one here: this table is LaTeX, and a
   `\begin{array}` cannot be re-flowed into cards without destroying the
   column alignment that makes a cumulative-frequency table readable at all.
   The numbers have to stay in their columns. So the box keeps its shape and
   gains a horizontal scroll — the content becomes REACHABLE rather than
   truncated, and the page stops scrolling sideways.

   `overflow-y` is set EXPLICITLY. Per CSS, an axis left `visible` beside a
   non-visible one computes to `auto`, so `overflow-x: auto` alone would give
   tall display maths a vertical scrollbar it never asked for. `hidden` clips
   nothing that the padding does not already clear — integrals and stacked
   fractions were checked at this width, not assumed. */
.katex-display{overflow-x:auto;overflow-y:hidden;
  padding-top:3px;padding-bottom:3px;
  -webkit-overflow-scrolling:touch;scrollbar-width:thin}

/* ══ STATE PANELS — loading · empty · error (Phase E #11) ══════════════════
   ONE implementation, because thirteen was the defect.

   The driven audit (`scripts/drive_states.py`) found every data screen had
   rolled its own. The consequences were not cosmetic:
     * /dashboard's error panel read "Server not running — open a terminal and
       run: python session_server.py". That is a message to a developer,
       rendered to a student.
     * eight screens said "Please refresh the page" with nothing to press.
     * /report and /profile rendered a full page of zeros and dashes when the
       fetch FAILED — a failure shown as an honest empty account.

   The brief's rule for the error state is "plain, retry", so the retry is part
   of the component and not an optional extra a screen can forget. The border
   is a --wrong hairline, not a red fill: colour carries meaning here, and the
   house rule is that it never shouts. ── */
.cx-state {
  padding: var(--sp-6) var(--sp-5);
  border: var(--bw-hair) solid var(--rule);
  background: var(--white);
  text-align: center;
  margin-bottom: var(--sp-5);
}
.cx-state__title {
  font-family: var(--serif);
  font-size: var(--fs-title);
  font-weight: 400;
  color: var(--ink);
  margin: 0 0 var(--sp-2);
}
.cx-state__line {
  font-family:var(--sans);
  font-size: var(--fs-small);
  line-height: var(--lh-body);
  color: var(--ink-2);
  margin: 0 auto;
  max-width: 46ch;
}
/* The error variant. Meaning, at a hairline. */
.cx-state--error { border-color: var(--wrong); background: var(--wrong-bg); }
.cx-state--error .cx-state__title { color: var(--wrong); }

/* LOADING: one calm line, no spinner — the brief is explicit. The hairline
   shimmer is an opacity breath, 2.4s, which stays inside the "quiet motion"
   rule (opacity/position only, never blink/bounce/spin). */
.cx-state--loading { border-style: none; background: none; }
.cx-state--loading .cx-state__line {
  font-style: italic;
  color: var(--ink-3);
  animation: cx-breathe 2.4s ease-in-out infinite;
}
@keyframes cx-breathe { 0%, 100% { opacity: 1 } 50% { opacity: 0.55 } }
@media (prefers-reduced-motion: reduce) {
  .cx-state--loading .cx-state__line { animation: none; }
}

/* 44px minimum target, per the house accessibility rule. */
.cx-state__action {
  display: inline-block;
  margin-top: var(--sp-4);
  min-height: 44px;
  padding: 12px var(--sp-5);
  border: var(--bw-hair) solid var(--command);
  background: var(--command);
  color: var(--on-command);
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  border-radius: var(--radius);
}
.cx-state__action:hover { background: var(--command-2); border-color: var(--command-2); }

/* ══ TAP TARGETS ON A PHONE (Phase E #13) ═════════════════════════════════
   MEASURED, not assumed — `scripts/drive_mobile.py` at a TRUE 390px viewport
   (Emulation.setDeviceMetricsOverride; Chrome clamps --window-size to ~500px
   on Windows, which is why the render harness could never see any of this).
   Twenty-one distinct controls across nineteen screens came back under the
   house minimum of 44px, several of them badly: the concept-panel close was
   26x26, the session back button 28x32, the home footer links 15px tall.

   WHY THIS BLOCK IS HERE AND NOT IN NINETEEN FILES.
   44px is a house accessibility rule, not a page's opinion, and Phase A's
   whole purpose was to stop screens deciding these things individually. The
   selectors below are page-local class names, which is a smell — but the
   alternative is nineteen media queries that will drift apart, and the drift
   is the failure this directive exists to fix. When a screen is next
   rebuilt its control should take a `cx-` component and drop out of this list.

   Every rule is a MINIMUM, never a fixed size, so nothing that is already
   large enough changes. Inline elements become inline-flex and centre their
   own text, so the box grows without the label moving. ── */
@media (max-width: 768px) {
  /* Auth / marketing topbars — the brand lockup is a link to home. */
  .topbar-brand { min-height: 44px; }
  /* In focus mode the nav collapses to the mark alone: 26px wide, and it was
     the only control in the top bar. */
  .cx-nav__brand { min-width: 44px; }

  /* Buttons that were merely short. */
  .tree-topic-btn,
  .path-pill,
  .path-new-btn,
  .new-goal-btn,
  .action-btn,
  .nav-cta        { min-height: 44px; }

  .goal-btn       { min-height: 44px; min-width: 44px; }
  .toggle-btn     { min-width: 44px; }
  .back-btn       { min-height: 44px; min-width: 44px;
                    display: inline-flex; align-items: center;
                    justify-content: center; }

  /* Small text links. inline-flex keeps the type exactly where it was and
     grows the pressable box around it. min-width matters as much as height
     here: "Terms", "Cancel" and "Forgot?" reached 44px tall on the first pass
     and were still 36-43px WIDE, and a target is only as good as its smaller
     dimension. Centred, so the label stays put as the box grows. */
  .footer-link,
  .concept-review-link,
  .path-manage-name,
  .nav-link,
  .forgot,
  .back           { min-height: 44px; min-width: 44px;
                    display: inline-flex; align-items: center;
                    justify-content: center; }

  /* NOT LISTED HERE, ON PURPOSE: .path-pill, .path-new-btn and .cp-close.
     Each sets its own explicit size in its page's <style> block, which loads
     AFTER this sheet — so on equal specificity the page wins and a rule here
     would sit in the file looking effective and doing nothing. They were
     corrected at source (ui/dashboard.html, ui/student_session.html). */
}


/* ═══════════════════════════════════════════════════════════════════════════
   THE ELITE LAYER — `el-`
   Built to docs/design/design-reference.html and docs/design/onepage-app.html,
   the two founder-approved pixel targets (RUN 44, 2026-08-11). Open them in a
   browser next to any screen that uses these classes; if they disagree, the
   reference is right and the class is the bug.

   WHY A SECOND PREFIX AND NOT A REWRITE OF `cx-`. `cx-` is on 43 pages. A
   screen is migrated to the standard one at a time, and a half-migrated `cx-`
   card would change every page that had not been rebuilt yet — which is the
   "two systems live at once" failure the standard itself warns about, arrived
   at from the other direction. `el-` is additive: a page carries it only once
   it has actually been rebuilt, and `cx-` shrinks as the phases land. Both
   read the SAME tokens, so there is one palette and one spacing scale
   throughout. This is one system mid-migration, not two systems.

   THE SIX TYPE ROLES. Everything on an elite screen is one of these. There is
   no seventh, and adding one is how a design system dies.

     .el-eyebrow  Cinzel 500/600 · 10–11px · UPPERCASE · +1.6–2.4px tracking
     .el-eyebrow  Inter 600 · 10px — functional, so the sans face (v2)
     .el-title    EB Garamond 600 · 26–28px — the page title, one per screen
     .el-section  EB Garamond 500 · 21–23px
     .el-body     EB Garamond 400 · 16–17px / 1.55
     .el-focal    EB Garamond 500 italic · 21–33px — the human voice
     .el-meta     12.5–13.5px

   THE ONE RULE MOVED TWO OF THESE, RUN 67, AND IN OPPOSITE DIRECTIONS. v2 says
   serif for hero words and sans for everything functional, so .el-eyebrow and
   .el-btn leave Cinzel for Inter — and .el-title, which was Cinzel, becomes EB
   GARAMOND rather than Inter. A page title is the most hero word on a screen;
   the mechanical reading of "convert the display face to sans" would have set
   it in Inter and broken the rule while appearing to apply it.

   THREE WEIGHTS: 400, 500, 600. Never 700 — EB Garamond's heavier cuts read as
   shouting on parchment.

   THE TAUPE DEVIATION THIS BLOCK RECORDED IS SPENT. It said the reference's
   #9A8E7E measured 2.75:1 on cream and that --ink-3 would stand in as the taupe
   here. The founder ruled in RUN 67 that light --taupe is #877B6B — 3.77:1 on
   --bg — so the token is usable in its own right and no substitution is needed.
   --ink-3 is a SEPARATE tone in a four-tone scale, not a stand-in for taupe;
   static/css/codex-tokens.css carries the measurements and the ruling.
   ═══════════════════════════════════════════════════════════════════════════ */

.el-eyebrow {
  font-family: var(--sans); font-weight: 600;
  font-size: 10px; letter-spacing: 1.9px; text-transform: uppercase;
  color: var(--sage); display: block;
}
/* The quiet variant — a label that names a region rather than an action. */
.el-eyebrow--quiet { color: var(--ink-3); }

.el-title {
  font-family: var(--serif); font-weight: 600;
  font-size: 27px; line-height: 1.25; letter-spacing: 0.01em; color: var(--ink);
}

.el-section {
  font-family:var(--sans); font-weight: 500;
  font-size: 22px; line-height: 1.3; color: var(--ink);
}

.el-body {
  font-family:var(--sans); font-weight: 400;
  font-size: 16.5px; line-height: 1.55; color: var(--ink-2);
}

/* ITALIC IS THE HUMAN VOICE, and the standard allows it ONE job per screen.
   On Home that job is the reason a skill was chosen; on Progress it is the
   readiness sentence; in a question it is the encouragement above the stem.
   If two things on a screen are italic, one of them is wrong. */
.el-focal {
  font-family:var(--sans); font-weight: 500; font-style: italic;
  font-size: 21px; line-height: 1.4; color: var(--ink-2);
}
.el-focal--lg { font-size: 33px; line-height: 1.2; }

.el-meta {
  font-family:var(--sans); font-size: 13px; line-height: 1.5;
  color: var(--ink-3);
}

/* NUMERALS ARE SERIF. --mono is for a seed or a UID, never for a figure a
   student reads about themselves: a courier percentage on parchment is the
   single loudest cheap-tell in the product. tabular-nums keeps a changing
   number from shifting its neighbours. */
.el-num { font-family:var(--sans); font-variant-numeric: tabular-nums; }

/* ── Elevation, all three levels ────────────────────────────────────────────
   page   nothing
   card   1px hairline · --radius-card · NO shadow
   frame  --radius-frame · --shadow-frame · once per screen
   A border of 2px or more on a resting surface is forbidden; the ONLY wider
   edge permitted anywhere is the 4px sage left border of the Now card, and it
   is a semantic mark rather than a boundary. */
.el-card {
  background: var(--white); border: var(--bw-hair) solid var(--rule);
  border-radius: var(--radius-card); padding: 24px 26px;
}
.el-card--now { border-left: var(--bw-edge-focal) solid var(--sage); }
.el-card--roomy { padding: 40px 44px 34px; }

.el-frame {
  background: var(--cream); border: var(--bw-hair) solid var(--rule);
  border-radius: var(--radius-frame); box-shadow: var(--shadow-frame);
  overflow: hidden;
}

/* ── The one fill button ────────────────────────────────────────────────────
   INK IS THE ONLY FILL-BUTTON COLOUR. Not sage, not sienna, not a mode accent
   — a coloured primary button is the standard's forbidden list in one object.
   Its label is INTER caps under v2's ONE RULE (it was Cinzel), and the
   vocabulary is fixed by docs/STYLE_GUIDE.md:
   Begin / Continue / Submit / Review answers / End session. */
.el-btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 8px; min-height: 44px; padding: 13px 28px;
  background: var(--command); color: var(--on-command);
  font-family: var(--sans); font-weight: 600;
  font-size: 11.5px; letter-spacing: 1.6px; text-transform: uppercase;
  border: none; border-radius: var(--radius-btn); cursor: pointer;
  text-decoration: none; transition: opacity var(--t-fast);
}
.el-btn:hover { opacity: 0.88; }

/* The quiet secondary strip: Inter caps in taupe (Cinzel until v2), no boxes,
   no chips. It is a list of places, set small enough that it cannot compete
   with the one action above it. */
.el-quiet {
  display: flex; flex-wrap: wrap; gap: 26px;
  font-family: var(--sans); font-weight: 500;
  font-size: 11px; letter-spacing: 1.4px; text-transform: uppercase;
}
.el-quiet a {
  color: var(--ink-3); text-decoration: none;
  min-height: 44px; display: inline-flex; align-items: center;
  transition: color var(--t-fast);
}
.el-quiet a:hover { color: var(--ink); }

/* ── The reading column ─────────────────────────────────────────────────────
   ≤560px, and the focal element gets 2–3x the air of any secondary pair. The
   spacing scale is the token ladder (4·8·12·16·24·32·40·48·64); these two
   helpers exist so a screen does not have to reach for a literal. */
.el-col { max-width: 560px; }
.el-col--wide { max-width: 600px; }
.el-air   { margin-bottom: var(--sp-8); }   /* around the focal element */
.el-air-s { margin-bottom: var(--sp-4); }   /* between a secondary pair */
