/* =================================================================
   ENGNOVA — Learner surface kit (LC-1 brand pass; design freeze
   2026-07-04). Per-surface CSS on top of the shared token primitives,
   mirroring how handoff-v2 ui_kits/app/app.css layers on the system
   ("Kit-specific CSS stays per-kit by design" — styles.css).

   Canonical anchors (docs/design-reference/handoff-v2/):
   - app kit quiz screen: quiet white hairline options → tint feedback
     (#EAFFF2/#14C262/#0A7A3E correct · #FFF0F1/#FF4757/#C2273A wrong,
     shake 360ms) · yellow sentence strip w/ 2px black border = the ONE
     loud element on a question screen
   - landing v3 Signature hero: A/B/C letter chips (yellow, 1.5px black)
   - voice kit listening: purple circular sticker play button
   - app kit result screen: space celebration + glass stats (C2 standing
     exception: glass ONLY on space surfaces) + glow CTA
   - onboarding goals: selection = #F3EEFF tint + 2px purple border
   Quiet borders: var(--hairline) per ruling C3 (never the kits'
   hardcoded #E4E4E0 drift).
   ================================================================= */

/* ---------- QUIZ OPTIONS (multiple choice) ---------- */
.lrn-opt {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 52px;                      /* ≥44px touch target */
  padding: 12px 16px;
  background: var(--eng-white);
  border: 1.5px solid var(--hairline);
  border-radius: 16px;
  cursor: pointer;
  font-size: var(--fs-body-lg);
  transition: border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.lrn-opt:hover { border-color: var(--eng-ink); }
/* Keyboard path: the radio is visually hidden but focusable — surface its
   focus on the pill (3px purple ring per system focus-visible language). */
.lrn-opt:focus-within {
  outline: 3px solid var(--eng-purple);
  outline-offset: 2px;
}
.lrn-opt__radio {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.lrn-opt__key {
  flex: none;
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Quiet default — a quiet lavender wash, NOT yellow: 4 repeated yellow badges
     violated "yellow = micro-accent, never repeated/large". The answered-state
     colors (purple/green/red) take over via the is-* classes below. */
  background: var(--eng-purple-soft);
  border: 1.5px solid color-mix(in srgb, var(--eng-purple) 30%, transparent);
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 14px;
  color: var(--eng-purple);
}
.lrn-opt__text { flex: 1; overflow-wrap: anywhere; }

/* Selected (pre-submit) — onboarding goal-card selection language */
.lrn-opt.is-sel {
  background: var(--eng-purple-soft);
  border: 2px solid var(--eng-purple);
}
.lrn-opt.is-sel .lrn-opt__key {
  background: var(--eng-purple);
  border-color: var(--eng-purple);
  color: var(--eng-white);
}
/* Correct / wrong (post-submit) — app-kit tint ramps, never solid fills. The
   badge is a small solid accent (a true micro-moment) that mirrors the state. */
.lrn-opt.is-ok {
  background: var(--eng-correct-tint);
  border: 2px solid var(--eng-green-deep);
  color: var(--eng-correct-text);
}
.lrn-opt.is-ok .lrn-opt__key {
  background: var(--eng-green);
  border-color: var(--eng-green-deep);
  color: var(--eng-ink);
}
.lrn-opt.is-no {
  background: var(--eng-wrong-tint);
  border: 2px solid var(--eng-red);
  color: var(--eng-wrong-text);
}
.lrn-opt.is-no .lrn-opt__key {
  background: var(--eng-red);
  border-color: var(--eng-red);
  /* audit:contrast — white on --eng-red is 3.34:1; ink clears AA and
     mirrors the is-ok badge (ink on green). */
  color: var(--eng-ink);
}
.lrn-opt.is-dim { opacity: 0.45; }        /* non-chosen options after submit */
.lrn-opt.is-locked { cursor: default; }
.lrn-opt.is-locked:hover { border-color: var(--hairline); }
.lrn-opt.is-sel.is-locked:hover { border-color: var(--eng-purple); }
.lrn-opt.is-ok:hover  { border-color: var(--eng-green-deep); }
.lrn-opt.is-no:hover  { border-color: var(--eng-red); }

/* ---------- SENTENCE STRIP (text-question stimulus) ----------
   The ONE loud element on a text-question screen (app kit). */
.lrn-sentence {
  display: block;
  background: var(--eng-yellow);
  border: 2px solid var(--eng-ink);
  border-radius: 14px;
  padding: 14px 18px;
  font-weight: 600;
  line-height: var(--lh-loose);          /* Thai body needs air */
  overflow-wrap: anywhere;               /* long English tokens never overflow */
}

/* ── FD structured stem (UAT round 1, 2026-09-02) ──────────────────────
   The stem's fenced document block renders as a real DOCUMENT panel — a
   handout pinned to the console — instead of a flattened run-on line.
   Quiet English instruction above (.lrn-stem-intro; the Thai chip is the
   real instruction), the document in its own bordered panel with a mono
   header row + real bullet list, the QUESTION keeps the loud sentence
   strip below. Content renders verbatim; only the SHAPE is styled. */
.lrn-stem-intro {
  font-size: 0.95rem;                     /* UAT-2 legibility */
  color: rgba(255, 255, 255, 0.8);
  margin: 0 0 10px;
}
.lrn-doc {
  text-align: left;
  border: 1px solid var(--hairline-dark);
  border-left: 3px solid color-mix(in srgb, var(--eng-purple) 55%, transparent);
  background: rgba(255, 255, 255, 0.05);
  border-radius: var(--radius-lg);
  padding: 14px 16px 10px;
  margin: 0 0 14px;
}
.lrn-doc__head {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: rgba(255, 255, 255, 0.88);
  padding-bottom: 8px;
  margin: 0 0 10px;
  border-bottom: 1px dashed var(--hairline-dark);
}
.lrn-doc p {
  font-size: 0.95rem;
  line-height: 1.55;
  white-space: pre-line;                  /* D2 step 3: document lines stay lines */
  color: rgba(255, 255, 255, 0.92);
  margin: 0 0 8px;
}
.lrn-doc ul { list-style: none; margin: 0 0 8px; padding: 0; }
.lrn-doc li {
  position: relative;
  padding-left: 18px;
  font-size: 0.95rem;
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.92);
  margin-bottom: 6px;
}
.lrn-doc li::before {
  content: "▸";
  position: absolute;
  left: 2px;
  color: rgba(255, 255, 255, 0.5);
}

/* the document rail + skill chip take the SKILL's hue when the arena knows
   it (--skill-hue set per question from the server-owned identity map);
   decorative accents only — never text color (contrast stays token-owned) */
.lrn-doc { border-left-color: color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 55%, transparent); }
.lrn-skill-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border-color: color-mix(in srgb, var(--skill-hue, var(--eng-green)) 55%, transparent);
}
.lrn-skill-tag__craft { width: 18px; height: 18px; object-fit: contain; }

/* ── the mission craft band (brief card 0; UAT round 1) ─────────────────
   "ภารกิจนี้คุณขับยานการอ่าน" — the five-craft fleet story inside the
   lesson itself. The craft raster sits in a hue ring; name + payoff beside. */
.msn-craft {
  display: flex;
  align-items: center;
  gap: 14px;
  text-align: left;
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-xl);
  background:
    radial-gradient(120px 60px at 16% 50%, color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 16%, transparent), transparent 70%),
    rgba(255, 255, 255, 0.04);
  padding: 12px 16px;
  margin: 0 0 16px;
}
.msn-craft__art { flex: none; width: 72px; height: 72px; }
.msn-craft__art img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 0 14px color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 45%, transparent));
}
.msn-craft__label {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: rgba(255, 255, 255, 0.7);
  margin: 0 0 3px;
}
.msn-craft__payoff {
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--eng-white);
  margin: 0;
}
/* card-0 level chip: hue TINT on the dark surface (the base eng-tag is a
   solid yellow field; yellow is a micro-accent only). */
.msn-tag {
  background: color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 18%, transparent);
  border-color: color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 55%, transparent);
  color: var(--eng-white);
}
/* "why this lesson" folded into the craft band (was its own glass box) */
.msn-why {
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.82);
  margin: 6px 0 0;
  line-height: 1.5;
}
.msn-why__label { color: rgba(255, 255, 255, 0.62); }
.msn-why--solo { text-align: center; margin: 0 0 14px; }
@media (max-width: 480px) {
  .msn-craft { gap: 10px; padding: 10px 12px; }
  .msn-craft__art { width: 56px; height: 56px; }
}

/* ---------- AUDIO PLAYER (listening questions) ----------
   Voice-kit listening language: purple circular sticker play button =
   the ONE loud element on a listening-question screen; quiet card body. */
.lrn-audio {
  display: flex;
  align-items: center;
  gap: 14px;
  background: var(--eng-white);
  border: 1.5px solid var(--hairline);
  border-radius: 16px;
  padding: 12px 16px;
  margin: 10px 0;
}
.lrn-audio__btn {
  flex: none;
  width: 56px;
  height: 56px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--eng-purple);
  color: var(--eng-white);
  border: 2px solid var(--eng-ink);
  border-radius: var(--radius-pill);
  box-shadow: 3px 3px 0 0 var(--eng-ink);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring),
              box-shadow var(--dur-fast) var(--ease-spring);
}
.lrn-audio__btn:hover  { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 0 var(--eng-ink); }
.lrn-audio__btn:active { transform: translate(2px, 2px);  box-shadow: 1px 1px 0 0 var(--eng-ink); }
.lrn-audio__btn:focus-visible { outline: 3px solid var(--eng-purple); outline-offset: 3px; }
.lrn-audio__btn svg { width: 22px; height: 22px; display: block; }
.lrn-audio__body { flex: 1; min-width: 0; }
.lrn-audio__track {
  position: relative;
  height: 7px;
  background: #EFEDF6;
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.lrn-audio__fill {
  height: 100%;
  width: 0%;
  background: var(--eng-purple);
  border-radius: var(--radius-pill);
}
.lrn-audio__time {
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--eng-slate);
  margin-top: 6px;
  display: block;
}

/* ---------- MOOD CHIPS (idle screen) ----------
   Quiet selection chips (goal-card family — deliberate emoji allowed per
   DESIGN_RULES; selection = purple tint + purple border). aria-pressed
   drives the selected style — no JS class juggling. */
.lrn-mood-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;                      /* touch target */
  padding: 9px 16px;
  background: var(--eng-white);
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-size: var(--fs-body-sm);
  color: var(--eng-ink);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.lrn-mood-chip:hover { border-color: var(--eng-ink); }
.lrn-mood-chip:focus-visible { outline: 3px solid var(--eng-purple); outline-offset: 2px; }
.lrn-mood-chip[aria-pressed="true"] {
  background: var(--eng-purple-soft);
  border: 2px solid var(--eng-purple);
  font-weight: 700;
}

/* Mood explainer fold (p1walk@ UAT 2026-09-05; round-3 layout rework) —
   zero-JS <details> under the chips. The open state is a quiet PANEL CARD
   (hairline border on the dark arena, left-aligned) with a name→meaning
   grid — the first version's centered run-on paragraph read as unstyled.
   Default disclosure triangle hidden (the ⓘ in the summary IS the marker).
   Ink guard authored WITH the block (the INK TRAP class): the token sheet
   paints bare <p> dark; on the dark arena these inherit the light ink. */
.lrn-mood-info { margin: 14px auto 0; max-width: 420px; font-size: var(--fs-body-sm); }
.lrn-mood-info summary {
  cursor: pointer;
  list-style: none;
  text-align: center;
  color: rgba(255, 255, 255, 0.62);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.lrn-mood-info summary::-webkit-details-marker { display: none; }
.lrn-mood-info summary:hover,
.lrn-mood-info[open] summary { color: var(--eng-white); }
.lrn-mood-info__panel {
  margin-top: 10px;
  padding: 14px 16px;
  text-align: left;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 12px;
}
.lrn-mood-info__panel p { color: rgba(255, 255, 255, 0.78); margin: 0; }
.lrn-mood-info__list { list-style: none; margin: 12px 0; padding: 0; display: grid; gap: 8px; }
.lrn-mood-info__list li { display: grid; grid-template-columns: 7.5rem 1fr; gap: 10px; align-items: baseline; }
.lrn-mood-info__list b { color: var(--eng-white); font-weight: 600; white-space: nowrap; }
.lrn-mood-info__list span { color: rgba(255, 255, 255, 0.78); }
.lrn-mood-info__note { border-top: 1px solid rgba(255, 255, 255, 0.10); padding-top: 10px; }
/* Narrow phones: the 7.5rem name column would squeeze the meaning — stack. */
@media (max-width: 400px) {
  .lrn-mood-info__list li { grid-template-columns: 1fr; gap: 2px; }
}

/* ---------- CELEBRATION (session complete) ----------
   App-kit result screen: space surface + glow display + glass stats
   (C2 standing exception: glass ONLY on space surfaces) + glow CTA. */
.lrn-celebrate {
  border-radius: var(--radius-xl);
  border: 1px solid var(--hairline-dark);
  padding: clamp(28px, 6vw, 56px) 20px;
  color: var(--eng-white);
}
.lrn-stats-glass {
  display: inline-flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(20px, 5vw, 44px);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--hairline-dark);
  border-radius: 20px;
  padding: 20px 28px;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.lrn-stats-glass b {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--fs-h1);
  color: var(--eng-green);
}
.lrn-stats-glass span { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.70); }

/* ---------- FORM ERROR CHIP (login + quiz) ----------
   MOVED to design-tokens/components.css (CL-LEADS 2026-07-17): four admin
   surfaces (leads / payments / magic-links / lead-detail) use the chip but
   never load this sheet — it rendered unstyled there. One definition, one
   home; the [hidden] guard below stays (harmless double-guard). */

/* ---------- SESSION PROGRESS ----------
   Kit quiz screens run their progress bars PURPLE (green is the XP bar's
   color); scoped variant over the shared .eng-progress primitive. */
.lrn-progress--quiz .eng-progress__fill { background: var(--eng-purple); }

/* ---------- FIELD-LEVEL VALIDATION (LC-2 signup) ----------
   Per-field inline error line (wrong-state TEXT color only — the tint chip
   stays reserved for form-level errors per V2 rule 5 restraint) + the quiet
   hint line + the invalid-input border state. */
.lrn-field-error {
  margin-top: 6px;
  font-size: var(--fs-body-sm);
  color: var(--eng-wrong-text);
  overflow-wrap: anywhere;
}
.lrn-hint {
  margin-top: 6px;
  font-size: var(--fs-body-sm);
  color: var(--eng-ink);
  opacity: 0.55;
}
.eng-input.lrn-invalid { border-color: var(--eng-red); }

/* ---------- DASHBOARD (LC-3 learner home) ----------
   handoff-v2 app-kit HomeScreen: TopBar (avatar + greeting + streak/XP
   pills) → the ONE loud green resume CTA card → quiet plan card. The kit's
   app-* classes are kit-local (ui_kits/app/app.css); translated here to
   .lrn-dash-* over the .eng-card + hairline + token system (LC-1/LC-2
   pattern — never reference app-top/app-avatar/app-stat directly). */
.lrn-dash__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 14px;
}
.lrn-dash__id { display: flex; align-items: center; gap: 12px; min-width: 0; }
.lrn-dash__avatar {
  flex: none;
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--eng-purple);
  color: var(--eng-white);
  border: 2px solid var(--eng-ink);
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 18px;
}
.lrn-dash__hi {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-body-lg);
  color: var(--eng-ink);
  overflow-wrap: anywhere;
}
.lrn-dash__sub { font-size: var(--fs-body-sm); color: var(--eng-ink); opacity: 0.6; margin-top: 2px; }
.lrn-dash__stats { display: flex; gap: 8px; flex: none; }

/* Stat pills — white hairline chips (README:73). Streak = flame; XP = green
   star. The glyphs are inline SVG (fill: currentColor) so the pill's `color`
   drives them — no icon-font dependency (CSP-clean). */
.lrn-stat {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  background: var(--eng-white);
  border: 1.5px solid var(--hairline);
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-body-sm);
  color: var(--eng-ink);
  white-space: nowrap;
}
.lrn-stat svg { width: 16px; height: 16px; display: block; flex: none; }
.lrn-stat--streak svg { color: var(--eng-flame); }
/* Gold XP star — brand gold #F2C94C (Premium accent); NOT green, which V2
   reserves for action/correct-only (taste-gate polish 3a). */
.lrn-stat--xp svg { color: #F2C94C; }

/* Plan card name — payment-kit "Manage" card language (quiet: the resume CTA
   owns the ONE loud slot; the plan card never carries a loud fill). */
.lrn-plan__name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-body-lg);
  color: var(--eng-ink);
  overflow-wrap: anywhere;
}

/* ---------- NOTICE CHIP (login post-verify / post-reset landing) ----------
   Server-rendered banner on /login (?verified / ?reset — LC-3 rider). ok =
   correct tint (V2 green ramp), warn = wrong tint (V2 rule 5: red as tint
   only). Rendered only when present → no [hidden] toggling. */
.lrn-notice {
  display: block;
  border-radius: 12px;
  padding: 10px 14px;
  font-size: var(--fs-body-sm);
  overflow-wrap: anywhere;
}
.lrn-notice--ok {
  background: var(--eng-correct-tint);
  color: var(--eng-correct-text);
  border: 1.5px solid var(--eng-green-deep);
}
.lrn-notice--warn {
  background: var(--eng-wrong-tint);
  color: var(--eng-wrong-text);
  border: 1.5px solid var(--eng-red);
}
/* Neutral informational notice (B1): brand-primary purple, distinct from
   error (red) + success (green) — a "handled gracefully" heads-up, not a
   warning. Used for the MediaRecorder-unsupported auto-skip on placement. */
.lrn-notice--info {
  background: var(--eng-purple-soft);
  color: var(--eng-purple-deep);
  border: 1.5px solid var(--eng-purple);
}
/* Caution notice (D-1): a "heads-up that affects your result" — distinct from
   error (red) + info (purple). Yellow is the brand caution accent, but yellow
   TEXT on light is unreadable (D-1 result.ejs bug) — so this is dark ink on a
   soft yellow-TINT chip, derived from the yellow token via color-mix (zero new
   hex per the LC-4 discipline). Used for the placement speaking-skipped notice. */
.lrn-notice--caution {
  background: color-mix(in srgb, var(--eng-yellow) 18%, white);
  color: var(--eng-ink);
  border: 1.5px solid color-mix(in srgb, var(--eng-yellow) 60%, var(--eng-ink));
}

/* ---------- Placement speaking per-word feedback (CL-COPY, rebaseline R4) ----------
   Word chips on the LIGHT placement card (var(--eng-white) bg). Status colors
   reuse the quiz answered-state token pairs (colors_and_type.css:295-298 —
   AA-verified ink-on-tint pairs) + the brand yellow micro-highlight with
   --fg-on-yellow ink. Tokens only — zero new hex. */
.plc-words {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: center;
}
.plc-word {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-weight: 600;
  font-size: 0.9rem;
  border: 1.5px solid transparent;
}
.plc-word--exact {
  background: var(--eng-correct-tint);
  color: var(--eng-correct-text);
  border-color: var(--eng-green-deep);
}
.plc-word--close {
  background: color-mix(in srgb, var(--eng-yellow) 45%, white);
  color: var(--eng-ink);
  border-color: var(--eng-yellow-deep);
}
.plc-word--miss {
  background: var(--eng-wrong-tint);
  color: var(--eng-wrong-text);
  border-color: var(--eng-red);
}
.plc-legend {
  display: flex;
  gap: 8px;
  justify-content: center;
}
#speaking-feedback {
  text-align: center;
}

/* ---------- [hidden] guards ----------
   Classes above set `display`, which would defeat the UA's [hidden] rule
   (equal specificity, later source order). State toggling in login.js /
   quiz.js / signup.js relies on the hidden attribute — make it always win
   here. (.lrn-field-error sets no display, but the guard is cheap insurance
   against future edits — the LC-1 error-chip leak class.) */
.lrn-error-chip[hidden],
.lrn-notice[hidden],
.lrn-field-error[hidden],
.lrn-audio__btn[hidden],
.lrn-audio__body[hidden],
#speaking-feedback[hidden],
#record-controls[hidden],
#restart-btn[hidden] {
  /* #record-controls carries Tailwind .flex (display:flex beats the UA
     [hidden] rule) — same leak class as the LC-1 error chip. #restart-btn
     is an .eng-btn (display:inline-flex) hidden by quiz.js on a completed
     mission (CL-MISSION-2) — same class. */
  display: none;
}

/* =================================================================
   DELIGHT D-2 — quiz "practice arena" (dark-space). 2026-07-05.
   The /quiz surface is a full-bleed .eng-bg-space arena (the shipped
   star-field + purple-nebula primitive — zero new hex). Dark variants
   of the quiz primitives scope under .lrn-arena so the light-surface
   defaults (placement, login, dashboard) are untouched. Surfaces use
   white-channel rgba; glows use brand-channel rgba (--eng-purple
   99,53,240 / --eng-green 57,255,137) — the convention the token file
   uses. Motion rides the --dur- / --ease- tokens (inherits [data-mood]
   scaling + the prefers-reduced-motion guard below). NOTE: never write
   a star-slash pair inside this comment — it terminates the comment
   early and the parser then eats the NEXT rule block (this silently
   killed .lrn-appbar's flex/height until the P-1 polish round).
   ================================================================= */

/* ---- learner app-bar (DX-1 seed): thin dark chrome above the arena; replaces
   the light marketing nav on the /quiz surface (in-app chrome ≠ marketing). ---- */
.lrn-appbar {
  display: flex;
  align-items: center;
  justify-content: space-between;        /* brand left · way-out link right (P-1 polish) */
  height: 58px;                          /* arena min-height offsets this exactly */
  padding: 0 clamp(16px, 4vw, 32px);
  background: var(--eng-space);
  border-bottom: 1px solid var(--hairline-dark);
}
.lrn-appbar__brand { display: block; line-height: 0; }
.lrn-appbar__brand img { height: 32px; width: auto; display: block; }
/* Persistent way out on standalone flow pages (หน้าหลัก / เข้าสู่ระบบ) —
   quiet hairline pill, light text (WCAG AA on --eng-space). */
.lrn-appbar__link {
  font-size: var(--fs-body-sm);
  font-weight: 600;
  color: rgba(255, 255, 255, 0.85);
  text-decoration: none;
  padding: 7px 14px;
  border: 1.5px solid var(--hairline-dark);
  border-radius: var(--radius-pill);
  transition: color var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.lrn-appbar__link:hover { color: var(--eng-white); border-color: rgba(255, 255, 255, 0.35); }

/* ── App-bar bell (UAT round 2) — unread-from-coach, links to /messages.
   The badge is the flame channel (--eng-flame): a signal, not an action —
   green stays reserved for CTAs. No badge = nothing unread (or the count
   failed; the middleware logs that case). */
.lrn-appbar__right { display: inline-flex; align-items: center; gap: 10px; }
.lrn-appbar__bell {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  color: rgba(255, 255, 255, 0.8);
  border: 1.5px solid var(--hairline-dark);
  border-radius: 50%;
  transition: color var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.lrn-appbar__bell:hover { color: var(--eng-white); border-color: rgba(255, 255, 255, 0.35); }
.lrn-appbar__bell-n {
  position: absolute; top: -5px; right: -6px;
  min-width: 17px; height: 17px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;
  color: var(--eng-space);                 /* dark ink on flame ≈7:1 (white would be ~2.6:1) */
  background: var(--eng-flame);
  border-radius: 999px;
  box-shadow: 0 0 8px color-mix(in srgb, var(--eng-flame) 55%, transparent);
}

.lrn-arena {
  position: relative;
  min-height: calc(100vh - 58px);        /* fill below the thin dark app-bar */
  display: flex;
  flex-direction: column;
  justify-content: flex-start;           /* D-2 refine: upper-⅔ bias — content
                                            anchored high w/ arena floor below,
                                            not floating dead-center in a void
                                            (also never clips tall content) */
  color: var(--eng-white);
  padding: clamp(28px, 8vh, 88px) 20px clamp(48px, 12vh, 140px);
  overflow: hidden;
}
.lrn-arena__inner { width: 100%; position: relative; z-index: 1; }

/* Mascot — transparent RGBA hero (DX-3 regen via gpt-image-1; gpt-image-2 can't
   do transparent). Idle hero waves; the celebrating variant lands on the
   complete screen + level-up overlay + the correct-answer cheer. */
.lrn-arena__hero-mascot {
  max-width: 168px; height: auto; display: block; margin: 0 auto 12px;
  filter: drop-shadow(0 0 30px rgba(57, 255, 137, 0.28));   /* green persona aura */
}
.lrn-arena__complete-mascot {
  max-width: 132px; height: auto; display: block; margin: 0 auto 10px;
  filter: drop-shadow(0 0 32px rgba(57, 255, 137, 0.32));
}
.lrn-arena__state-img {
  max-width: 240px; height: auto; display: block; margin: 0 auto 16px;
  border-radius: 16px;
}

/* Question card — glass-on-space (C2 exception) + soft purple glow: the
   card sits ON the dark surface, never a white void. */
.lrn-arena .lrn-qcard {
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-xl);
  padding: clamp(20px, 4vw, 36px);
  box-shadow: 0 0 48px rgba(99, 53, 240, 0.20);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

/* 🎯 skill-target chip — "targets YOUR weakness" (green = on-target) */
.lrn-arena .lrn-skill-tag {
  background: color-mix(in srgb, var(--eng-green) 14%, transparent);
  color: var(--eng-green);
  border-color: color-mix(in srgb, var(--eng-green) 45%, transparent);
}

/* Question stem (D-2 refine) — prominent light text on the glass card, NOT the
   full-yellow strip (which rendered white-on-yellow = unreadable on the arena).
   Yellow stays a MICRO-accent — it lives on the A/B/C option keys only. */
.lrn-arena .lrn-sentence {
  background: transparent;
  border: none;
  border-radius: 0;
  padding: 0;
  color: var(--eng-white);
  font-size: var(--fs-h3);               /* matches the listening stems */
}
/* q-number chip — ghosted purple to match the ghosted green skill chip (a bright
   light chip clashes on the dark card). Light-purple text = WCAG AA on space. */
.lrn-arena .eng-tag--purple {
  background: color-mix(in srgb, var(--eng-purple) 22%, transparent);
  color: var(--eng-purple-soft);
  border-color: color-mix(in srgb, var(--eng-purple) 50%, transparent);
}
/* fill_blank text input — dark field on the arena (a white box jars on space). */
.lrn-arena .eng-input {
  background: rgba(255, 255, 255, 0.06);
  border-color: var(--hairline-dark);
  color: var(--eng-white);
}
.lrn-arena .eng-input::placeholder { color: rgba(255, 255, 255, 0.5); }

/* Progress — subtle track + green glow fill (arena momentum) */
.lrn-progress--arena { background: rgba(255, 255, 255, 0.12); border: none; }
.lrn-progress--arena .eng-progress__fill {
  background: var(--eng-green);
  box-shadow: 0 0 16px rgba(57, 255, 137, 0.45);
}
.lrn-arena .lrn-momentum {
  flex: none; font-size: var(--fs-body-sm); font-weight: 600;
  color: rgba(255, 255, 255, 0.72); white-space: nowrap;
}

/* คำอธิบายไทย — the value moment: a designed panel, not gray scrap text */
.lrn-arena .lrn-explain {
  margin-top: 14px; padding: 14px 16px;
  background: rgba(255, 255, 255, 0.05);
  border-left: 3px solid var(--eng-green);
  border-radius: 10px;
}
.lrn-arena .lrn-explain__label {
  display: block; font-size: var(--fs-caption); font-weight: 700;
  letter-spacing: var(--tracking-caps); text-transform: uppercase;
  color: var(--eng-green); margin-bottom: 6px;
}
.lrn-arena #q-feedback-explanation {
  font-size: var(--fs-body-lg); line-height: var(--lh-loose);
  color: rgba(255, 255, 255, 0.92); overflow-wrap: anywhere;
}

/* Question enter — snappy slide+fade (rides --dur-slow / --ease-out → mood
   scaling + reduced-motion apply). quiz.js re-triggers it per render. */
@keyframes lrn-q-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}
.lrn-q-anim { animation: lrn-q-in var(--dur-base) var(--ease-out); }
#q-feedback:not([hidden]) { animation: lrn-q-in var(--dur-base) var(--ease-out); }

/* ---- dark variants of the quiz primitives (scoped to the arena) ---- */
.lrn-arena .eng-card {
  background: rgba(255, 255, 255, 0.05);
  border-color: var(--hairline-dark);
  color: var(--eng-white);
}
.lrn-arena .lrn-opt:not(.is-ok):not(.is-no),
body.eng-bg-space .eng-main--read .lrn-opt:not(.is-ok):not(.is-no) {
  background: rgba(255, 255, 255, 0.05);
  border-color: var(--hairline-dark);
  color: var(--eng-white);
}
.lrn-arena .lrn-opt:not(.is-ok):not(.is-no):hover,
body.eng-bg-space .eng-main--read .lrn-opt:not(.is-ok):not(.is-no):hover { border-color: rgba(255, 255, 255, 0.45); }
.lrn-arena .lrn-opt.is-sel,
body.eng-bg-space .eng-main--read .lrn-opt.is-sel {
  /* audit:contrast — white on the 32% purple wash was 4.4:1; the 2px purple
     border carries the selection, the wash can sit darker. */
  background: color-mix(in srgb, var(--eng-purple) 22%, transparent);
  border-color: var(--eng-purple);
}
/* Option badge default on dark — quiet ghosted-purple + light-purple letter
   (the is-sel/is-ok/is-no state rules above, higher specificity, override). */
.lrn-arena .lrn-opt__key,
body.eng-bg-space .eng-main--read .lrn-opt__key {
  /* audit:contrast — 20% chip put the lavender letter at 3.7:1; a darker
     chip + stronger border keeps the ghosted look and clears AA. */
  background: color-mix(in srgb, var(--eng-purple) 12%, transparent);
  border-color: color-mix(in srgb, var(--eng-purple) 55%, transparent);
  color: var(--eng-purple-soft);
}
.lrn-arena .lrn-mood-chip {
  background: rgba(255, 255, 255, 0.06);
  border-color: var(--hairline-dark);
  color: var(--eng-white);
}
.lrn-arena .lrn-mood-chip[aria-pressed="true"] {
  background: color-mix(in srgb, var(--eng-purple) 22%, transparent); /* audit:contrast family */
  border-color: var(--eng-purple);
}
.lrn-arena .lrn-audio {
  background: rgba(255, 255, 255, 0.05);
  border-color: var(--hairline-dark);
}
.lrn-arena .lrn-audio__track { background: rgba(255, 255, 255, 0.14); }
.lrn-arena .lrn-audio__time  { color: rgba(255, 255, 255, 0.7); }
/* Dark-surface quiet-button contrast (P-1 UI polish; WCAG AA). The base
   .eng-btn--ghost/--outline ride --eng-ink for light surfaces; every dark app
   surface (body.eng-bg-space: quiz arena · dashboard · /subscribe) needs the
   light text. Hover is re-stated because these rules tie the components.css
   :hover rules on specificity and would win on source order, freezing the
   hover color. Generalizes the old .lrn-arena-scoped pair — the dashboard
   logout + /subscribe LINE button were falling back to ink-on-dark. */
.eng-bg-space .eng-btn--ghost   { color: rgba(255, 255, 255, 0.85); }
.eng-bg-space .eng-btn--ghost:hover { color: var(--eng-purple-soft); }
.eng-bg-space .eng-btn--outline { color: var(--eng-white); border-color: var(--hairline-dark); }
.eng-bg-space .eng-btn--outline:hover {
  color: var(--eng-white);
  border-color: rgba(255, 255, 255, 0.35);
  background: rgba(255, 255, 255, 0.08);
}

/* =================================================================
   DELIGHT D-3 / DX-3 — Motion & Celebration surfaces. 2026-07-05.
   Static styling for the GSAP + canvas-confetti moments (the motion is
   JS-driven in quiz-celebrate.js, which also honours prefers-reduced-
   motion). Glows use the brand-channel rgba the token file uses.
   ================================================================= */

/* Revealed-correct option on a miss — a soft green glow draws the eye to the
   right answer (is-ok already tints it; this distinguishes it from the pick). */
.lrn-arena .lrn-opt--reveal,
body.eng-bg-space .eng-main--read .lrn-opt--reveal { box-shadow: 0 0 20px rgba(57, 255, 137, 0.35); }

/* Correct-answer mascot cheer — the celebrating mascot pops up from the bottom
   edge + fades (GSAP-driven; never overlaps the card). Motion-only → stays
   hidden under reduced-motion (quiz-celebrate.js skips it). */
.lrn-cheer-mascot {
  position: fixed; left: 50%; bottom: 0;
  width: clamp(108px, 24vw, 172px); height: auto;
  z-index: 55; pointer-events: none;
  filter: drop-shadow(0 0 26px rgba(57, 255, 137, 0.4));
}
.lrn-cheer-mascot[hidden] { display: none; }

/* +N XP fly-up — rises + fades over the answered card (GSAP-positioned, fixed,
   non-interactive). Green = the reward channel. */
.lrn-xp-fly {
  position: fixed;
  transform: translate(-50%, -100%);
  z-index: 60;
  pointer-events: none;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(22px, 4vw, 30px);
  color: var(--eng-green);
  text-shadow: 0 0 18px rgba(57, 255, 137, 0.55);
  white-space: nowrap;
}

/* Milestone overlay (level-up / streak) — the loud 10% on top of the complete
   screen: a space scrim + glass-on-space card (C2 exception) + purple glow. */
.lrn-overlay {
  position: fixed; inset: 0; z-index: 80;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  /* Opaque scrim — nothing behind (complete-screen mascot / heading / green
     glow) may bleed through the level-up card (zero-defect gate fix). */
  background: var(--eng-space);
}
.lrn-overlay[hidden] { display: none; }
.lrn-overlay__card {
  max-width: 380px; width: 100%; text-align: center;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-xl);
  padding: clamp(24px, 5vw, 40px) 24px;
  box-shadow: 0 0 64px rgba(99, 53, 240, 0.35);
}
.lrn-overlay__art {
  max-width: 180px; height: auto; display: block; margin: 0 auto 16px;
  border-radius: 16px;
}
.lrn-overlay__art[hidden] { display: none; }
.lrn-overlay__sub {
  color: rgba(255, 255, 255, 0.80);
  margin: 10px 0 22px;
  font-size: var(--fs-body-lg);
}

/* =================================================================
   DX-3.5 — state completeness + in-question motion. 2026-07-06.
   No-dead-end CTA clusters, the listening audio-viz equalizer, and the
   per-skill ambient signature. All motion is prefers-reduced-motion
   guarded (below). Zero new hex — brand tokens + brand-channel rgba only.
   ================================================================= */

/* Empty-state ("วันนี้ครบแล้ว!") standing-totals line — quiet light on space. */
.lrn-arena__mini {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-body);
  color: rgba(255, 255, 255, 0.82);
}

/* CTA cluster (complete / error / subscribe) — primary + a quiet way home,
   centered, wrapping on narrow so nothing overflows the arena. */
.lrn-arena__cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px;
}

/* ---- listening audio-viz equalizer ----
   5 bars that bounce while the clip plays (.lrn-audio--playing toggled on the
   wrap by quiz.js). Green = the "sound is live" channel. Quiet + low when idle. */
.lrn-eq {
  display: inline-flex;
  align-items: flex-end;
  gap: 3px;
  height: 15px;
  margin-bottom: 8px;
}
.lrn-eq i {
  display: block;
  width: 3px;
  height: 100%;
  border-radius: 2px;
  background: var(--eng-green);
  transform: scaleY(0.3);
  transform-origin: bottom;
  opacity: 0.5;
}
.lrn-audio--playing .lrn-eq i { opacity: 0.92; animation: lrn-eq-bounce 0.8s ease-in-out infinite; }
.lrn-audio--playing .lrn-eq i:nth-child(1) { animation-delay: 0s;    animation-duration: 0.72s; }
.lrn-audio--playing .lrn-eq i:nth-child(2) { animation-delay: 0.15s; animation-duration: 0.94s; }
.lrn-audio--playing .lrn-eq i:nth-child(3) { animation-delay: 0.30s; animation-duration: 0.64s; }
.lrn-audio--playing .lrn-eq i:nth-child(4) { animation-delay: 0.10s; animation-duration: 0.86s; }
.lrn-audio--playing .lrn-eq i:nth-child(5) { animation-delay: 0.22s; animation-duration: 0.78s; }
@keyframes lrn-eq-bounce {
  0%, 100% { transform: scaleY(0.28); }
  50%      { transform: scaleY(1); }
}

/* ---- per-skill ambient signature (taste-gated) ----
   A single low-opacity radial glow behind the arena content (z-index 0 <
   .lrn-arena__inner z-index 1), tinted per the current question's skill and
   drifting very slowly. ~4 reused brand hues across the 5 skills. Deliberately
   faint so it never fights the star-field or the card. */
.lrn-arena::before {
  content: "";
  position: absolute;
  inset: -20% -10%;
  z-index: 0;
  pointer-events: none;
  opacity: 0.55;
  background:
    radial-gradient(closest-side,
      color-mix(in srgb, var(--lrn-ambient, var(--eng-purple)) 14%, transparent),
      transparent 72%);
  background-repeat: no-repeat;
  background-position: 14% 20%;
  background-size: 72% 72%;
  animation: lrn-ambient-drift 26s ease-in-out infinite alternate;
}
.lrn-arena[data-skill="grammar"]    { --lrn-ambient: var(--eng-purple); }
.lrn-arena[data-skill="vocabulary"] { --lrn-ambient: var(--eng-green); }
.lrn-arena[data-skill="listening"]  { --lrn-ambient: var(--eng-yellow); }
.lrn-arena[data-skill="reading"]    { --lrn-ambient: var(--eng-purple); }
.lrn-arena[data-skill="speaking"]   { --lrn-ambient: var(--eng-flame); }
@keyframes lrn-ambient-drift {
  from { background-position: 12% 18%; }
  to   { background-position: 30% 34%; }
}

/* =================================================================
   DX-3.6 — Thai instruction chip + explanation polish. 2026-07-07.
   The Thai "what to do" line above the English stem, and the คำอธิบาย
   flourish (quoted key-term highlight + concept icon). Zero new hex —
   brand tokens + brand-channel rgba only.
   ================================================================= */

/* Thai instruction chip — the "what to do" line, visually separated above the
   English stem (a quiet glass pill; the green icon carries a light accent). Calm
   on purpose: it guides, the stem/options stay primary. */
.lrn-instruction {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
  padding: 7px 13px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-size: var(--fs-body-sm);
  font-weight: 600;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.9);
  overflow-wrap: anywhere;
}
.lrn-instruction[hidden] { display: none; }
.lrn-instruction__icon { flex: none; width: 17px; height: 17px; display: block; color: var(--eng-green); }

/* Quoted key terms in the explanation — a yellow micro-highlight + a soft
   underline that draws in on reveal. Yellow (not green): green is reserved for
   correct/action, and some highlighted terms are the WRONG-form token the
   explanation is contrasting — a green glow would misread as "this is correct".
   Yellow is the brand's micro-highlight channel (streak/emphasis), sized small
   (inline tokens only). Reduced-motion → static underline. */
.lrn-arena .lrn-key {
  color: var(--eng-yellow);
  font-weight: 700;
  padding-bottom: 1px;
  background-image: linear-gradient(var(--eng-yellow), var(--eng-yellow));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 1.5px;
  animation: lrn-key-in 0.5s var(--ease-out) both;
}
@keyframes lrn-key-in {
  from { background-size: 0% 1.5px; text-shadow: none; }
  to   { background-size: 100% 1.5px; text-shadow: 0 0 10px rgba(255, 242, 0, 0.4); }
}

/* Concept icon on the คำอธิบาย label (override display:block → inline-flex so the
   lightbulb sits beside the label; font/color/margin cascade from the base rule). */
.lrn-arena .lrn-explain__label { display: inline-flex; align-items: center; gap: 6px; }
.lrn-explain__icon { flex: none; width: 15px; height: 15px; display: block; }

/* ---------- DX-3.7 fill_blank inline input ---------- */
/* Single-blank fill_blank types INTO the sentence: an underline-style input in
   place of the ______, sitting in the prose flow. Purple accent (interactive/
   primary — NOT green, which is reserved for correct/action). Zero new hex
   (purple/white brand channels via rgba; glow not an offset shadow). */
.lrn-sentence--fill { line-height: 2.15; }
.lrn-blank-input {
  display: inline-block;
  min-width: 5.5em;
  max-width: 100%;
  margin: 0 3px;
  padding: 1px 9px 2px;
  font: inherit;
  font-weight: 700;
  text-align: center;
  color: var(--eng-white);
  background: rgba(255, 255, 255, 0.05);
  border: none;
  border-bottom: 2px solid rgba(99, 53, 240, 0.5);
  border-radius: 7px 7px 0 0;
  outline: none;
  appearance: none;
  -webkit-appearance: none;
  transition: border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out);
}
.lrn-blank-input::placeholder { color: rgba(255, 255, 255, 0.4); }
/* Round-6 post-answer slot verdicts: green/red underline + tint per box so a
   multi-blank miss shows WHICH slot failed. Colors mirror .lrn-opt is-ok/is-no
   (dark-arena bright tokens; the audit's inactive exemption applies — the
   inputs are disabled post-answer). */
.lrn-blank-input.is-slot-ok {
  border-bottom-color: var(--eng-green);
  background: rgba(57, 255, 137, 0.10);
}
.lrn-blank-input.is-slot-no {
  border-bottom-color: var(--eng-red);
  background: rgba(255, 71, 87, 0.12);
}
/* Round-6 answer reveal line under the miss verdict — the answer in words,
   bright green so "มาดูเฉลยกัน" delivers what it promises. */
.lrn-fb-reveal {
  margin: -4px 0 12px;
  font-weight: 700;
  color: var(--eng-green);
}

/* ---------- Round 7: "ยังงงข้อนี้? ถามได้เลย" ask panel ---------- */
/* The wrapper + panel deliberately carry NO display property — the hidden
   attribute must always win ((127) hidden-display-guard class). Quiet glass
   card on the dark arena; bubbles reuse the measured dark-surface ink pairs
   (white/0.92 body text ≈ the explanation panel's). */
.lrn-askai { margin-top: 14px; }
/* The ask-AI CTA must READ as a tappable chat chip, not a line of text
   (Jeff, round 7.1). Purple channel on purpose — green is the primary-action
   channel and "ข้อต่อไป" owns it; this is the secondary invite. */
.lrn-askai__btn {
  gap: 6px;
  color: var(--eng-white);
  background: rgba(99, 53, 240, 0.16);
  border: 1px solid rgba(99, 53, 240, 0.55);
}
.lrn-askai__btn:hover,
.lrn-askai__btn:focus-visible {
  background: rgba(99, 53, 240, 0.28);
  border-color: var(--eng-purple);
}
.lrn-askai__ico { font-size: 1rem; line-height: 1; }
/* IDLE NUDGE ((128) pattern): the chip breathes a soft purple glow until the
   first touch, then rests forever (quiz.js adds .is-touched once). NOT a fake
   live-status dot — the (139) lens allows motion that invites a first
   interaction and dies on it. */
.lrn-askai__btn:not(.is-touched) { animation: askaiNudge 2.4s ease-in-out infinite; }
@keyframes askaiNudge {
  0%, 100% { box-shadow: 0 0 0 0 rgba(99, 53, 240, 0); }
  50% { box-shadow: 0 0 14px 2px rgba(99, 53, 240, 0.45); }
}
#q-askai-panel {
  margin-top: 10px;
  padding: 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 12px;
}
.lrn-askai__msgs {
  max-height: 260px;
  overflow-y: auto;
}
.lrn-askai__msg {
  margin: 0 0 8px;
  padding: 8px 12px;
  border-radius: 10px;
  font-size: 0.95rem;
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.92);
}
.lrn-askai__msg--user {
  background: rgba(99, 53, 240, 0.22);
  margin-left: 2rem;
}
.lrn-askai__msg--ai {
  background: rgba(255, 255, 255, 0.06);
  margin-right: 2rem;
}
.lrn-askai__note {
  margin: 2px 0 8px;
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.72);
}
.lrn-askai__row { display: flex; gap: 8px; }
.lrn-askai__input {
  flex: 1;
  min-width: 0;
  padding: 8px 12px;
  font: inherit;
  color: var(--eng-white);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 9px;
  outline: none;
}
.lrn-askai__input::placeholder { color: rgba(255, 255, 255, 0.45); }
.lrn-askai__input:focus { border-color: var(--eng-purple); }
.lrn-blank-input:focus {
  border-bottom-color: var(--eng-purple);
  background: rgba(99, 53, 240, 0.13);
  box-shadow: 0 5px 16px -7px rgba(99, 53, 240, 0.65);
}

/* ---------- DX-3.7 structure card (grammar formula anchor) ---------- */
/* Sits on the arena surface (glass + neon accents; inherits the space bg — NO
   separate background). condition/pattern-A = purple, result/pattern-B = gold
   (NO green — reserved for correct). The per-block steady glow is the settle
   target the GSAP arrival-flare eases down to (clearProps hands boxShadow back
   here). Zero new hex — purple #6335F0 / gold #F2C94C / white channels via rgba. */
.lrn-sc {
  margin: 4px 0 18px;
  padding: 16px 16px 14px;
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-lg);
}
.lrn-sc__concept {
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  margin-bottom: 12px;
}
.lrn-sc__formula {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 10px;
  margin-bottom: 12px;
}
.lrn-sc__block {
  flex: 1 1 40%;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 9px 13px;
  border-radius: 12px;
  border: 1.5px solid transparent;
}
.lrn-sc__block--cond {
  background: rgba(99, 53, 240, 0.14);
  border-color: rgba(99, 53, 240, 0.5);
  box-shadow: 0 0 12px rgba(99, 53, 240, 0.28);
}
.lrn-sc__block--result {
  background: rgba(242, 201, 76, 0.12);
  border-color: rgba(242, 201, 76, 0.45);
  box-shadow: 0 0 12px rgba(242, 201, 76, 0.26);
}
.lrn-sc__label {
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 600;
}
/* ⚠️ THE THIRD BLOCK IS DELIBERATELY QUIET, and that is meaning rather than a
   colour compromise. Purple and gold are the two taken; green is reserved for
   correct/action, flame for the streak, red for wrong, and gold again for the
   Premium tier — there is no fourth loud hue to spend here. But the third case
   IS the absence case: "(ไม่ใส่)" is the rule about putting nothing there. A
   block with no glow and a hairline edge says that better than a third colour
   would. */
.lrn-sc__block--third {
  background: rgba(255, 255, 255, 0.06);
  border-color: var(--hairline-dark);
}
.lrn-sc__block--cond .lrn-sc__label { color: var(--eng-purple-soft); }
.lrn-sc__block--result .lrn-sc__label { color: var(--eng-gold); }
.lrn-sc__block--third .lrn-sc__label { color: rgba(255, 255, 255, 0.82); }
/* Three blocks + two separators do not fit a 40% basis — they wrap into a
   column and stop reading as alternatives. Narrow enough to sit in one row on
   the plate, still free to wrap at 390 where a row of three would be unreadable
   anyway. */
.lrn-sc--triple .lrn-sc__block { flex: 1 1 25%; }
/* At 390 a 25% basis fits two blocks then drops the third — a 2+1 row that
   reads as "these two, and also that one" when all three are equal
   alternatives. One per row states the alternation instead. */
@media (max-width: 640px) {
  .lrn-sc--triple .lrn-sc__block { flex: 1 1 100%; }
  .lrn-sc--triple .lrn-sc__sep { align-self: flex-start; }
}
.lrn-sc__tokens {
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: 700;
  color: var(--eng-white);
  line-height: 1.35;
}
.lrn-sc__sep {
  align-self: center;
  flex: none;
  font-weight: 800;
  font-size: 1.05rem;
  color: rgba(255, 255, 255, 0.6);
  text-shadow: 0 0 8px rgba(255, 255, 255, 0.22);
}
.lrn-sc__example {
  font-family: var(--font-body);
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.82);
  line-height: 1.5;
  margin-bottom: 6px;
}
.lrn-sc-key {
  font-weight: 700;
  color: var(--eng-white);
  padding-bottom: 1px;
  background-image: linear-gradient(var(--eng-purple), var(--eng-purple));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 1.5px;
}
.lrn-sc__caption {
  font-family: var(--font-body);
  font-size: 0.78rem;
  color: rgba(255, 255, 255, 0.62);
  line-height: 1.5;
}

/* =================================================================
   DX-2 — Command Center dashboard (the student office). 2026-07-06.
   A dark app surface (the arena's .eng-bg-space language + the DX-1 app-bar)
   that surfaces the learner's OWN identity + progress: CEFR + level ring,
   today's #1-weakness goal, a 5-skill profile radar, streak week-strip, and
   an aspirational badges shelf. Glass-on-space cards (C2 exception); glows use
   the brand-channel rgba the token file uses (--eng-purple 99,53,240 /
   --eng-green 57,255,137 / --eng-flame 255,107,44). Entrance motion is
   GSAP-driven in dashboard.js (SSR shows the final state; motion is polish).
   ================================================================= */
.lrn-home {
  min-height: calc(100vh - 58px);        /* fill below the thin dark app-bar */
  padding: clamp(20px, 5vh, 44px) 20px clamp(48px, 10vh, 96px);
}
.lrn-home__wrap {
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 3vh, 22px);
}

/* Shared glass card + section title */
.lrn-card {
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-xl);
  padding: clamp(18px, 4vw, 26px);
  box-shadow: 0 0 40px rgba(99, 53, 240, 0.14);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
/* ⚠️ flex-wrap IS LOAD-BEARING, and it is CONDITIONAL BY CONSTRUCTION. The note
   is `white-space: nowrap`, so without wrap it wins the squeeze and the heading
   loses: at 390 the fleet's h2 broke into three lines (เครื่องยนต์ / 5 ลำของ /
   คุณ) to make room for its own caption. With wrap the note simply drops to its
   own line WHEN IT CANNOT FIT — wide cards are untouched, which is why this is
   safe on a rule five sections share. */
.lrn-sec-title { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 10px; margin-bottom: 14px; }
.lrn-sec-title h1,
.lrn-sec-title h2 { min-width: 0; font-family: var(--font-display); font-weight: 700; font-size: var(--fs-h3); color: var(--eng-white); }
.lrn-sec-note { font-size: var(--fs-caption); color: rgba(255, 255, 255, 0.5); letter-spacing: var(--tracking-caps); text-transform: uppercase; white-space: nowrap; }
.lrn-empty { margin-top: 12px; font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.65); overflow-wrap: anywhere; }
/* p1walk@ UAT (2026-09-05) — 4th INK TRAP instance: when .lrn-empty is a DIV
   wrapping bare <p> (the /messages no-coach state), the token sheet's
   p { color: var(--fg-primary) } paints those children DARK on the dark
   surface (~1.x:1, "สีจม อ่านไม่ออก"). Dashboard escapes it only because it
   puts the class ON the <p>. :where() keeps (0,0,1) — ties with the token
   rule, wins on source order, loses to any classed color by design. */
:where(.lrn-empty) p { color: inherit; }
.lrn-empty a { color: var(--eng-green); text-decoration: underline; text-underline-offset: 3px; }

/* Item 8: trial-expiry banner. Colour/ink come from .lrn-notice--caution
   (self-contained light tint + dark ink — readable on the space surface too);
   this class only owns layout + the inline CTA link (inherits the dark ink). */
.lrn-home__notice { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
.lrn-home__notice a { color: inherit; font-weight: 700; text-decoration: underline; text-underline-offset: 3px; white-space: nowrap; }

/* 1 · Identity header */
.lrn-hero { display: flex; align-items: center; gap: clamp(12px, 3vw, 20px); }
.lrn-hero__mascot { flex: none; width: clamp(64px, 18vw, 88px); height: auto; filter: drop-shadow(0 0 24px rgba(57, 255, 137, 0.28)); }
.lrn-hero__body { flex: 1; min-width: 0; }
.lrn-hero__hi { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-h2); color: var(--eng-white); overflow-wrap: anywhere; }
.lrn-hero__sub { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.75); margin: 2px 0 12px; }

/* CEFR badge — the identity anchor (big) */
.lrn-cefr {
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  min-width: 82px; padding: 8px 16px;
  background: color-mix(in srgb, var(--eng-purple) 22%, transparent);
  border: 1.5px solid color-mix(in srgb, var(--eng-purple) 55%, transparent);
  border-radius: 16px; text-decoration: none;
  box-shadow: 0 0 26px rgba(99, 53, 240, 0.22);
}
.lrn-cefr small { font-size: 10px; letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--eng-purple-soft); }
.lrn-cefr b { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-h1); line-height: 1; color: var(--eng-white); }
/* The measurement date. Quiet, but PRESENT — it is deliberately not a title
   tooltip: a tooltip does not exist on a phone, and the phone is where a
   five-minute-a-day habit lives. A date nobody can see does not date anything. */
.lrn-cefr__at { display: block; margin-top: 4px; font-size: var(--fs-caption); color: rgba(255, 255, 255, 0.62); }
.lrn-cefr--empty b { font-size: var(--fs-body-lg); color: var(--eng-purple-soft); }

/* Level ring */
.lrn-ring { display: inline-flex; flex-direction: column; align-items: center; gap: 4px; }
.lrn-ring svg { display: block; }
.lrn-ring__track { fill: none; stroke: rgba(255, 255, 255, 0.12); stroke-width: 6; }
.lrn-ring__fill { fill: none; stroke: var(--eng-purple); stroke-width: 6; stroke-linecap: round; filter: drop-shadow(0 0 6px rgba(99, 53, 240, 0.85)); }
.lrn-ring__lvl { fill: var(--eng-white); font-family: var(--font-display); font-weight: 800; font-size: 22px; }
.lrn-ring__lbl { fill: rgba(255, 255, 255, 0.5); font-size: 8px; letter-spacing: 1.5px; }

/* ── FLIGHT PASS (UAT round 2) — the dashboard identity block as a dark neon
   boarding pass. Design intent: a learner screenshots this for a Story, so
   it must read as a TICKET at a glance — brand strip, mono field labels,
   perforated stub with punched notches, barcode, a route leg crawling
   จุดปล่อย → SATURN. Presentation only: every value already renders elsewhere
   on this page. The .lrn-cefr element inside keeps its base identity (it
   also lives on /onboarding); the pass merely re-dresses it. */
.lrn-pass {
  position: relative;
  border: 1.5px solid rgba(255, 255, 255, 0.14);
  border-radius: 20px;
  background:
    radial-gradient(120% 160% at 88% -20%, color-mix(in srgb, var(--eng-purple) 26%, transparent), transparent 55%),
    color-mix(in srgb, var(--eng-white) 5%, transparent);
  overflow: hidden;
}
/* shimmer sweep — the "holo foil" luxury cue; slow, subtle, rm-killed */
.lrn-pass::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(115deg, transparent 42%, rgba(255, 255, 255, 0.07) 50%, transparent 58%);
  transform: translateX(-100%);
  animation: lrn-pass-sheen 7s var(--ease-in-out) infinite;
  pointer-events: none;
}
@keyframes lrn-pass-sheen { 0%, 55% { transform: translateX(-100%); } 85%, 100% { transform: translateX(100%); } }
.lrn-pass__strip {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 18px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--eng-purple) 34%, transparent), transparent 78%);
  border-bottom: 1.5px dashed rgba(255, 255, 255, 0.22);
}
.lrn-pass__brand {
  font-family: var(--font-mono); font-size: 0.74rem; font-weight: 700;
  letter-spacing: 0.16em; color: var(--eng-green);
  text-shadow: 0 0 12px color-mix(in srgb, var(--eng-green) 45%, transparent);
}
.lrn-pass__code { font-family: var(--font-mono); font-size: 0.74rem; font-weight: 700; letter-spacing: 0.1em; color: rgba(255, 255, 255, 0.8); }
.lrn-pass__grid { display: grid; grid-template-columns: 1fr auto; }
.lrn-pass__main { padding: 16px 18px 18px; min-width: 0; }
.lrn-pass__row { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.lrn-pass__field { display: inline-flex; flex-direction: column; gap: 2px; min-width: 0; }
.lrn-pass__field small {
  font-family: var(--font-mono); font-size: 0.66rem; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase; color: rgba(255, 255, 255, 0.66);
}
.lrn-pass__field b {
  font-family: var(--font-display); font-weight: 700; font-size: 1.06rem;
  color: var(--eng-white); overflow-wrap: anywhere;
}
/* the route leg — a pip crawling launch → Saturn (decoration, no progress claim:
   both endpoints are the METAPHOR, the pip loops; aria-hidden in markup) */
.lrn-pass__route {
  display: flex; align-items: center; gap: 10px;
  margin: 14px 0 12px;
  font-family: var(--font-mono); font-size: 0.7rem; font-weight: 700;
  letter-spacing: 0.12em; color: rgba(255, 255, 255, 0.7);
}
.lrn-pass__leg {
  position: relative; flex: 1; height: 0;
  border-top: 2px dashed rgba(255, 255, 255, 0.25);
}
.lrn-pass__leg::after {
  content: "✦";
  position: absolute; top: -0.72em; left: 0;
  color: var(--eng-green); font-size: 0.85rem; font-style: normal;
  text-shadow: 0 0 8px color-mix(in srgb, var(--eng-green) 60%, transparent);
  animation: lrn-pass-leg 5.5s var(--ease-in-out) infinite;
}
@keyframes lrn-pass-leg { 0%, 100% { left: 0; } 50% { left: calc(100% - 12px); } }
.lrn-pass__leg.has-craft::after { content: none; }
.lrn-pass__craft {
  position: absolute; top: -13px; left: 0;
  width: 26px; height: 26px; object-fit: contain;
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--eng-green) 45%, transparent));
  animation: lrn-pass-leg 5.5s var(--ease-in-out) infinite;
}
.lrn-pass__stats { display: flex; align-items: center; gap: clamp(14px, 4vw, 28px); flex-wrap: wrap; }
/* stub: perforation + punched notches + level ring + barcode */
.lrn-pass__stub {
  position: relative;
  display: flex; align-items: center; gap: 14px;
  padding: 14px 16px 14px 18px;
  border-left: 2px dashed rgba(255, 255, 255, 0.22);
}
.lrn-pass__stub::before,
.lrn-pass__stub::after {
  content: "";
  position: absolute; left: -8px;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--eng-space);
  border: 1.5px solid rgba(255, 255, 255, 0.14);
}
.lrn-pass__stub::before { top: -8px; }
.lrn-pass__stub::after { bottom: -8px; }
.lrn-pass__barcode {
  display: block; width: 34px; align-self: stretch; border-radius: 3px;
  background: repeating-linear-gradient(90deg,
    rgba(255, 255, 255, 0.55) 0 2px, transparent 2px 5px,
    rgba(255, 255, 255, 0.55) 5px 9px, transparent 9px 11px,
    rgba(255, 255, 255, 0.55) 11px 12px, transparent 12px 16px);
}
/* CEFR chip re-dressed as a ticket field inside the pass (base identity for
   /onboarding untouched) */
.lrn-pass .lrn-cefr { background: color-mix(in srgb, var(--eng-purple) 30%, transparent); box-shadow: 0 0 22px color-mix(in srgb, var(--eng-purple) 35%, transparent); }
@media (max-width: 430px) {
  .lrn-pass__stub { flex-direction: column; gap: 10px; padding: 12px; }
  .lrn-pass__barcode { width: 100%; height: 22px; align-self: auto; }
}
.lrn-ring__cap { font-size: var(--fs-caption); color: rgba(255, 255, 255, 0.6); }

/* 2 · Today's Session — goal-gradient card (the ONE loud element) */
.lrn-today {
  position: relative; overflow: hidden;
  border-radius: var(--radius-xl); padding: clamp(20px, 5vw, 28px);
  background: linear-gradient(135deg, color-mix(in srgb, var(--eng-purple) 52%, var(--eng-space)) 0%, var(--eng-space) 72%);
  border: 1px solid color-mix(in srgb, var(--eng-purple) 40%, transparent);
  box-shadow: 0 0 48px rgba(99, 53, 240, 0.30);
}
.lrn-today__eyebrow { display: inline-block; font-size: var(--fs-caption); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--eng-green); font-weight: 700; margin-bottom: 8px; }
.lrn-today__title { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-h2); color: var(--eng-white); }
.lrn-today__focus { font-size: var(--fs-body); color: rgba(255, 255, 255, 0.85); margin: 6px 0 18px; overflow-wrap: anywhere; }
.lrn-today__focus b { color: var(--eng-green); }

/* 3 · Skill Profile — pentagon radar + legend */
/* ── THE FLEET ────────────────────────────────────────────────────────────
   Five craft on five lanes: launch pad at the left edge, Saturn at the right.
   Distance along a lane is STRENGTH; the flame is STARTED. Replaced the radar
   2026-08-30 — a pentagon shows a shape, and the metaphor promises a journey
   toward something, which a shape has no room for.

   ⚠️ NO PER-CRAFT COLOUR (ruling A is vocabulary only). Lanes separate by row
   and by label — categorical, so it survives 390px, which five hues would not.
   Green stays reserved for action and focus. */
.lrn-fleet { position: relative; display: grid; grid-template-columns: 1fr auto; gap: 0 clamp(6px, 1.6vw, 12px); align-items: stretch; }
.lrn-fleet__lanes { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
/* ⚠️ THE DESTINATION IS SHARED, AND IT HAS TO LOOK IT. Centred in the column,
   Saturn landed level with the third lane and read as THAT craft's destination
   — five journeys to one planet became one journey past four bystanders. It now
   stretches the full stack with a hairline down its leading edge: the finish
   line every craft is short of, with the planet ON it. Same element, same
   markup; the line is what makes the five a fleet. */
.lrn-fleet__dest { position: relative; display: grid; place-items: center; padding-left: clamp(6px, 1.6vw, 12px); }
.lrn-fleet__dest::before {
  content: "";
  position: absolute; left: 0; top: 4px; bottom: 4px; width: 1px;
  background: linear-gradient(180deg, transparent, var(--eng-purple-soft) 14%, var(--eng-purple-soft) 86%, transparent);
  opacity: 0.5;
}
.lrn-fleet__ring { fill: none; stroke: var(--eng-purple-soft); stroke-width: 1.6; opacity: 0.85; }
.lrn-fleet__body { fill: color-mix(in srgb, var(--eng-purple) 65%, transparent); stroke: var(--eng-purple); stroke-width: 1.6; filter: drop-shadow(0 0 8px rgba(99, 53, 240, 0.55)); }
/* ⚠️ TWO ROWS, NOT THREE COLUMNS — AND THE REASON IS THE METAPHOR. Laid out as
   label | track | value, the track got ~120px in this card and 34% was
   indistinguishable from 75%: a journey nobody could read the length of. The
   pentagon it replaced did not care, because a shape has no length. Stacking
   the labels above gives the track the card's full width (~2.5x the travel)
   and it reads better at 390, where a three-column grid squeezes hardest. */
.lrn-lane { display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: 3px 10px; font-size: var(--fs-body-sm); }
.lrn-lane__name { grid-column: 1; grid-row: 1; color: rgba(255, 255, 255, 0.85); min-width: 0; }
.lrn-lane__val { grid-column: 2; grid-row: 1; }
.lrn-lane__track { grid-column: 1 / -1; grid-row: 2; position: relative; height: 12px; margin-top: 2px; }
.lrn-lane.is-focus .lrn-lane__name { color: var(--eng-green); font-weight: 700; }
/* The lane itself — the route not yet travelled. */
/* ⚠️ THE ROAD REACHES THE PLANET (right: -12px). It used to stop at the track's
   own edge, leaving a gap before the destination column — five craft travelling
   toward nothing in particular. The unflown remainder of the journey IS the
   dim rail, so it has to arrive somewhere. -12px is the dest column's
   padding-left; the two are one gap, not two. */
.lrn-lane__track::before { content: ""; position: absolute; left: 0; right: -12px; top: 5px; height: 2px; background: rgba(255, 255, 255, 0.12); border-radius: var(--radius-pill); }
/* The trail — how far this craft has carried the learner. */
.lrn-lane__trail { position: absolute; left: 0; top: 5px; height: 2px; transform-origin: left center; border-radius: var(--radius-pill); background: linear-gradient(90deg, color-mix(in srgb, var(--eng-purple) 25%, transparent), var(--eng-purple)); }
.lrn-lane.is-started .lrn-lane__trail { box-shadow: 0 0 10px rgba(99, 53, 240, 0.55); }
/* Where placement put this craft — only drawn when there IS a placement. */
.lrn-lane__base { position: absolute; top: 1px; width: 2px; height: 10px; background: var(--eng-purple-soft); border-radius: 1px; transform: translateX(-1px); opacity: 0.8; }
/* The craft: a nose pointing at Saturn. A drifted craft is dim; a started one
   is under power and carries the flame, so the difference is a KIND not a
   shade — readable at 390 and in greyscale. */
/* The craft is the REAL raster (UAT round 2.1) — /img/skill-marks/<skill>.png,
   the same approved set the brief band, arena chip and flight pass fly.
   The <b> wrapper keeps the left:% positioning (GSAP + the per-lane test
   helper both key on it); only the glyph inside changed. */
.lrn-lane__craft { position: absolute; top: calc(50% - 12px); width: 24px; height: 24px; margin-left: -12px; }
.lrn-lane__craft img { display: block; width: 100%; height: 100%; object-fit: contain; opacity: 0.82; }
.lrn-lane.is-started .lrn-lane__craft img { opacity: 1; filter: drop-shadow(0 0 5px rgba(57, 255, 137, 0.35)); }
/* The flame — present only under power. */
.lrn-lane.is-started .lrn-lane__craft::before { content: ""; position: absolute; top: calc(50% - 2px); left: -9px; width: 9px; height: 4px; border-radius: var(--radius-pill); background: linear-gradient(90deg, transparent, var(--eng-green)); box-shadow: 0 0 8px rgba(57, 255, 137, 0.6); }
/* Parked at the pad: desaturated + dim, still recognisably THAT craft. */
.lrn-lane.is-unmeasured .lrn-lane__craft img { opacity: 0.38; filter: grayscale(1); }
.lrn-lane__val { font-family: var(--font-display); font-weight: 700; color: var(--eng-white); white-space: nowrap; min-width: 22px; text-align: right; }
.lrn-lane__val em { color: var(--eng-green); font-style: normal; }
.lrn-lane.is-unmeasured .lrn-lane__val { color: rgba(255, 255, 255, 0.55); }
.lrn-fleet__note { margin-top: 10px; font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.6); overflow-wrap: anywhere; }

/* CL-MEANING M2+A9 · §7.3 evidence-strength chip under each skill name.
   Quiet by default; green only for improving (color law); confirmed gets
   brighter white + weight, NOT yellow text (yellow = micro-highlight, never
   text fields — and a class-level !important here defeats the contrast
   audit's universal transparent-text override, painting the glyphs into its
   background screenshot). State rules win over the is-focus tint by equal
   specificity + later order — no !important. Tokens only. */
.lrn-lane__ev {
  display: inline; margin-left: 6px; font-size: 10.5px; font-weight: 400; letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.5); white-space: nowrap;   /* margin-top was for the old block layout; inert on an inline box */
}
.lrn-lane.is-focus .lrn-lane__ev { color: rgba(255, 255, 255, 0.6); }
.lrn-lane .lrn-lane__ev.lrn-ev--improving { color: var(--eng-green); }
.lrn-lane .lrn-lane__ev.lrn-ev--confirmed { color: rgba(255, 255, 255, 0.85); font-weight: 600; }
.lrn-ev--stale { font-style: italic; }

/* §6.2.H: the weakness row ends in an ACTION, not a floating number. */
.lrn-profile__started { margin-top: 10px; font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.6); overflow-wrap: anywhere; }
.lrn-profile__act { margin-top: 14px; font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.75); }
.lrn-profile__act b { color: var(--eng-white); }
.lrn-profile__act a { color: var(--eng-green); text-decoration: underline; text-underline-offset: 3px; }

/* 2.5 · แผนปิดจุดอ่อน (CL-MEANING M1) — quiet numbered list on the dark card;
   the ONE loud element stays the Today CTA above. Tokens + rgba only. */
.lrn-plan__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.lrn-plan__row {
  display: grid; grid-template-columns: auto 1fr auto; align-items: start; gap: 12px;
  padding: 12px 0; border-bottom: 1px solid var(--hairline-dark);
}
.lrn-plan__row:last-child { border-bottom: 0; }
.lrn-plan__slot {
  width: 26px; height: 26px; border-radius: 999px; display: inline-flex;
  align-items: center; justify-content: center; margin-top: 2px;
  font-family: var(--font-display); font-weight: 700; font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.7); background: rgba(255, 255, 255, 0.08);
  border: 1px solid var(--hairline-dark);
}
.lrn-plan__body { min-width: 0; }
.lrn-plan__title {
  color: var(--eng-white); font-weight: 600; text-decoration: none;
  display: inline-block; overflow-wrap: anywhere;
}
.lrn-plan__title:hover { color: var(--eng-green); }
.lrn-plan__meta { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.55); margin-top: 2px; }
.lrn-plan__reason { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.75); margin-top: 4px; }
.lrn-plan__reason::before { content: '✓ '; color: var(--eng-green); }
.lrn-plan__flag {
  font-size: var(--fs-caption); white-space: nowrap; margin-top: 4px;
  padding: 3px 10px; border-radius: 999px;
  color: rgba(255, 255, 255, 0.75); border: 1px solid var(--hairline-dark);
}
.lrn-plan__foot { margin-top: 12px; font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.55); }

/* 3.6 · A2 goal escape hatch — a QUIET disclosure inside the plan card.
   Deliberately not a loud button: it changes what the list above contains, so
   it belongs with the list, and a learner who picked wrong is already
   embarrassed. Zero new hex — tokens + the white channel. */
.lrn-goalswap { margin: 0; }
/* UAT round 3.1 (Jeff): the widget stops hiding the shop. The old <details>
   wrapper put ALL THREE routes behind a "เลือกใหม่" toggle; now the routes are
   always in the open (side by side where the card is wide enough) and only
   each route's FULL topic list stays behind its own <details>. */
.lrn-goalswap__now {
  margin: 0 0 2px; font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.65);
}
.lrn-goalswap__now b { color: rgba(255, 255, 255, 0.9); font-weight: 600; }
/* The reassurance line is the whole point of the control — it is what stops
   this reading as "reset my progress". Higher contrast than the summary. */
/* ⚠️ A MEASURE CAP IS THE OTHER HALF OF THE FULL-WIDTH FIX. Putting this card
   on `grid-column: 1 / -1` cured a 92px track and created the opposite problem:
   77.6em of unbroken Thai across a 1140px bento row. Both are unreadable; only
   one of them looks broken. `--container-flow` (560px) is the existing tier the
   article package already uses for prose inside a wide container — not a new
   number. */
.lrn-goalswap__safe {
  max-width: var(--container-flow);
  margin: 12px 0 14px; font-size: var(--fs-body-sm); line-height: 1.7;
  color: rgba(255, 255, 255, 0.75);
}
.lrn-goalswap__safe b { color: rgba(255, 255, 255, 0.95); font-weight: 600; }
.lrn-goalswap__keep { color: var(--eng-green); }
.lrn-goalswap__keep b { color: var(--eng-green); }
.lrn-goalswap__opts {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  align-items: start;
}
/* UAT round 3: each door = the choose BUTTON + a collapsed route manifest
   underneath. The button keeps its class + data attrs verbatim (dashboard.js
   and the goal-change pins key on them); the wrapper only groups. */
.lrn-goalswap__door {
  position: relative;
  border-radius: var(--radius-lg);
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.03);
  padding: 10px 4px 8px;
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
/* FD chrome (UAT 3.1 verdict: "เลือกเส้นทางชีวิต" deserves instrument
   ceremony): HUD corner brackets — same recipe as .lf-card, purple at rest,
   green on the ACTIVE route. */
.lrn-goalswap__door::before,
.lrn-goalswap__door::after {
  content: ""; position: absolute; width: 18px; height: 18px;
  border: 2px solid color-mix(in srgb, var(--eng-purple) 55%, transparent);
  pointer-events: none;
  transition: border-color var(--dur-base) var(--ease-out);
}
.lrn-goalswap__door::before { top: -2px; left: -2px; border-right: 0; border-bottom: 0; border-top-left-radius: 14px; }
.lrn-goalswap__door::after { bottom: -2px; right: -2px; border-left: 0; border-top: 0; border-bottom-right-radius: 14px; }
.lrn-goalswap__door:hover { border-color: rgba(255, 255, 255, 0.24); }
.lrn-goalswap__door:hover::before,
.lrn-goalswap__door:hover::after { border-color: color-mix(in srgb, var(--eng-purple) 85%, transparent); }
.lrn-goalswap__door.is-current {
  border-color: color-mix(in srgb, var(--eng-green) 45%, transparent);
  background: color-mix(in srgb, var(--eng-green) 5%, transparent);
  box-shadow: 0 0 18px color-mix(in srgb, var(--eng-green) 14%, transparent);
}
.lrn-goalswap__door.is-current::before,
.lrn-goalswap__door.is-current::after { border-color: color-mix(in srgb, var(--eng-green) 70%, transparent); }
/* mono route designation — a measured label, so it speaks mono like every
   other instrument value on the deck */
.lrn-goalswap__routeno {
  display: block; padding: 0 12px 6px;
  font-family: var(--font-mono); font-size: 0.64rem; font-weight: 700;
  letter-spacing: 0.16em; color: rgba(255, 255, 255, 0.55);
  border-bottom: 1px dashed rgba(255, 255, 255, 0.1);
}
.lrn-goalswap__door.is-current .lrn-goalswap__routeno { color: var(--eng-green); }
.lrn-goalswap__opt {
  display: flex; align-items: flex-start; gap: 12px;
  width: 100%; text-align: left; cursor: pointer;
  padding: 12px 14px; border-radius: var(--radius-lg);
  border: 1px solid var(--hairline-dark);
  background: rgba(255, 255, 255, 0.03);
  color: inherit; font: inherit;
  transition: border-color var(--dur-base) ease, background var(--dur-base) ease;
}
.lrn-goalswap__opt-icon {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; font-size: 20px;
  border: 1.5px solid color-mix(in srgb, var(--eng-purple) 45%, transparent);
  border-radius: 12px;
  background: color-mix(in srgb, var(--eng-purple) 14%, transparent);
}
.lrn-goalswap__door.is-current .lrn-goalswap__opt-icon {
  border-color: color-mix(in srgb, var(--eng-green) 55%, transparent);
  background: color-mix(in srgb, var(--eng-green) 12%, transparent);
  box-shadow: 0 0 12px color-mix(in srgb, var(--eng-green) 25%, transparent);
}
.lrn-goalswap__opt-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* ── the route manifest: what this path actually contains, as a document —
   mono module headers + one line per mission topic. Collapsed by default;
   capped + scrollable when open so 200+ topics never swallow the page. */
.lrn-manifest { margin: 6px 0 2px; }
.lrn-manifest__sum {
  cursor: pointer; list-style: none;
  font-family: var(--font-mono); font-size: 0.76rem; font-weight: 700;
  letter-spacing: 0.06em; color: rgba(255, 255, 255, 0.75);
  padding: 6px 8px; border-radius: 10px;
}
.lrn-manifest__sum::-webkit-details-marker { display: none; }
.lrn-manifest__sum:hover { color: var(--eng-white); background: rgba(255, 255, 255, 0.04); }
.lrn-manifest[open] .lrn-manifest__sum { color: var(--eng-green); }
.lrn-manifest__scroll {
  max-height: 320px; overflow-y: auto;
  margin-top: 4px; padding: 10px 12px;
  border: 1px solid var(--hairline-dark); border-radius: 12px;
  background: rgba(0, 0, 0, 0.25);
}
.lrn-manifest__mod { margin-bottom: 12px; }
.lrn-manifest__mod:last-child { margin-bottom: 0; }
.lrn-manifest__modhead {
  font-family: var(--font-mono); font-size: 0.68rem; font-weight: 700;
  letter-spacing: 0.14em; color: rgba(255, 255, 255, 0.62);
  margin: 0 0 6px; padding-bottom: 4px;
  border-bottom: 1px dashed rgba(255, 255, 255, 0.14);
}
.lrn-manifest__list { list-style: none; margin: 0; padding: 0; }
.lrn-manifest__list li {
  display: flex; align-items: baseline; gap: 8px;
  padding: 3px 0; font-size: 0.88rem; line-height: 1.45;
}
.lrn-manifest__cefr {
  flex: none; font-family: var(--font-mono); font-style: normal;
  font-size: 0.64rem; font-weight: 700; letter-spacing: 0.06em;
  color: var(--eng-purple-soft);
  border: 1px solid color-mix(in srgb, var(--eng-purple) 40%, transparent);
  border-radius: 6px; padding: 1px 5px;
}
.lrn-manifest__title { flex: 1; min-width: 0; color: rgba(255, 255, 255, 0.92); overflow-wrap: anywhere; }
.lrn-manifest__skill { flex: none; font-size: 0.72rem; color: rgba(255, 255, 255, 0.55); }

/* ── Reading card (UAT round 3 ③) — related articles + bookmarks. */
/* UAT round 3.1 (Jeff: "ไม่ทำให้รู้สึกอยากพัฒนาตัวเอง อย่างน้อยมี excerpt"):
   the related articles become feature TILES — hue-tinted glass, the skill's
   craft raster, the article's own quick answer as a clamped excerpt, and the
   page's real read-time in mono. Bookmarks below keep the compact row form
   (they are a checklist the learner already chose, not a pitch). */
.lrn-reading__grid {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  align-items: stretch;
  margin: 0 0 4px;
}
.lrn-reading__tile {
  display: flex; flex-direction: column; gap: 7px;
  padding: 12px 14px 11px;
  border-radius: var(--radius-lg);
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgb(var(--read-hue, 99 53 240) / 0.08);
  text-decoration: none;
  transition: border-color var(--dur-base) var(--ease-out),
              background-color var(--dur-base) var(--ease-out);
}
.lrn-reading__tile:hover {
  background: rgb(var(--read-hue, 99 53 240) / 0.14);
  border-color: rgb(var(--read-hue, 99 53 240) / 0.55);
}
.lrn-reading__tile.is-focus { border-color: color-mix(in srgb, var(--eng-green) 40%, transparent); }
.lrn-reading__top { display: flex; align-items: center; gap: 8px; }
.lrn-reading__craft { width: 30px; height: 30px; margin-left: auto; opacity: 0.9; }
.lrn-reading__ex {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: 0.83rem; line-height: 1.6;
  color: rgba(255, 255, 255, 0.66);
}
.lrn-reading__meta {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin-top: auto;
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.7);
}
.lrn-reading__chip { white-space: nowrap; }
.lrn-reading__list { list-style: none; margin: 0 0 4px; padding: 0; }
.lrn-reading__row + .lrn-reading__row { border-top: 1px solid rgba(255, 255, 255, 0.08); }
.lrn-reading__link {
  display: flex; align-items: baseline; gap: 10px;
  padding: 10px 4px; text-decoration: none; color: inherit;
  border-radius: 10px;
  transition: background var(--dur-base) var(--ease-out);
}
.lrn-reading__link:hover { background: rgba(255, 255, 255, 0.05); }
.lrn-reading__cat {
  flex: none; font-family: var(--font-mono); font-size: 0.68rem; font-weight: 700;
  letter-spacing: 0.08em; color: var(--eng-purple-soft);
  border: 1px solid color-mix(in srgb, var(--eng-purple) 45%, transparent);
  border-radius: 999px; padding: 2px 8px;
}
.lrn-reading__cat.is-focus { color: var(--eng-green); border-color: color-mix(in srgb, var(--eng-green) 50%, transparent); }
.lrn-reading__title { flex: 1; min-width: 0; font-size: 0.95rem; color: rgba(255, 255, 255, 0.94); overflow-wrap: anywhere; }
.lrn-reading__tile .lrn-reading__title { flex: none; font-weight: 600; line-height: 1.45; }
.lrn-reading__go { flex: none; font-size: 0.85rem; font-weight: 600; color: var(--eng-green); }
.lrn-reading__tile .lrn-reading__go { margin-left: auto; font-family: var(--font-body); }
.lrn-reading__bm { margin-top: 12px; padding-top: 10px; border-top: 1px dashed rgba(255, 255, 255, 0.16); }
.lrn-reading__bmhead { font-family: var(--font-mono); font-size: 0.74rem; font-weight: 700; letter-spacing: 0.1em; color: rgba(255, 255, 255, 0.75); margin: 0 0 4px; }
.lrn-reading__empty { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.6); margin: 6px 0 0; }
.lrn-goalswap__opt:hover:not(:disabled):not(.is-current) {
  border-color: color-mix(in srgb, var(--eng-green) 45%, transparent);
  background: rgba(255, 255, 255, 0.06);
}
.lrn-goalswap__opt:disabled { opacity: 0.6; cursor: default; }
.lrn-goalswap__opt.is-current {
  border-color: color-mix(in srgb, var(--eng-green) 50%, transparent);
  background: color-mix(in srgb, var(--eng-green) 10%, transparent);
  cursor: default;
}
.lrn-goalswap__opt-title { display: block; font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.92); }
.lrn-goalswap__opt-sub { display: block; font-size: var(--fs-caption); color: rgba(255, 255, 255, 0.6); margin-top: 3px; }
.lrn-goalswap__opt-count {
  display: block; margin-top: 5px;
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.05em;
  color: rgba(255, 255, 255, 0.72);
}
.lrn-goalswap__opt-flag { display: inline-block; margin-top: 6px; font-size: var(--fs-caption); color: var(--eng-green); }
/* the teaser: the first three mission topics IN THE OPEN — the bait rows.
   Same CEFR chip vocabulary as the manifest so the two read as one document. */
.lrn-goalswap__teaser { list-style: none; margin: 2px 0 0; padding: 0 10px; }
.lrn-goalswap__teaser li {
  display: flex; align-items: baseline; gap: 8px;
  padding: 5px 0; font-size: 0.84rem; line-height: 1.5;
  color: rgba(255, 255, 255, 0.85);
  border-top: 1px dashed rgba(255, 255, 255, 0.1);
}
.lrn-goalswap__teaser li span { overflow-wrap: anywhere; }
.lrn-goalswap .lrn-error-chip { margin-top: 10px; }
@media (prefers-reduced-motion: reduce) {
  .lrn-goalswap__opt { transition: none; }
}

/* 4 · Streak */
.lrn-streak__head { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; }
.lrn-streak__flame { flex: none; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; color: var(--eng-flame); }
.lrn-streak__flame svg { width: 32px; height: 32px; filter: drop-shadow(0 0 12px rgba(255, 107, 44, 0.5)); }
.lrn-streak__num { font-size: var(--fs-body); color: rgba(255, 255, 255, 0.85); }
.lrn-streak__num b { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-h2); color: var(--eng-white); }
.lrn-streak__best { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.55); margin-top: 2px; }
/* State line — one per state, ALWAYS rendered. Quiet by default; at-risk is the
   only case that raises its voice, in the streak's own flame language (the same
   treatment .lrn-trend__badge.is-down already uses on this surface). */
.lrn-streak__state { margin-top: 14px; font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.62); }
.lrn-streak--at_risk .lrn-streak__state { color: var(--eng-flame); font-weight: 700; }
.lrn-streak--at_risk .lrn-streak__flame svg { filter: drop-shadow(0 0 16px color-mix(in srgb, var(--eng-flame) 75%, transparent)); }
/* The flame is LIT while the streak is alive and OUT when it is not — a
   difference in kind, not in degree, matching the badge shelf's locked state. */
.lrn-streak--none .lrn-streak__flame,
.lrn-streak--lapsed .lrn-streak__flame { color: rgba(255, 255, 255, 0.3); }
.lrn-streak--none .lrn-streak__flame svg,
.lrn-streak--lapsed .lrn-streak__flame svg { filter: none; }
.lrn-week { display: flex; gap: clamp(4px, 2vw, 10px); }
.lrn-week__cell { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; }
.lrn-week__cell i {
  width: clamp(22px, 7vw, 32px); height: clamp(22px, 7vw, 32px); border-radius: 10px;
  background: rgba(255, 255, 255, 0.07); border: 1px solid var(--hairline-dark);
}
.lrn-week__cell.is-on i {
  /* N10: opaque flame-forward fill — the 30%-on-transparent mix read as
     muddy brown-red against the dark card, clashing with the flame icon.
     Same --eng-flame language, mixed opaque toward the flame. */
  background: color-mix(in srgb, var(--eng-flame) 72%, var(--eng-space));
  border-color: var(--eng-flame);
  box-shadow: 0 0 14px rgba(255, 107, 44, 0.4);
}
.lrn-week__cell.is-today i { outline: 2px solid var(--eng-green); outline-offset: 1px; }
.lrn-week__cell small { font-size: 10px; color: rgba(255, 255, 255, 0.6); } /* audit:contrast — 0.45 measured 4.49-4.50:1 once short text got 3 samples */

/* 5 · Badges shelf (aspirational locked preview; real award = DX-5) */
.lrn-badges__shelf { display: flex; gap: clamp(10px, 3vw, 18px); flex-wrap: wrap; }
.lrn-badges__shelf .lrn-badge { min-width: 76px; }
.lrn-badge { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; min-width: 0; }
/* LOCKED is the default state here; earned overrides it further down.
   The two must differ in KIND, not in degree. They used to differ only by
   brightness — locked was the reward greyed to 50% — and a reward that is
   merely dim reads as "you have this, styled quietly". The failure runs both
   ways: the `ten_missions` v1 art was rejected precisely because, lacking a
   glow, it read as ALREADY LOCKED while being the earned state. Two states
   separated by one continuous dimension will be confused in both directions,
   and a brightness-only difference is invisible to a low-vision learner. So
   locked now differs three ways at once: flat silhouette (no colour, no glow,
   no interior detail), a DASHED edge that reads as an empty slot, and its own
   text line. None of the three is a matter of degree. */
.lrn-badge__art {
  position: relative; width: 100%; max-width: 88px; aspect-ratio: 1;
  border-radius: 16px; overflow: hidden; padding: 6px;
  border: 1px dashed var(--hairline-dark);
}
.lrn-badge__art img {
  width: 100%; height: 100%; object-fit: contain;
  /* A flat pale silhouette: brightness(0) collapses every opaque pixel to
     black with alpha intact, invert(1) lifts it to white, opacity drops it to
     a ghost. What remains is the SHAPE of the badge — legible as a placeholder,
     impossible to mistake for the award itself. */
  filter: brightness(0) invert(1) opacity(0.22);
}
/* No scrim: the silhouette already reads as absent, and a 45% wash over a
   pale shape only muddies it. The glyph carries the state on its own. */
.lrn-badge__lock { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.6); }
.lrn-badge__lock svg { width: 22px; height: 22px; }
.lrn-badge__name { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.8); overflow-wrap: anywhere; }
.lrn-badge__hint { font-size: var(--fs-caption); color: rgba(255, 255, 255, 0.45); overflow-wrap: anywhere; }
/* The earned state said in words, so it is not carried by colour alone. */
.lrn-badge__state { font-size: var(--fs-caption); font-weight: 700; color: var(--eng-green); overflow-wrap: anywhere; }

/* foot — logout + plan chip */
.lrn-home__foot { display: flex; flex-direction: column; align-items: center; gap: 12px; margin-top: 8px; }
.lrn-home__status { display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap; }
.lrn-home__foot .lrn-legal-links { margin-top: 0; }
.lrn-home__plan { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.5); }

/* ---------- P-1 /subscribe QR payment page ----------
   Narrow single-column on every width (a payment flow stays focused; the
   DX-2 bento grid on .lrn-home__wrap deliberately NOT reused). Composes
   existing card/notice/btn classes; zero new hex. */
.sub-wrap { width: 100%; max-width: 560px; margin: 0 auto; display: flex; flex-direction: column; gap: clamp(14px, 2.5vh, 20px); }
.sub-head__eyebrow { font-size: var(--fs-caption); letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--eng-purple-soft); font-weight: 700; }
.sub-head__title { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-h1); color: var(--eng-white); margin-top: 2px; }
.sub-head__price { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-h2); color: var(--eng-green); margin: 4px 0 8px; }
.sub-head__price span { font-family: var(--font-body); font-weight: 400; font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.6); }
.sub-switch { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.55); }
.sub-switch a { color: var(--eng-purple-soft); text-decoration: underline; text-underline-offset: 3px; }
.sub-step {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; margin-right: 8px; border-radius: 50%;
  background: color-mix(in srgb, var(--eng-purple) 30%, transparent);
  border: 1px solid color-mix(in srgb, var(--eng-purple) 60%, transparent);
  font-size: var(--fs-body-sm); color: var(--eng-white); vertical-align: -3px;
}
.sub-note { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.75); margin-bottom: 10px; }
.sub-note b { color: var(--eng-white); }
.sub-note--dim { color: rgba(255, 255, 255, 0.5); margin-bottom: 0; }
.sub-qr {
  display: block; width: min(300px, 80%); margin: 4px auto 0;
  border-radius: var(--radius-lg); border: 1px solid var(--hairline-dark);
  background: var(--eng-white); /* QR needs a light field to scan reliably */
}
.sub-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.sub-actions .eng-btn { flex: 1 1 200px; }

/* ---------- DX-2 responsive bento (tablet ≥768px / desktop ≥1024px) ----------
   Mobile keeps the single-column stack. Tablet pairs the four cards into a
   2-column grid; desktop becomes a centered bento (max ~1140px — balanced
   side margins, no infinite stretch on ultrawide):
     row 1 · identity hero, full width (identity left · CEFR + ring right)
     row 2 · [ Today's Session ~60% ]  [ Skill Profile ~40% ]
     row 3 · [ Streak 50% ]            [ Badges 50% ]
   Pure CSS on the existing source order — both dashboard states (engaged +
   cold-start) render every section, so grid placement is stable. */
@media (min-width: 768px) {
  .lrn-home__wrap {
    max-width: 880px;
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-items: stretch;
    gap: clamp(16px, 2.5vw, 24px);
  }
  .lrn-hero,
  .lrn-pass, /* UAT round 2: the flight pass is a full bento row */
  .lrn-reading, /* UAT round 3: reading card — full row (narrow-track trap) */
  .lrn-plan, /* CL-MEANING M1: the plan strip is a full bento row — never a narrow auto-placed track */
  .lrn-trend, /* DX-5 P1: a time series needs width, and auto-placing it wrapped the heading to four lines — same narrow-track trap */
  .lrn-home__notice, /* Item 8: trial-expiry banner — full bento row (narrow-track trap) */
  .lrn-goalcard, /* A2 goal hatch: same trap, found by measurement not by report —
     it was auto-placing into a 92px track at 1280/1440 (86px at 1024, where it
     also skewed the whole bento's track widths), wrapping Thai one to two
     characters per line over ten lines. `git log -S` says it NEVER had CSS: it
     shipped 5c92a6a (2026-08-01), six days AFTER .lrn-msgcard got this exact
     line at 1a7ff7b — the trap was already solved and documented three lines
     above, the card just landed after the group was written. A class with no
     rule anywhere is a promise of styling that no code keeps. */
  .lrn-home__foot { grid-column: 1 / -1; }
  /* hero spreads: greeting left · CEFR badge + level ring right */
  .lrn-hero__sub { margin-bottom: 0; }
  /* equal-height rows: CTA pinned to the card bottom, text block centered in
     the remaining space (the profile card is the tall row-mate — distribute
     the air instead of leaving one dead gap) */
  .lrn-today { display: flex; flex-direction: column; }
  .lrn-today__eyebrow { margin-top: auto; }
  .lrn-today .eng-btn { margin-top: auto; }
  .lrn-today__focus { margin-bottom: 18px; }
}
@media (min-width: 1024px) {
  .lrn-home__wrap { max-width: 1140px; grid-template-columns: repeat(10, 1fr); }
  .lrn-today   { grid-column: 1 / span 6; padding: clamp(28px, 3.2vw, 42px); }  /* HERO card — wider */
  .lrn-today__title { font-size: var(--fs-h1); }
  .lrn-today__focus { font-size: var(--fs-body-lg); margin-top: 8px; }
  .lrn-profile { grid-column: 7 / span 4; }
  .lrn-streak  { grid-column: 1 / span 5; }
  .lrn-badges  { grid-column: 6 / span 5; }
}

/* ---------- ACCESSIBILITY ---------- */
@media (prefers-reduced-motion: reduce) {
  .lrn-opt,
  .lrn-mood-chip,
  .lrn-audio__btn,
  .lrn-blank-input {
    transition-duration: 0.01ms;
  }
  /* D-2 arena enter/reveal animations off under reduced-motion */
  .lrn-q-anim,
  #q-feedback:not([hidden]) {
    animation-name: none;
  }
  /* DX-3.5 motion off under reduced-motion — the equalizer freezes at a quiet
     static height, the ambient glow stops drifting (stays a still tint). The
     GSAP choreography no-ops itself in quiz-celebrate.js. */
  .lrn-audio--playing .lrn-eq i { animation: none; transform: scaleY(0.5); }
  .lrn-arena::before { animation: none; }
  /* DX-3.6: key-term highlight stays (static underline via base background-size),
     just no draw-in / glow motion. */
  .lrn-arena .lrn-key { animation: none; }
  /* (ข) 2026-08-23 article page: scroll reveal resolves to "already revealed",
     the mark simply is, the structure card simply appears. Joining the EXISTING
     block rather than opening a second one — two blocks is how one of them
     silently stops covering what the other added.

     🚨 EVERY SELECTOR HERE IS PREFIXED `html`, AND THAT IS LOAD-BEARING.
     A media query adds NO specificity. This block sits at ~line 1380; the rules
     it overrides live at ~2310-2520 — over a thousand lines LATER, at exactly
     equal specificity — so plain selectors LOSE on source order and the whole
     group was silently inert. Measured, not reasoned: under
     prefers-reduced-motion a below-fold section still computed opacity 0 and
     translateY(10px), i.e. a reader who asked for less motion got all of it.
     The `html` prefix (+1 element) wins regardless of position.

     ⚠️ Do not "tidy" the prefix away. The rules read as redundant precisely
     because their effect is invisible when they work. */
  html.art-reveal-ready [data-reveal] { opacity: 1; transform: none; }
  html.art-reveal-ready [data-reveal].is-in { transition: none; }
  html .art-answer__mark { animation: none; }
  html #article-structure-card > * { animation: none; }
  html .art-vehicle__markwrap { transform: none; }
  /* ⭐ THE BAR GOES AWAY ENTIRELY, it does not merely stop transitioning. A
     scroll-linked indicator is motion in the periphery even when each step is
     instant, and the reader asked for stillness. article.js also returns early
     under this query, so this rule is the belt to that braces — and it is HERE,
     inside the existing block, rather than in a second @media beside the rules
     it overrides: a media query adds no specificity, so a later-in-file
     override would win on source order. The `html` prefix is what makes this
     block beat rules declared ~700 lines further down. (110) shipped four
     overrides that were inert for exactly that reason.) */
  html .art-progress { display: none; }
  /* (③ hub, 2026-08-29) The reveal is already covered — the two
     `[data-reveal]` rules above are attribute-keyed, not `.art-*`-keyed, so the
     hub's cards inherit them the moment its script adds the same class. What is
     NOT covered is what the hub added on top: a hover tint that transitions and
     an arrow that translates. A new `.ghub-*` rule inherits nothing from a
     block written for `.art-*`.

     🚨 `html` PREFIX, like every selector in this block. A media query adds no
     specificity and these rules sit ~1,750 lines before the .ghub-* block they
     override, so a plain selector would lose on source order and be silently
     inert — which is exactly what happened to four overrides at (110). The
     colour still changes on hover (that is information, not motion); it simply
     stops animating, and the arrow appears without travelling. */
  html .ghub-group { transition: none; }
  html .ghub-item::after { transition: none; transform: none; }
}

/* =================================================================
   M2-A — /pricing V2 dark-space redesign. 2026-07-10.
   Dark page + dark-glass tier cards (.lrn-card primitive reused) +
   the 3 tier mascots kept. Adult flagship = PURPLE sticker + GOLD
   edge (--eng-gold / --eng-gold-edge restrained-luxury tokens) — the
   old full-yellow field dropped per brand law (yellow = micro-accent,
   never a large field). Composes tokens + white-channel rgba only.
   ================================================================= */
.prc-page { flex: 1; }
.prc-wrap {
  width: 100%;
  max-width: 1140px;
  margin: 0 auto;
  padding: clamp(28px, 6vh, 64px) clamp(16px, 4vw, 32px);
}
.prc-head { text-align: center; margin-bottom: clamp(28px, 5vh, 48px); }
.prc-sub  { color: rgba(255, 255, 255, 0.75); }
.prc-dim  { color: rgba(255, 255, 255, 0.6); }

.prc-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(16px, 2.5vw, 24px);
  align-items: stretch;
}
@media (min-width: 768px) {
  /* ⚠️ WAS repeat(3, 1fr) — written for Student/Adult/Premium and left behind
     when FIX-B collapsed the product to two tiers (2026-07-26). The grid stayed
     centred while the CARDS sat left of centre, because the third track kept
     367px of the row at 1280px. auto-fit COLLAPSES a track with nothing in it,
     so the column count now follows the number of tiers rendered instead of a
     number written here — the same drift cannot come back if a tier is ever
     added or removed. justify-content centres what is left once the tracks stop
     growing at their max. */
  .prc-grid {
    /* max is 1fr, NOT 380px: auto-fit counts how many tracks fit using the
       MAXIMUM when it is definite, so minmax(260px, 380px) put ONE track in a
       756px container and stacked the cards at 768-820px. 1fr lets a track
       shrink, and the cap below stops two cards spreading to 526px wide on a
       desktop. */
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    /* The cap is DERIVED from the number of tiers the template rendered
       (--prc-tiers), so it cannot go stale the way repeat(3, 1fr) did. 380px is
       the card width the 3-tier layout used to produce; 24px is the gap ceiling
       in the clamp above. */
    max-width: calc(var(--prc-tiers, 3) * 380px + (var(--prc-tiers, 3) - 1) * 24px);
    margin-inline: auto;
  }
}

.prc-card { display: flex; flex-direction: column; }
.prc-card .eng-btn { margin-top: auto; }          /* CTA pinned to the card floor */
/* the tokens paint bare <p> with --fg-primary (ink) directly — price needs the
   explicit light color on dark (flagship gold rule below overrides) */
.prc-card .text-display-sm { color: var(--eng-white); }
.prc-feat li { color: rgba(255, 255, 255, 0.82); }
.prc-tick   { color: var(--eng-green); font-weight: 700; }

/* FIX-B5 two-tier badge design (Jeff-ratified 2026-07-26):
   Standard = "แนะนำ" flagship card — purple emphasis (sticker + glow + lift);
   the gold language moves to Premium (its landing crown treatment carried
   over) so gold stays the Premium-tier signature per the brand tokens. */
.prc-card--flagship {
  border: 1.5px solid color-mix(in srgb, var(--eng-purple) 65%, transparent);
  background: rgba(255, 255, 255, 0.07);
  box-shadow: 0 0 44px rgba(99, 53, 240, 0.3);
}
@media (min-width: 768px) {
  .prc-card--flagship { transform: scale(1.04); }
}

/* Premium — restrained luxury: gold hairline edge + gold price + crown tag;
   no color field, no scale (the "แนะนำ" lift belongs to Standard). */
.prc-card--premium {
  border: 1.5px solid var(--eng-gold-edge);
  background: rgba(255, 255, 255, 0.05);
}
.prc-card--premium .text-display-sm { color: var(--eng-gold); }
.prc-flag--gold {
  color: var(--eng-gold);
  border: 1px solid var(--eng-gold-edge);
  background: color-mix(in srgb, var(--eng-gold) 12%, transparent);
}

/* .prc-placeholder REMOVED 2026-08-01 — a rotated yellow "PLACEHOLDER ·
   PRE-LAUNCH" tag, in English, on the page where a visitor decides to pay
   ฿9,990. It rendered unconditionally: no flag, no guard. The test that pinned
   its presence now pins its ABSENCE — a test asserting a placeholder is there
   is what keeps the placeholder alive. */

/* ============================================================
   M2-B — LEGAL PAGES + IN-APP LEGAL LINKS (2026-07-10)
   Static policy pages (/privacy /terms /refund /ai-disclaimer
   /cookies) on the dark space surface, plus the quiet legal-links
   line on app surfaces (dashboard) and the subscribe refund note.
   Zero new hex: tokens + white-channel rgba only.
   NOTE: never write a star-slash pair inside this comment.
   ============================================================ */

.lgl-page { padding: clamp(32px, 6vh, 56px) 20px clamp(48px, 8vh, 80px); }
.lgl-wrap { max-width: 760px; margin: 0 auto; }
.lgl-meta { color: rgba(255, 255, 255, 0.55); font-size: var(--fs-body-sm, 14px); }

/* Article card rides .lrn-card (dark glass); prose rhythm inside. */
.lgl-card { padding: clamp(24px, 4vw, 40px); }
.lgl-body h2 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.125rem;
  color: var(--eng-white);
  margin: 28px 0 10px;
}
.lgl-body h2:first-child { margin-top: 0; }
.lgl-body p { color: rgba(255, 255, 255, 0.78); margin: 0 0 12px; line-height: 1.75; }
.lgl-body ul { list-style: disc; padding-left: 1.4rem; margin: 0 0 14px; }
.lgl-body li { color: rgba(255, 255, 255, 0.78); margin-bottom: 8px; line-height: 1.7; }
.lgl-body b { color: var(--eng-white); }
.lgl-body a {
  color: var(--eng-purple-soft);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.lgl-body a:hover { color: var(--eng-white); }

/* "ดูเพิ่มเติม" cross-links under the article */
.lgl-related { margin-top: 20px; text-align: center; font-size: 13px; color: rgba(255, 255, 255, 0.6); } /* audit:contrast — 0.45 was 4.48:1 */
.lgl-related a { color: rgba(255, 255, 255, 0.72); text-decoration: underline; text-underline-offset: 3px; }
.lgl-related a:hover { color: var(--eng-white); }

/* Quiet legal-links line on app surfaces (dashboard foot; Q1 PLUS —
   app pages hide the marketing footer, so the authed home carries the
   PDPA reachability line). */
/* One quiet dot-separated row (in-app footer redesign 2026-07-11): links
   are nowrap so Thai phrases never break mid-word — the flex row wraps
   BETWEEN items only (390 → max 2 centered lines); underline on hover only. */
.lrn-legal-links {
  margin-top: 18px;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  column-gap: 10px; row-gap: 4px;
  font-size: 12px;
}
.lrn-legal-links a {
  color: rgba(255, 255, 255, 0.62);
  text-decoration: none;
  white-space: nowrap;
}
.lrn-legal-links a:hover { color: var(--eng-white); text-decoration: underline; text-underline-offset: 3px; }
.lrn-legal-links__dot { color: rgba(255, 255, 255, 0.3); }

/* Subscribe refund disclosure link (rides .sub-note--dim tone) */
.sub-refund a { color: rgba(255, 255, 255, 0.65); text-decoration: underline; text-underline-offset: 3px; }
.sub-refund a:hover { color: var(--eng-white); }

/* M2-B signup consent checkbox (light surface — the signup page rides the
   default light layout; ink colors inherit). */
.sgn-consent { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; }
.sgn-consent input[type="checkbox"] {
  width: 18px;
  height: 18px;
  margin-top: 2px;
  flex-shrink: 0;
  accent-color: var(--eng-purple);
}

/* M2-B Q3: AI-content disclaimer line on the quiz IDLE screen (tiny, quiet). */
.lrn-ai-note { margin-top: 22px; font-size: 12px; color: rgba(255, 255, 255, 0.55); } /* audit:contrast — 0.35 was 3.12:1 */
.lrn-ai-note a { color: rgba(255, 255, 255, 0.7); text-decoration: underline; text-underline-offset: 3px; }
.lrn-ai-note a:hover { color: var(--eng-white); }

/* ============================================================
   DX-4 · First-run onboarding (2026-07-11) — one motivational
   interstitial after placement. V2 dark-space; the plan card is
   the screen's ONE loud element (goal-gradient, mirrors the
   dashboard's .lrn-today composition — tokens + brand-channel
   rgba only, zero new hex). GSAP staged reveal lives in
   /js/onboarding.js; SSR markup is the final state, so no
   opacity tricks here (reduced-motion is static by default).
   Note: never write a star-slash pair inside this comment. */
.lrn-onb {
  min-height: calc(100vh - 58px); /* under the thin app-bar */
  display: flex; align-items: center; justify-content: center;
  padding: clamp(24px, 6vw, 56px) 20px;
}
.lrn-onb__wrap { width: 100%; max-width: 480px; text-align: center; }

/* 1 · hero */
.lrn-onb__mascot {
  width: clamp(96px, 26vw, 132px); height: auto;
  filter: drop-shadow(0 0 28px rgba(57, 255, 137, 0.30));
  /* Tailwind preflight makes img block-level; text-align:center on the
     wrap cannot center a block box - explicit auto margins do. */
  margin: 0 auto 14px;
}
.lrn-onb__title {
  font-family: var(--font-display); font-weight: 800;
  font-size: var(--fs-h1); color: var(--eng-white);
  overflow-wrap: anywhere;
}
.lrn-onb__sub {
  margin-top: 12px; font-size: var(--fs-body-lg);
  color: rgba(255, 255, 255, 0.75);
  display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap;
}
.lrn-onb__cefr { min-width: 68px; padding: 6px 14px; }
.lrn-onb__cefr b { font-size: var(--fs-h2); }

/* 2 · the opportunity chip-card (quiet glass) */
.lrn-onb__focus {
  margin-top: 22px; padding: 16px 18px;
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-xl);
}
.lrn-onb__focus-label {
  font-size: var(--fs-caption); letter-spacing: var(--tracking-caps);
  text-transform: uppercase; font-weight: 700;
  color: var(--eng-green); margin-bottom: 6px;
}
.lrn-onb__focus-line { font-size: var(--fs-body-lg); color: rgba(255, 255, 255, 0.85); overflow-wrap: anywhere; }
.lrn-onb__focus-line b { color: var(--eng-white); font-weight: 700; }
.lrn-onb__arrow { color: var(--eng-green); font-weight: 700; }

/* 3 · the plan card — the ONE loud element (goal-gradient) */
.lrn-onb__plan {
  position: relative; overflow: hidden; text-align: center;
  margin-top: 18px; padding: clamp(22px, 5vw, 30px);
  border-radius: var(--radius-xl);
  background: linear-gradient(135deg, color-mix(in srgb, var(--eng-purple) 52%, var(--eng-space)) 0%, var(--eng-space) 72%);
  border: 1px solid color-mix(in srgb, var(--eng-purple) 40%, transparent);
  box-shadow: 0 0 48px rgba(99, 53, 240, 0.30);
}
.lrn-onb__plan-eyebrow { margin-bottom: 8px; }
/* (typography now comes from the shared .lrn-eyebrow instrument pill —
   Flight Deck Batch 4; only the spacing is local) */
.lrn-onb__plan-title { font-family: var(--font-display); font-weight: 800; font-size: var(--fs-h2); color: var(--eng-white); }
.lrn-onb__plan-sub { font-size: var(--fs-body); color: rgba(255, 255, 255, 0.85); margin: 6px 0 18px; }

/* 4 · quiet way home */
.lrn-onb__home { margin-top: 18px; font-size: var(--fs-body-sm); }
.lrn-onb__home a { color: rgba(255, 255, 255, 0.55); text-decoration: underline; text-underline-offset: 3px; }
.lrn-onb__home a:hover { color: var(--eng-white); }

/* =================================================================
   MISSION BRIEF (rebaseline CL-MISSION-1) — the lesson technique page.
   Space surface + glass cards (C2 standing exception: glass ONLY on
   space surfaces; recipe mirrors the arena question card). Tokens
   only — zero new hex. */
.msn-brief .eng-card {
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-xl);
  padding: clamp(16px, 3vw, 28px);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow: none;
}
/* Section labels: the token sheet paints bare .eng-small with --fg-primary
   (ink) — on the space surface that's 1.1:1 (the DX-4 "bare <p> rides token
   ink" class; caught by audit:contrast at this surface's first scan). */
.msn-brief .eng-small { color: var(--eng-white); }
/* Goal card = the one loud element on the brief (purple glow). */
.msn-brief .msn-goal { box-shadow: 0 0 48px rgba(99, 53, 240, 0.20); }
/* Patched coach copy carries meaningful newlines (short guidance lines). */
.msn-prose { white-space: pre-line; line-height: 1.75; }
.msn-vocab { display: flex; flex-wrap: wrap; gap: 8px; }
.msn-vocab__chip {
  padding: 4px 12px;
  border-radius: 999px;
  border: 1px solid var(--hairline-dark);
  background: color-mix(in srgb, var(--eng-purple) 22%, transparent);
  color: var(--eng-white);
  font-size: 0.9rem;
}
.msn-examples { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.msn-examples li {
  padding-left: 14px;
  border-left: 2px solid color-mix(in srgb, var(--eng-green) 55%, transparent);
}

/* =================================================================
   MISSION RESULT (rebaseline CL-MISSION-2) — the complete-screen block
   (3 faces: completed / needs_reinforcement / partial). Rides the dark
   celebration surface; cards are the same glass-on-space .eng-card the
   brief uses. Zero new hex — tokens + color-mix only. */
.mr { max-width: 560px; margin-left: auto; margin-right: auto; }
.mr .eng-card {
  background: color-mix(in srgb, var(--eng-white) 6%, transparent);
  border-color: var(--hairline-dark);
}
.mr .eng-small { color: var(--eng-white); }
.mr__score { color: var(--eng-white); opacity: 0.85; margin-bottom: 24px; }
.mr__chips { display: flex; flex-wrap: wrap; gap: 8px; }
.mr-chip {
  padding: 4px 12px;
  border-radius: 999px;
  border: 1px solid var(--hairline-dark);
  color: var(--eng-white);
  font-size: 0.9rem;
}
.mr-chip__state { white-space: nowrap; }
/* Fail face: the button row moved up under the review block (quiz.js). */
.lrn-arena__cta--inline { margin: 0 0 24px; }
/* Improved = the green moment (correct/action color law). */
.mr-chip--up {
  border-color: color-mix(in srgb, var(--eng-green) 55%, transparent);
  background: color-mix(in srgb, var(--eng-green) 14%, transparent);
}
.mr__reasons { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.mr__reasons li {
  color: var(--eng-white);
  opacity: 0.85;
  padding-left: 18px;
  position: relative;
}
.mr__reasons li::before { content: '✓'; position: absolute; left: 0; color: var(--eng-green); }

/* CL-MEANING M4 · the FAIL-face teach block — quiet card, warm teacher
   register (never red-field; wrongness reads as "ทวนตรงนี้"). Tokens only. */
.mr-teach__head { color: var(--eng-white); font-weight: 700; margin-bottom: 10px; }
.mr-teach__row { color: var(--eng-white); opacity: 0.85; margin-bottom: 6px; }
.mr-teach__line {
  color: var(--eng-white); opacity: 0.9; margin-top: 10px; padding: 10px 14px;
  border-left: 3px solid color-mix(in srgb, var(--eng-yellow) 70%, transparent);
  background: rgba(255, 255, 255, 0.05); border-radius: 0 10px 10px 0;
  font-size: var(--fs-body-sm);
}

/* ── Journey (CL-MISSION-3): the mission map — dark app surface, quiet
   hairline groups, native <details> collapse (zero JS). The one loud
   element = the next-mission hero card. Zero new hex — tokens + color-mix. */
.jny .eng-card {
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-xl);
  padding: clamp(16px, 3vw, 28px);
}
.jny .eng-small { color: var(--eng-white); }
.jny-next { box-shadow: 0 0 48px rgba(99, 53, 240, 0.20); text-align: center; }
.jny-next__title { font-family: var(--font-display); font-weight: 700; font-size: 1.35rem; color: var(--eng-white); margin-bottom: 4px; }

.jny-vert__head {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 16px; margin-bottom: 12px;
  border: 1px solid var(--hairline-dark); border-radius: var(--radius-lg);
  background: linear-gradient(135deg, color-mix(in srgb, var(--eng-purple) 26%, var(--eng-space)) 0%, var(--eng-space) 78%);
}
.jny-vert__banner { width: 56px; height: 56px; border-radius: 14px; object-fit: cover; border: 1px solid var(--hairline-dark); }
.jny-vert__persona { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; margin-left: auto; border: 1px solid var(--hairline-dark); }
.jny-vert__name { font-family: var(--font-display); font-weight: 700; font-size: 1.15rem; color: var(--eng-white); }
.jny-vert__id .eng-small { color: var(--eng-white); }

.jny-path {
  border: 1px solid var(--hairline-dark); border-radius: var(--radius-lg);
  margin-bottom: 10px; background: rgba(255, 255, 255, 0.03);
}
.jny-path__sum {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px; cursor: pointer; list-style: none;
  color: var(--eng-white); font-weight: 600;
}
.jny-path__sum::-webkit-details-marker { display: none; }
.jny-path__sum::after { content: '▾'; margin-left: 6px; opacity: 0.5; transition: transform 0.2s; }
.jny-path[open] > .jny-path__sum::after { transform: rotate(180deg); }
.jny-path__art { width: 34px; height: 34px; border-radius: 10px; object-fit: cover; border: 1px solid var(--hairline-dark); }
.jny-path__label { flex: 1; }
.jny-path__count {
  font-size: 0.8rem; font-weight: 600; padding: 2px 10px; border-radius: 999px;
  border: 1px solid var(--hairline-dark);
  background: color-mix(in srgb, var(--eng-purple) 22%, transparent);
  color: var(--eng-white);
}

.jny-list { list-style: none; margin: 0; padding: 4px 8px 10px; display: flex; flex-direction: column; gap: 4px; }
.jny-lesson {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 12px; border-radius: var(--radius-md);
  color: var(--eng-white); text-decoration: none;
  border: 1px solid transparent;
}
.jny-lesson:hover { border-color: var(--hairline-dark); background: color-mix(in srgb, var(--eng-purple) 14%, transparent); }
.jny-lesson__title { flex: 1; min-width: 0; font-size: 0.95rem; line-height: 1.4; }
.jny-lesson__meta { display: flex; align-items: center; gap: 8px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
/* CEFR chip on the dark surface — the ghosted-purple tag idiom (the arena's
   q-number chip precedent); the token default is yellow-bg/ink for light
   surfaces and my first cut forced white ON that yellow (1.17:1 — caught by
   the N7 gate on its first journey scan). */
.jny-lesson__meta .eng-tag {
  /* OPAQUE mix (purple into space, not transparent): the chip's contrast is
     then its own — independent of whatever art/gradient sits underneath. */
  background: color-mix(in srgb, var(--eng-purple) 30%, var(--eng-space));
  color: var(--eng-purple-soft);
  border: 1px solid color-mix(in srgb, var(--eng-purple) 50%, transparent);
}
.jny-lesson__skill { font-size: 0.78rem; color: var(--eng-white); }
/* DX-5 locked preview. Deliberately the OPPOSITE of the badge shelf's locked
   state and both are right: a badge's value is the earning, so its art is
   withheld; a lesson's value is its content, so the title, level and skill stay
   fully legible and only the doing is withheld. Nothing here is greyed to the
   point of illegibility — that would withhold the very thing that motivates. */
.jny-lesson--locked { cursor: default; }
.jny-lesson--locked .jny-lesson__title { color: rgba(255, 255, 255, 0.82); }
.jny-lesson__lock { display: inline-flex; color: rgba(255, 255, 255, 0.45); }
.jny-lesson__lock svg { width: 15px; height: 15px; }
/* One quiet standing note above the map, not 400 individual walls. */
.jny-lockednote {
  text-align: center; margin: -20px 0 26px;
  color: rgba(255, 255, 255, 0.62);
}
.jny-state { font-size: 0.75rem; font-weight: 600; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--hairline-dark); }
.jny-state--done {
  color: var(--eng-green);
  border-color: color-mix(in srgb, var(--eng-green) 55%, transparent);
  background: color-mix(in srgb, var(--eng-green) 14%, transparent);
}
.jny-state--retry {
  color: var(--eng-yellow);
  border-color: color-mix(in srgb, var(--eng-yellow) 45%, transparent);
  background: color-mix(in srgb, var(--eng-yellow) 12%, transparent);
}
.jny-state--open { color: var(--eng-white); background: color-mix(in srgb, var(--eng-white) 10%, transparent); }

@media (max-width: 640px) {
  .jny-lesson { flex-direction: column; align-items: flex-start; gap: 6px; }
  .jny-lesson__meta { justify-content: flex-start; }
}

/* Home mission-control secondary link (Today card) */
.lrn-today__all {
  display: inline-block; margin-top: 12px;
  color: var(--eng-white); opacity: 0.75; font-size: 0.9rem; text-decoration: underline;
  text-underline-offset: 3px;
}
.lrn-today__all:hover { opacity: 1; }

/* EARNED: the award as drawn — full colour, its own glow, a solid green ring,
   and a stated "ได้รับแล้ว". Differs from locked in kind on every axis. */
.lrn-badge--earned .lrn-badge__art img { filter: none; }
.lrn-badge--earned .lrn-badge__art {
  border-style: solid;
  border-color: color-mix(in srgb, var(--eng-green) 55%, transparent);
  box-shadow: 0 0 14px color-mix(in srgb, var(--eng-green) 30%, transparent);
}
.lrn-badge--earned .lrn-badge__name { color: var(--eng-white); }

/* Result-screen badge toast (mission result card) */
.mr-badges { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.mr-badges[hidden] { display: none; }  /* display:flex would beat the UA [hidden] rule */
.mr-badge {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; border-radius: var(--radius-md);
  border: 1px solid color-mix(in srgb, var(--eng-green) 45%, transparent);
  background: color-mix(in srgb, var(--eng-green) 12%, transparent);
  color: var(--eng-white);
}
.mr-badge b { color: var(--eng-green); }

/* ══ CL-LESSON — card flow + generic visual renderer + guided (2026-07-18) ══
   L-P1′ §6.2-D card-by-card learning flow (brief.ejs + lesson-flow.js) +
   L-P2′ A3 generic 4-type visual (contrast/blocks/ladder/timeline) +
   L-P3 guided scaffold + misconception line + A10 technique ref + T1 mastery.
   Tokens only (zero-new-hex); motion = CSS transitions, reduced-motion-guarded
   at the bottom of this block. NOTE: never star-slash inside CSS comments
   (the P-1 CSS-comment early-termination ledger class). */

/* D2 step 2: no min-height — a 380px floor stretched short cards (checklist,
   trap) into half-empty boxes and pushed the Next row off the phone screen.
   Cards size to content; the nav follows directly under each one. */
.lf-stage { display: flex; align-items: stretch; }
.lf-card {
  position: relative;                     /* anchors the HUD corner brackets */
  flex: 1; padding: 28px 22px;
  border: 1.5px solid rgba(255, 255, 255, 0.10);
  border-radius: 18px;
  background: color-mix(in srgb, var(--eng-white) 6%, transparent);
  backdrop-filter: blur(6px);
}
/* FD HUD corner brackets (UAT round 2) — every flow card is an instrument
   panel: two L-corners in the mission's skill hue. Decorative (aria-free,
   pointer-events none); identity carried by silhouette + hue, not text. */
.lf-card::before,
.lf-card::after {
  content: "";
  position: absolute;
  width: 22px; height: 22px;
  border: 2px solid color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 65%, transparent);
  pointer-events: none;
}
.lf-card::before { top: -2px; left: -2px; border-right: 0; border-bottom: 0; border-top-left-radius: 18px; }
.lf-card::after { bottom: -2px; right: -2px; border-left: 0; border-top: 0; border-bottom-right-radius: 18px; }
.lf-enter::before, .lf-enter::after,
.lf-enter-back::before, .lf-enter-back::after {
  animation: lf-corner-in 0.5s var(--ease-out) both;
}
@keyframes lf-corner-in { from { opacity: 0; transform: scale(0.4); } to { opacity: 1; transform: none; } }
.lf-card::before { transform-origin: top left; }
.lf-card::after { transform-origin: bottom right; }
/* FD: the eyebrow is an instrument readout, not prose — mono + tracking
   (the emoji each card carries is its marker; no extra blip needed). */
.lf-card__eyebrow {
  font-family: var(--font-mono);
  font-size: 0.78rem;                     /* UAT-2 legibility: 0.72 → 0.78 */
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--eng-white);
  opacity: 0.78;                          /* UAT-2 legibility: 0.62 → 0.78 */
  margin-bottom: 10px;
}
.lf-card__title { font-family: var(--font-display); font-weight: 700; font-size: 1.45rem; color: var(--eng-white); margin-bottom: 10px; }
.lf-card__sub { color: var(--eng-white); opacity: 0.92; margin-bottom: 14px; }
.lf-card__body--lg { font-size: 1.1rem; line-height: 1.75; color: var(--eng-white); white-space: pre-line; }

/* enter motion (CSS-only; GSAP not loaded on the brief page) */
.lf-enter { animation: lf-in 0.34s cubic-bezier(0.22, 1, 0.36, 1); }
.lf-enter-back { animation: lf-in-back 0.3s cubic-bezier(0.22, 1, 0.36, 1); }
@keyframes lf-in { from { opacity: 0; transform: translateX(26px); } to { opacity: 1; transform: none; } }
@keyframes lf-in-back { from { opacity: 0; transform: translateX(-26px); } to { opacity: 1; transform: none; } }

.lf-nav { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 18px; }
/* FD CTA (UAT round 2): the armed next-button glows green like a GO light;
   a GATED next-button reads as a locked switch (dim + dashed), so "why can't
   I press this" is answered by the control itself + the gold gate hint. */
.lf-nav .eng-btn--primary:not(:disabled) {
  box-shadow: 0 0 18px color-mix(in srgb, var(--eng-green) 38%, transparent);
  animation: lf-go-pulse 2.6s var(--ease-in-out) infinite;
}
.lf-nav .eng-btn--primary:not(:disabled):hover { animation-play-state: paused; }
@keyframes lf-go-pulse {
  0%, 100% { box-shadow: 0 0 12px color-mix(in srgb, var(--eng-green) 28%, transparent); }
  50% { box-shadow: 0 0 24px color-mix(in srgb, var(--eng-green) 52%, transparent); }
}
.lf-nav .eng-btn--primary:disabled {
  opacity: 0.5;
  border: 1.5px dashed color-mix(in srgb, var(--eng-gold) 55%, transparent);
  box-shadow: none;
  cursor: not-allowed;
}
.lf-dots { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; }
/* console ticks, not dots (UAT round 2): rectangular indicator lights */
.lf-dot { width: 14px; height: 5px; border-radius: 2px; background: rgba(255, 255, 255, 0.18); transition: background 0.2s, transform 0.2s, box-shadow 0.2s; }
.lf-dot.is-done { background: color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 75%, white 10%); }
.lf-dot.is-now { background: var(--eng-green); transform: scaleY(1.6); box-shadow: 0 0 8px color-mix(in srgb, var(--eng-green) 60%, transparent); }

.lf-reasons li { margin-bottom: 4px; }
.lf-meta { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 6px; }
.lf-meta__chip {
  font-size: 0.85rem; color: var(--eng-white);
  border: 1.5px solid rgba(255, 255, 255, 0.10);
  border-radius: 999px; padding: 4px 12px;
  background: color-mix(in srgb, var(--eng-white) 5%, transparent);
}

/* predict-reveal (A5: hook tease + mistake reveal) */
.lf-predict { margin-top: 14px; }
.lf-predict__ask { font-weight: 700; color: var(--eng-white); margin-bottom: 8px; }
.lf-predict__stim {
  font-size: 1.05rem; color: var(--eng-white);
  border: 1.5px solid color-mix(in srgb, var(--eng-red) 45%, transparent);
  background: color-mix(in srgb, var(--eng-red) 10%, transparent);
  border-radius: 12px; padding: 12px 14px; margin-bottom: 12px;
}
.lf-predict__btn { margin-bottom: 8px; }
.lf-predict__tease { color: var(--eng-white); opacity: 0.9; }
.lf-predict__reveal { animation: lf-in 0.3s ease; }
.lf-mistake__why { color: var(--eng-white); opacity: 0.9; margin-bottom: 10px; }
.lf-mistake__pair { display: grid; gap: 8px; margin-bottom: 10px; }
.lf-mistake__wrong {
  color: var(--eng-white); text-decoration: line-through;
  text-decoration-color: var(--eng-red); opacity: 0.75;
  padding: 8px 12px; border-radius: 10px;
  background: color-mix(in srgb, var(--eng-red) 8%, transparent);
}
.lf-mistake__right {
  color: var(--eng-white); font-weight: 700;
  padding: 8px 12px; border-radius: 10px;
  border: 1.5px solid color-mix(in srgb, var(--eng-green) 45%, transparent);
  background: color-mix(in srgb, var(--eng-green) 10%, transparent);
}
.lf-mistake__cue { color: var(--eng-white); opacity: 0.95; }

.lf-examples__item {
  color: var(--eng-white); font-size: 1.05rem; line-height: 1.7;
  padding: 10px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.lf-examples__item:last-child { border-bottom: none; }

/* ── generic visual renderer (A3) ── */
.lf-visual__title { font-family: var(--font-display); font-weight: 700; color: var(--eng-white); margin-bottom: 12px; text-align: center; }
.lf-visual__caption { font-size: 0.9rem; color: var(--eng-white); opacity: 0.8; margin-top: 12px; text-align: center; }

.lf-vc { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.lf-vc__col { border-radius: 14px; padding: 12px; }
.lf-vc__col--left { border: 1.5px solid color-mix(in srgb, var(--eng-purple) 55%, transparent); background: color-mix(in srgb, var(--eng-purple) 12%, transparent); }
.lf-vc__col--right { border: 1.5px solid color-mix(in srgb, var(--eng-gold) 55%, transparent); background: color-mix(in srgb, var(--eng-gold) 10%, transparent); }
.lf-vc__label { font-weight: 800; color: var(--eng-white); margin-bottom: 8px; text-align: center; }
.lf-vc__item { color: var(--eng-white); font-size: 0.95rem; padding: 4px 0; border-top: 1px solid rgba(255, 255, 255, 0.08); }
.lf-vc__item:first-of-type { border-top: none; }

.lf-vb { display: flex; flex-wrap: wrap; align-items: stretch; gap: 8px; justify-content: center; }
.lf-vb__seg { border: 1.5px solid color-mix(in srgb, var(--eng-purple) 55%, transparent); background: color-mix(in srgb, var(--eng-purple) 12%, transparent); border-radius: 12px; padding: 10px 12px; min-width: 90px; text-align: center; }
.lf-vb__label { font-size: 0.78rem; opacity: 0.8; color: var(--eng-white); margin-bottom: 4px; }
.lf-vb__tokens { font-weight: 700; color: var(--eng-white); }
.lf-vb__plus { align-self: center; color: var(--eng-white); opacity: 0.6; font-weight: 700; }

.lf-vl { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
.lf-vl__step { display: flex; gap: 10px; align-items: flex-start; color: var(--eng-white); }
.lf-vl__num {
  flex: none; width: 26px; height: 26px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 0.85rem; color: var(--eng-ink);
  background: var(--eng-green);
}
.lf-vl__body b { color: var(--eng-white); }

.lf-vt { position: relative; padding-left: 18px; display: grid; gap: 12px; }
.lf-vt__line { position: absolute; left: 6px; top: 6px; bottom: 6px; width: 2px; background: color-mix(in srgb, var(--eng-purple) 60%, white 10%); }
.lf-vt__point { position: relative; color: var(--eng-white); }
.lf-vt__point::before { content: ''; position: absolute; left: -16.5px; top: 6px; width: 9px; height: 9px; border-radius: 999px; background: var(--eng-green); }
.lf-vt__marker { font-weight: 800; margin-right: 8px; color: var(--eng-white); }
.lf-vt__label { opacity: 0.95; }
.lf-vt__example { display: block; font-size: 0.9rem; opacity: 0.75; margin-top: 2px; }

/* ── quiz-side: guided + misconception + technique ref + overlay ── */
.lrn-misconception {
  margin-top: 12px; padding: 10px 14px; border-radius: 12px;
  color: var(--eng-white);
  border: 1.5px solid color-mix(in srgb, var(--eng-yellow) 30%, transparent);
  background: color-mix(in srgb, var(--eng-yellow) 7%, transparent);
}
.lrn-technique-ref {
  display: inline-block; margin-top: 10px; padding: 8px 14px;
  border-radius: 999px; cursor: pointer;
  font-size: 0.9rem; color: var(--eng-white);
  border: 1.5px solid rgba(255, 255, 255, 0.14);
  background: color-mix(in srgb, var(--eng-purple) 14%, transparent);
  transition: background 0.15s;
}
.lrn-technique-ref:hover { background: color-mix(in srgb, var(--eng-purple) 26%, transparent); }
/* an author display beats the UA [hidden] rule — without this guard the
   button rendered as an EMPTY clickable pill on every practice-session
   answered state (quiz.js sets hidden=true; the CSS ignored it) */
.lrn-technique-ref[hidden] { display: none; }
.lrn-tech-overlay {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  padding: 18px; background: color-mix(in srgb, var(--eng-space) 82%, transparent);
}
/* LC-1 [hidden]-leak ledger class: class display BEATS the UA hidden rule —
   without this guard the invisible fixed overlay swallows every click on the
   page (caught live by the CL-LESSON smoke, 3rd recurrence of the class). */
.lrn-tech-overlay[hidden] { display: none; }
.lrn-tech-overlay__panel {
  max-width: 560px; width: 100%; max-height: 82vh; overflow-y: auto;
  border-radius: 18px; padding: 22px;
  border: 1.5px solid rgba(255, 255, 255, 0.14);
  background: var(--eng-space);
}
.lrn-tech-card { padding: 12px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.08); }
.lrn-tech-card:last-child { border-bottom: none; }
.lrn-tech-card__heading { font-weight: 800; color: var(--eng-white); margin-bottom: 4px; }
.lrn-tech-card__body { color: var(--eng-white); opacity: 0.9; line-height: 1.7; white-space: pre-line; }

/* ── CL-LESSON Part A: interactive cards (advisor GO 2026-07-19) ────────── */
/* progress: "การ์ด N/M" + thin bar (replaces dots — mobile-legible, P3) */
.lf-progress { display: flex; flex-direction: column; align-items: center; gap: 5px; flex: 1; min-width: 0; }
.lf-progress__label { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 0.8rem; letter-spacing: 0.08em; color: var(--eng-white); opacity: 0.85; }
.lf-progress__track { display: block; width: min(180px, 100%); height: 4px; border-radius: 999px; background: rgba(255, 255, 255, 0.14); overflow: hidden; }
.lf-progress__bar { display: block; height: 100%; width: 0; border-radius: 999px; background: var(--eng-green); transition: width 0.3s ease; }
/* FD: the gate hint is a quiet amber instrument line — it states what the
   quest needs before the flow moves on, in the console's voice. */
.lf-gate-hint {
  text-align: center;
  margin-top: 10px;
  font-size: 0.92rem;                     /* UAT-2 legibility */
  font-weight: 600;
  color: var(--eng-gold);
  opacity: 1;
}
.lf-gate-hint[hidden] { display: none; }

/* hook A/B choice */
.lf-choice { display: grid; gap: 10px; margin: 12px 0; }
.lf-choice__btn {
  text-align: left; font-size: 1.05rem; color: var(--eng-white);
  background: rgba(255, 255, 255, 0.05); border: 1.5px solid rgba(255, 255, 255, 0.16);
  border-radius: 14px; padding: 14px 16px; cursor: pointer;
  transition: border-color 0.18s, background 0.18s, transform 0.18s;
}
.lf-choice__btn:hover:not(:disabled) { border-color: color-mix(in srgb, var(--eng-purple) 70%, white 12%); }
.lf-choice__btn:disabled { cursor: default; opacity: 0.55; }
.lf-choice__btn.is-picked {
  opacity: 1; border-color: var(--eng-purple);
  background: color-mix(in srgb, var(--eng-purple) 18%, transparent);
  box-shadow: 0 0 14px color-mix(in srgb, var(--eng-purple) 40%, transparent);
}

/* techniques accordion */
/* ── PRE-FLIGHT CHECKLIST (techniques card; UAT round 1, 2026-09-02) ────
   The technique ideas are the pre-flight check before the craft launches:
   a vertical rail of numbered nodes — tap a step to open its panel and the
   node lights ✓ green; the NEXT unchecked node breathes (state class from
   lesson-flow*.js; CSS moves it). Body lines render structured: "เช่น …"
   becomes a specimen chip row. Replaces the lf-acc accordion. */
.lf-chk__counter {
  font-family: var(--font-mono);
  font-size: 0.8rem;                      /* UAT-2 legibility */
  letter-spacing: 0.08em;
  color: rgba(255, 255, 255, 0.85);
  margin: 0 0 12px;
}
.lf-chk { list-style: none; margin: 0; padding: 0; text-align: left; }
.lf-chk__step { position: relative; padding-bottom: 4px; }
.lf-chk__step::before {
  content: "";
  position: absolute;
  left: 17px;
  top: 44px;
  bottom: -4px;
  width: 2px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 40%, transparent),
    var(--hairline-dark));
}
.lf-chk__step:last-child::before { display: none; }
.lf-chk__head {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px 0;
  text-align: left;
}
.lf-chk__node {
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1.5px dashed var(--hairline-dark);
  background: rgba(255, 255, 255, 0.04);
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  font-weight: 700;
  color: rgba(255, 255, 255, 0.8);
}
.lf-chk__step.is-checked .lf-chk__node {
  border-style: solid;
  border-color: color-mix(in srgb, var(--eng-green) 65%, transparent);
  background: color-mix(in srgb, var(--eng-green) 12%, transparent);
  color: var(--eng-green);
}
@keyframes lf-chk-breathe {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 0%, transparent); border-color: var(--hairline-dark); }
  50% { box-shadow: 0 0 10px 0 color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 40%, transparent); border-color: color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 70%, transparent); }
}
.lf-chk__step.is-next:not(.is-checked) .lf-chk__node { animation: lf-chk-breathe 2.2s ease-in-out infinite; }
.lf-chk__head-label { font-weight: 700; color: var(--eng-white); line-height: 1.4; }
.lf-chk__body {
  margin: 0 0 10px 48px;
  padding: 10px 14px;
  border-left: 2px solid color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 45%, transparent);
  background: rgba(255, 255, 255, 0.04);
  border-radius: 0 12px 12px 0;
}
.lf-chk__line {
  font-size: 1.02rem;                     /* UAT-2 legibility */
  font-weight: 500;
  line-height: 1.6;
  color: rgba(255, 255, 255, 0.96);
  margin: 0 0 6px;
}
.lf-chk__spec {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 4px 0 6px;
}
.lf-chk__spec-label {
  font-size: 0.78rem;
  color: var(--eng-gold);
  font-weight: 700;
}
.lf-chk__spec-chip {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--eng-white);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-pill);
  padding: 2px 10px;
}

/* interactive visual (chip→zone + ordered) */
/* FD: the ask is the quest's instruction pill (mirrors the arena's
   .lrn-instruction) — one glance says what ACTION this card wants. */
.lf-iv__ask {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 13px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-pill);
  font-size: var(--fs-body-sm);
  font-weight: 700;
  color: var(--eng-white);
  margin-bottom: 12px;
}
.lf-iv__pool { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 14px; }
.lf-iv__chip {
  font-size: 0.95rem; color: var(--eng-white); cursor: pointer;
  background: rgba(255, 255, 255, 0.06); border: 1.5px solid rgba(255, 255, 255, 0.2);
  border-radius: 999px; padding: 8px 14px; transition: border-color 0.15s, background 0.15s;
}
.lf-iv__chip.is-armed {
  border-color: var(--eng-green);
  background: color-mix(in srgb, var(--eng-green) 14%, transparent);
  box-shadow: 0 0 12px color-mix(in srgb, var(--eng-green) 35%, transparent);
}
.lf-iv__zone { cursor: pointer; }
.lf-iv__zone-list { display: grid; gap: 4px; min-height: 30px; }

/* Tier-1 (flagship-proven): chips drag onto zones; correct zone glows.
   touch-action none = pointer drag on touch; swipe is suppressed on chips. */
.lf-iv__chip { touch-action: none; user-select: none; -webkit-user-select: none; }
.lf-iv__chip.is-dragging {
  cursor: grabbing;
  position: relative;
  z-index: 30;
  border-color: var(--eng-purple);
  box-shadow: 0 8px 24px color-mix(in srgb, var(--eng-space) 70%, transparent);
}
.lf-iv__zone.is-glow {
  animation: lf-zone-glow 0.9s var(--ease-out) forwards;
}
@keyframes lf-zone-glow {
  from { box-shadow: 0 0 0 1.5px var(--eng-green), 0 0 22px color-mix(in srgb, var(--eng-green) 45%, transparent); }
  to { box-shadow: 0 0 0 0 transparent; }
}

/* Tier-1 techmatch: term ↔ meaning text tiles (generic, parser-driven) */
.lf-tm__grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 12px; }
.lf-tm__tile {
  appearance: none;
  border: 1.5px dashed rgba(255, 255, 255, 0.2);
  border-radius: 14px;
  background: color-mix(in srgb, var(--eng-purple) 10%, transparent);
  color: var(--eng-white);
  padding: 12px 10px;
  cursor: pointer;
  text-align: center;
  transition: border-color 0.15s;
}
.lf-tm__tile:not(.is-done):hover { border-color: color-mix(in srgb, var(--eng-purple) 55%, transparent); }
.lf-tm__tile.is-done {
  border-style: solid;
  border-color: color-mix(in srgb, var(--eng-green) 55%, transparent);
  cursor: default;
}
.lf-tm__word { font-family: var(--font-display); font-weight: 700; font-size: 1.05rem; opacity: 0.55; }
.lf-tm__tile.is-done .lf-tm__word { opacity: 1; color: var(--eng-yellow); }
.lf-tm__label { font-size: 0.9rem; opacity: 0.9; margin-top: 2px; }
.lf-vb--zones { align-items: stretch; }
.lf-vb--zones .lf-vb__seg { min-width: 120px; }
.lf-iv.is-complete .lf-iv__ask { opacity: 0.55; }

/* ── FD drag affordance ─────────────────────────────────────────────────
   Answers the walk finding "quest นี้ต้องการให้ action อะไร":
   1. IDLE NUDGE — until the learner first touches the box (.is-touched,
      set once by lesson-flow.js), the first chip breathes gently: "start
      here".
   2. GHOST DEMO — on drag surfaces only (.lf-iv--dnd = buildChipZones), a
      faint dashed ✋ pill rises from the pool toward the zones on repeat:
      "chips travel up into the boxes". It asserts NO answer (no word, no
      target) — an instrument must not lie about which chip goes where.
   3. ARMED TARGETS — while a chip is armed (.has-armed), every valid drop
      zone pulses a dashed green ring: "now put it here".
   All three are state-driven (CSS moves it, JS only flips classes) and all
   animation dies in the reduced-motion tail; the armed ring keeps a static
   green border there so the affordance survives without motion. */
@keyframes lf-affordance-nudge {
  0%, 100% { transform: translateY(0); box-shadow: 0 0 0 0 rgba(57, 255, 137, 0); }
  50% { transform: translateY(-3px); box-shadow: 0 0 10px 0 rgba(57, 255, 137, 0.22); }
}
.lf-iv--dnd:not(.is-touched):not(.is-complete) .lf-iv__pool .lf-iv__chip:first-child,
.lf-tm:not(.is-touched):not(.is-complete) .lf-iv__pool .lf-iv__chip:first-child {
  animation: lf-affordance-nudge 2.2s ease-in-out infinite;
}
@keyframes lf-affordance-target {
  0%, 100% { border-color: rgba(57, 255, 137, 0.35); box-shadow: 0 0 0 0 rgba(57, 255, 137, 0); }
  50% { border-color: rgba(57, 255, 137, 0.85); box-shadow: 0 0 12px 0 rgba(57, 255, 137, 0.2); }
}
.lf-iv.has-armed .lf-iv__zone {
  border-style: dashed;
  animation: lf-affordance-target 1.4s ease-in-out infinite;
}
.lf-tm.has-armed .lf-tm__tile:not(.is-done) {
  animation: lf-affordance-target 1.4s ease-in-out infinite;
}
@keyframes lf-affordance-ghost {
  0% { opacity: 0; transform: translate(-50%, 0); }
  18% { opacity: 0.5; }
  70% { opacity: 0.5; }
  100% { opacity: 0; transform: translate(-50%, -84px); }
}
.lf-iv--dnd .lf-iv__pool { position: relative; }
.lf-iv--dnd:not(.is-touched):not(.is-complete) .lf-iv__pool::after {
  content: '✋';
  position: absolute;
  left: 50%;
  top: -6px;
  padding: 3px 10px;
  border: 1.5px dashed rgba(255, 255, 255, 0.45);
  border-radius: var(--radius-pill);
  font-size: 0.8rem;
  line-height: 1.4;
  pointer-events: none;
  opacity: 0;
  animation: lf-affordance-ghost 3.6s ease-in-out infinite;
}
.lf-vl__step--empty { opacity: 0.45; }
.lf-vl__step--empty .lf-vl__body { font-style: italic; }

/* mistake resolution */
.lf-mistake__picked { color: var(--eng-white); opacity: 0.85; margin-bottom: 10px; }
.lf-mistake__verdict { font-family: var(--font-display); font-weight: 700; font-size: 1.1rem; color: var(--eng-white); margin-bottom: 10px; }

/* go card examples fold-in */
.lf-go-examples { text-align: left; margin: 14px 0 18px; }

/* mastery self-check (result page) */
.ms-choice { display: flex; gap: 10px; flex-wrap: wrap; }

@media (max-width: 480px) {
  .lf-vc { grid-template-columns: 1fr; }
  .lf-card { padding: 20px 16px; }
  .lf-progress__track { width: 120px; }
}
/* (this section's reduced-motion overrides live in the FLIGHT DECK tail at
   the BOTTOM of this file — one block per concern-group maximum, and moving
   an override LATER in source can only strengthen it at a specificity tie.
   The grammar-article pin counts the file's blocks: exactly three.) */

/* ── SW-3 · ข้อความ (learner↔coach messaging) ──────────────────────────────
   Dark app surface (dashboard's space language). Bubbles: coach = quiet
   glass on the left, learner = purple on the right. Tokens only. */
/* Messages rides .lrn-home for the star-field sky, but must NOT inherit the
   dashboard's bento grid — at >=768px .lrn-home__wrap becomes a multi-track
   grid and a lone card lands in ONE track (~114px at 1140/10; the UAT-2
   "จอแคบ" screenshot). Opt back out at every width: single column, honest cap.
   Source order beats the media-query rules (same specificity, later sheet
   position), so no per-breakpoint overrides are needed. */
.lrn-msgs__wrap { max-width: 720px; display: flex; flex-direction: column; grid-template-columns: none; }
.lrn-msgs__list { list-style: none; margin: 14px 0 4px; padding: 0; display: flex; flex-direction: column; gap: 12px; max-height: 56vh; overflow-y: auto; }
.lrn-msg { display: flex; flex-direction: column; align-items: flex-start; max-width: 85%; }
.lrn-msg--mine { align-self: flex-end; align-items: flex-end; }
.lrn-msg__bubble {
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--hairline-dark);
  border-radius: 14px 14px 14px 4px;
  padding: 10px 14px;
  font-size: var(--fs-body-sm);
  line-height: 1.55;
  color: var(--eng-white);
  overflow-wrap: anywhere;
}
.lrn-msg--mine .lrn-msg__bubble {
  background: color-mix(in srgb, var(--eng-purple) 38%, transparent);
  border-color: color-mix(in srgb, var(--eng-purple) 55%, transparent);
  border-radius: 14px 14px 4px 14px;
}
.lrn-msg__meta { margin-top: 4px; font-size: 0.72rem; color: rgba(255, 255, 255, 0.55); }
.lrn-msgs__compose { margin-top: 16px; display: flex; flex-direction: column; gap: 10px; }
.lrn-msgs__input {
  width: 100%;
  background: rgba(255, 255, 255, 0.06);
  border: 1.5px solid var(--hairline-dark);
  border-radius: var(--radius-md, 10px);
  padding: 10px 12px;
  color: var(--eng-white);
  font-family: inherit;
  font-size: var(--fs-body-sm);
  line-height: 1.5;
  resize: vertical;
}
.lrn-msgs__input::placeholder { color: rgba(255, 255, 255, 0.45); }
.lrn-msgs__input:focus { outline: 2px solid var(--eng-purple); outline-offset: 1px; }
.lrn-msgs__actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; flex-wrap: wrap; }
.lrn-msgs__hint { margin-top: 6px; }
.lrn-msgs__empty p { margin: 0 0 6px; }
.lrn-msgs__disclaimer { margin-top: 14px; font-size: 0.72rem; color: rgba(255, 255, 255, 0.55); }

/* SW-3 dashboard messages card (quiet chip-row; full-width bento track like
   the plan strip — never a narrow auto-place) */
.lrn-msgcard { grid-column: 1 / -1; padding: 14px clamp(16px, 3vw, 22px); }
.lrn-msgcard__link { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; text-decoration: none; color: var(--eng-white); }
.lrn-msgcard__label { font-size: var(--fs-body-sm); font-weight: 600; }
.lrn-msgcard__badge {
  background: color-mix(in srgb, var(--eng-green) 22%, transparent);
  border: 1.5px solid color-mix(in srgb, var(--eng-green) 55%, transparent);
  color: var(--eng-green);
  border-radius: 999px;
  padding: 3px 12px;
  font-size: 0.78rem;
  font-weight: 700;
}
.lrn-msgcard__go { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.65); }
.lrn-msgcard__link:hover .lrn-msgcard__go { color: var(--eng-green); }

/* P0-A A5: quiet support link under the blocked-state CTA pair (LINE =
   support channel, never a competing big button) */
.lrn-arena__support { margin-top: 18px; font-size: var(--fs-body-sm); }
.lrn-arena__support a { color: rgba(255, 255, 255, 0.65); text-decoration: underline; text-underline-offset: 3px; }
.lrn-arena__support a:hover { color: var(--eng-green); }

/* T1 (GPT batch): motion predict-first — locked scenes + choice verdict
   states + reveal feedback lines (tokens/brand-rgba only) */
.lf-fsm__panel.is-locked { opacity: 0.45; cursor: default; }
.lf-choice__btn.is-right {
  border-color: var(--eng-green);
  background: color-mix(in srgb, var(--eng-green) 16%, transparent);
  color: var(--eng-green);
  opacity: 1;
}
.lf-choice__btn.is-wrong-pick {
  border-color: var(--eng-red-on-dark);
  background: color-mix(in srgb, var(--eng-red-on-dark) 14%, transparent);
  color: var(--eng-red-on-dark);
  opacity: 1;
}
.lf-fsm__feedback {
  margin-top: 10px;
  font-size: var(--fs-body-sm);
  line-height: 1.55;
  color: var(--eng-white);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--hairline-dark);
  border-radius: 10px;
  padding: 10px 14px;
}
.lf-fsm__feedback--ok {
  border-color: color-mix(in srgb, var(--eng-green) 45%, transparent);
  color: var(--eng-green);
  font-weight: 700;
}
.lf-fsm__feedback[hidden] { display: none; }

/* ── DX-5 P1 · แนวโน้มการฝึก (weakness trend) ──────────────────────────────
   Progress channel is PURPLE, not green — green is reserved for action/correct
   (brand law; the radar follows the same rule two blocks up). The direction
   badge is the ONE place a colour reads as judgement, and even there
   "slipping" gets a neutral tint rather than the red one, because a quiet week
   is not an error state. */
.lrn-trend__wrap { margin-top: 14px; }
.lrn-trend__svg { display: block; width: 100%; height: auto; overflow: visible; }
.lrn-trend__line {
  fill: none;
  stroke: var(--eng-purple);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 6px rgba(99, 53, 240, 0.55));
}
.lrn-trend__pt { fill: var(--eng-purple-soft); }
.lrn-trend__axis {
  display: flex; justify-content: space-between; margin-top: 6px;
  font-size: var(--fs-caption); color: rgba(255, 255, 255, 0.45);
}
.lrn-trend__read {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  margin-top: 12px; font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.7);
}
.lrn-trend__badge {
  padding: 3px 10px; border-radius: 999px; font-weight: 700;
  font-size: var(--fs-caption); white-space: nowrap;
}
.lrn-trend__badge.is-up   { background: color-mix(in srgb, var(--eng-green) 20%, transparent); color: var(--eng-green); }
.lrn-trend__badge.is-flat { background: rgba(255, 255, 255, 0.10); color: rgba(255, 255, 255, 0.85); }
.lrn-trend__badge.is-down { background: color-mix(in srgb, var(--eng-flame) 20%, transparent); color: var(--eng-flame); }

/* one-snapshot state: a POSITION, not a direction — so no axis, no slope */
.lrn-trend__start { display: flex; align-items: flex-start; gap: 12px; margin-top: 14px; }
.lrn-trend__dot {
  flex: none; width: 12px; height: 12px; margin-top: 4px; border-radius: 50%;
  background: var(--eng-purple);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--eng-purple) 25%, transparent);
}
.lrn-trend__startlabel { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.9); font-weight: 600; }
/* Same cap, same reason — the trend card is a full bento row too, and at 1440
   this hint was running 90.5em.
   ⚠️ But a NARROWER tier than the goal card above, and the difference is the
   point: an em measure is a claim PER FONT SIZE. `--container-flow` (560px) is a
   body-text tier; at `--fs-caption` the identical 560px reads 46.7em, because
   the same physical width holds more of a smaller glyph. `--container-sm`
   (480px) brings a caption back into the same measure the body prose sits at.
   The threshold rule from the article package, one level down: the invariant is
   the measure, not the pixel width that produced it once. */
.lrn-trend__hint { margin-top: 4px; max-width: var(--container-sm); font-size: var(--fs-caption); color: rgba(255, 255, 255, 0.55); overflow-wrap: anywhere; }


/* ==========================================================================
   (ข) ARTICLE PAGE TREATMENT — 2026-08-23
   The public article stopped being a teaching artifact and became an
   acquisition surface. These classes carry that: answer-first on the brand's
   dark field, a reading-serving motion layer, next-steps that look like
   destinations, and a way to keep the page.

   ⚠️ NO TYPE OVER ARTWORK. The mark sits BESIDE the heading on a FLAT
   --eng-space field. Every colour below is an explicit value on a known
   background, not an opacity multiplied onto an inherited one — opacity
   compounding has produced three separate contrast defects on this codebase,
   and the audit that would catch a fourth grades on its upper-middle sample.
   ========================================================================== */

.art-answer {
  display: flex;
  gap: 28px;
  /* ⚠️ THIS COMMENT NAMED AN ELEMENT THAT DOES NOT EXIST. It said the mark
     moves inside this box "see .art-answer__markwrap" and that the clip stops
     it escaping the radius. There is no .art-answer__markwrap in any template
     (0 occurrences in the served HTML); the parallax lives on
     .art-vehicle__markwrap, in a different block far below. So the clip is not
     holding a moving mark — and now that the band bleeds, the 20px radius it
     was said to protect is gone too. It is KEPT anyway, because .art-answer is
     a flex row whose child carries an entry animation, and a clip on a band
     that spans the viewport is cheap insurance. What is removed is the false
     reason: the next maintainer would have deleted this line on the grounds
     that the parallax it protects is not here. */
  overflow: hidden;
  /* flex-start, not center: the mark identifies the SKILL and the h1 names the
     topic, so they belong on the same line of sight. Centred, the mark drifts to
     the middle of the answer paragraph and reads as decoration. */
  align-items: flex-start;
  background-color: var(--eng-space);
  /* ⚠️ background-COLOR, not the `background` shorthand it replaced. The
     shorthand resets background-image, so the texture below would be wiped by a
     property that looks unrelated. */
  /* ---------- (ข) hero texture ----------
     A dot field at the quiet-border token's own alpha (rgba(255,255,255,0.10)),
     zero new assets and zero new colours. It exists because the band was a flat
     plate: the ONE loud element on the page had no surface, so it read as an
     absence of design rather than a decision. A 1px dot on a 22px pitch is under
     0.7% ink coverage — visible as a grain when you look at it, invisible when
     you read the h1 over it, which is the whole brief.
     ⚠️ CONTRAST RE-MEASURED AFTER THIS LANDED, not assumed: the h1 is white on
     #0B0716 and the dots sit at 10% white, so the worst pixel a glyph can meet
     is lighter than the ground by that alpha. audit:contrast covers the article
     surfaces and stays at 0. */
  /* (UAT 3.1c) the 22px dot grid read as graph paper — the band's surface is
     now a nebula corner in the article's own hue, depth instead of texture */
  background-image:
    radial-gradient(ellipse 70% 60% at 85% 0%, rgb(var(--art-hue-rgb, 99 53 240) / 0.14), transparent 70%),
    radial-gradient(ellipse 50% 45% at 8% 100%, rgb(var(--art-hue-rgb, 99 53 240) / 0.08), transparent 72%);
  border: 1px solid var(--hairline-dark);
  border-radius: 20px;
  padding: 32px 28px;
}

/* ---------- (ก) the answer band as a FULL-BLEED band ----------
   Reference #1 for `.eng-bleed`. The band was always a full-width object: a
   dark plate carrying a 112px mark and the h1, trapped inside a 736px text
   column and stopping 32px short of the viewport on each side. It now spans
   edge to edge — with NO viewport unit and NO negative margin, because it is a
   grid item asking for the `full` column (see public/css/layout.css).

   ⭐ THE BAND BLEEDS; ITS CONTENT DOES NOT. padding-inline centres the contents
   on the SAME measure the prose below uses, so the h1 and the first paragraph
   of the article share a left edge at every width:
     1280 → (1280 − 736) / 2 = 272 … content 272→1008, exactly the column
      390 → the max() floor = 16  … content 16→374, exactly the column
   That alignment is the whole reason to bleed at all. A band whose text drifts
   off the column reads as a different page, not a wider one.

   The radius and the side borders go, because a rounded corner at x=0 is a
   card pretending to be a band. The block borders stay: on a light page the
   colour change already draws the edge, but on --eng-space it does not. */
/* ⚠️ `.art-answer.art-answer--bleed` (0-2-0), NOT a bare modifier (0-1-0).
   The narrow-width block ~90 lines down re-declares `.art-answer { padding:
   24px 20px }`, same specificity and LATER in the file — so a single-class
   modifier lost the inline padding below 640px and the band's content sat at
   x=20 while the prose column sat at x=16. Four pixels, and it is exactly the
   misalignment this modifier exists to prevent. Measured, not reasoned: the
   first render of this rule shipped it. Doubling the class buys order
   independence without duplicating the centring formula anywhere. */
.art-answer.art-answer--bleed {
  border-radius: 0;
  border-inline: 0;
  padding-block: clamp(32px, 5vw, 48px);
  /* ⚠️ READS THE TIER, does not re-derive it. The first version inlined
     `(100% - (var(--container-md) - 2*var(--gutter-read))) / 2`, which happens
     to equal the read tier today and silently stops matching the moment the
     shell changes tier — a one-word edit the tier system exists to make cheap.
     --eng-container and --gutter are custom properties set on .eng-main--read
     and INHERITED down to this band, so this now says what it means: centre my
     content on whatever column my grid is using. */
  padding-inline: max(var(--gutter), (100% - var(--eng-container)) / 2);
}
/* ---------- (ข) parallax ----------
   ⚠️ OVERTURNS A RULING WRITTEN IN THIS FILE THREE HOURS EARLIER. The previous
   pass argued against parallax on a long-form reading surface and built a
   reveal-based alternative so the choice could be judged rendered rather than
   described. Jeff judged it: still นิ่ง. The argument was heard and lost, and
   that is recorded rather than quietly deleted — the reasoning below is what
   keeps the objection's ONE valid half alive.

   ⭐ THE READING COLUMN NEVER MOVES, BY CONSTRUCTION RATHER THAN BY PROMISE.
   Every `data-reveal` section is a SIBLING of .art-answer, so confining the
   transform to this subtree makes "the text does not move relative to the
   reader" a structural fact about the DOM, not a discipline someone must keep.

   ⚠️ THE TRANSFORM MUST NOT GO ON .art-answer__mark. That element runs
   `animation: art-mark-in ... both`, whose `to` state is `transform: none` —
   a filled final keyframe would overwrite the parallax permanently. Worse, it
   INVERTS: the reduced-motion block above removes that animation, so the
   declared transform would then apply ONLY to the readers who asked for less
   motion. Hence a wrapper element that owns nothing else.

   --art-par defaults to 0px, so no JS, failed JS, or an old browser yields
   exactly zero movement — the same fail-open shape as .art-reveal-ready. */
.art-answer__mark {
  flex: 0 0 auto;
  width: 112px;
  /* optical alignment with the h1's cap height, not its box top */
  margin-top: 4px;
  animation: art-mark-in 0.55s var(--ease-out) both;
}
.art-answer__mark svg,
.art-answer__mark img { width: 100%; height: auto; display: block; }
/* READ-PAGE INK-TRAP FIX (UAT 3.1): the token sheet's `p { color: var(--fg-primary) }`
   (colors_and_type.css:231) beats inherited white on the space ground — every
   bare paragraph/link on the dark article rendered BLACK (42 audit groups,
   2026-09-03). :where() keeps this at (0,0,1): it wins that token tie on source
   order and still loses to every single-class color in the .art-* hierarchy. */
:where(body.eng-bg-space .eng-main--read) p,
:where(body.eng-bg-space .eng-main--read) li,
:where(body.eng-bg-space .eng-main--read) a { color: inherit; }

/* READ-PAGE DARK RESTATEMENT (UAT 3.1): the practice islands reuse .eng-card
   (a WHITE component) and .lrn-opt; on the space ground the card goes glass
   and the options take the ARENA's own dark recipe (selectors twinned above
   at the arena block — one language, one copy). */
body.eng-bg-space .eng-main--read .eng-card {
  background: rgba(255, 255, 255, 0.04);
  border-color: rgba(255, 255, 255, 0.14);
}
/* the committed per-article cinematic hero (UAT 3.1 (b)) — 3:2 art in the
   band's visual slot; hue ring says which engine it belongs to */
.art-answer__hero {
  /* slow Ken Burns on the committed craft render — ambient per MOTION-LANGUAGE,
     one loop, no scroll coupling */
  flex: 0 1 400px; min-width: 240px;
  border-radius: 16px; overflow: hidden;
  border: 1px solid rgb(var(--art-hue-rgb, 99 53 240) / 0.4);
  box-shadow: 0 0 28px rgb(var(--art-hue-rgb, 99 53 240) / 0.18);
}
.art-answer__hero img { width: 100%; height: auto; display: block; }
/* FD ambient: the same quiet star dots the dashboard ground carries — the
   whole page is space now, not only the band */
.art-answer__hero img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  animation: artHeroDrift 26s ease-in-out infinite alternate;
}
@keyframes artHeroDrift {
  from { transform: scale(1); }
  to { transform: scale(1.06) translate(1.5%, -1%); }
}
/* ── THE SKY (UAT 3.1c) ── the uniform 26px dot grid read as graph paper, not
   space ("grid dot ไม่สวยเลย"). This is the dashboard's APPROVED sky recipe
   (.lrn-home::before, lrn-sky-drift) ported to the read page and the hub:
   three star layers at different pitches + a nebula wash in the brand purple
   with one faint green whisper (Saturn's colour, never a field). All alphas
   ≤0.22 — audit:contrast re-measured after this landed, not assumed. */
body.eng-bg-space .eng-main--read,
body.eng-bg-space .eng-main--app { position: relative; isolation: isolate; }
body.eng-bg-space .eng-main--read::before,
body.eng-bg-space .eng-main--app::before {
  content: "";
  position: absolute; inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(circle at 16% 28%, rgba(255, 255, 255, 0.22) 1px, transparent 1.7px),
    radial-gradient(circle at 78% 56%, rgba(255, 255, 255, 0.16) 1px, transparent 1.5px),
    radial-gradient(circle at 40% 82%, rgba(201, 178, 255, 0.20) 1px, transparent 1.7px);
  background-size: 230px 230px, 290px 290px, 350px 350px;
  animation: lrn-sky-drift 110s linear infinite;
}
body.eng-bg-space .eng-main--read::after,
body.eng-bg-space .eng-main--app::after {
  /* the nebula: depth behind the columns, so the ground stops being flat */
  content: "";
  position: absolute; inset: 0;
  z-index: -2;
  pointer-events: none;
  background-image:
    radial-gradient(ellipse 60% 40% at 18% 6%, color-mix(in srgb, var(--eng-purple) 16%, transparent), transparent 70%),
    radial-gradient(ellipse 45% 30% at 85% 20%, color-mix(in srgb, var(--eng-green) 5%, transparent), transparent 70%),
    radial-gradient(ellipse 70% 50% at 60% 95%, color-mix(in srgb, var(--eng-purple) 10%, transparent), transparent 72%);
}
.art-answer__body { min-width: 0; }
/* ORIENTATION, not invitation. --eng-purple-soft on --eng-space = 15.8:1, the
   same measured pairing as .art-answer__label below — a reader arriving from
   Google is told where they are inside Engnova before they are told anything
   else, and is asked for nothing here. The CTA stays where value has already
   been delivered. */
.art-answer__vehicle {
  color: var(--eng-purple-soft);
  font-size: 0.8125rem;
  letter-spacing: 0.03em;
  line-height: 1.5;
  margin: 0 0 10px;
}
/* 20.4:1 on --eng-space. */
.art-answer__h1 {
  color: var(--eng-white);
  font-size: clamp(1.5rem, 1.15rem + 1.6vw, 2.1rem);
  line-height: 1.25;
  margin: 0 0 14px;
}
/* --eng-purple-soft on --eng-space = 15.8:1. Deliberately NOT green: green is
   the action/correct channel, and a section label is neither. */
.art-answer__label {
  color: var(--eng-purple-soft);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  margin: 0 0 6px;
}
.art-answer__lead {
  color: var(--eng-white);
  font-size: 1.0625rem;
  line-height: 1.65;
  margin: 0;
}
/* The house pattern for muted text on a dark surface (learner.css .lrn-stats-glass,
   components.css .eng-btn--ghost on graphite). 10.2:1 over --eng-space, MEASURED.
   ⚠️ TWO THINGS THAT LOOK THE SAME AND ARE NOT: `opacity:` on an element scales
   the element AND every descendant AND compounds with any ancestor's opacity —
   that is the trap behind three contrast defects here. Alpha in `color: rgba()`
   applies to this text only and composites once; the audit's over(fg,bg) handles
   it explicitly.
   ⚠️ AND NOT color-mix(): it computes to `color(srgb …)`, which the audit's
   parseColor — matching only /rgba?\(/ — reads as BLACK. A colour our own
   instrument cannot measure is worse than a plainer one it can. */
.art-answer__scope {
  color: rgba(255, 255, 255, 0.72);
  font-size: 0.8125rem;
  line-height: 1.6;
  margin: 14px 0 0;
}

@keyframes art-mark-in {
  from { opacity: 0; transform: scale(0.9); }
  to   { opacity: 1; transform: none; }
}

@media (max-width: 640px) {
  .art-answer { flex-direction: column; align-items: flex-start; gap: 18px; padding: 24px 20px; }
  .art-answer__mark { width: 84px; }
}

/* ---------- the five engines ----------
   Five bars, empty, on a page whose reader has no account and no attempts. The
   emptiness is the argument: it is TRUE, it shows the size of what has not been
   measured, and an unfinished row is harder to leave than a sentence claiming
   there is more to do.

   🚫 NO FILL FRACTION. `is-started` is a boolean over real attempts. Nothing
   here renders a score, because on this page there is no evidence to render one
   from — the same refusal evidenceState makes under five attempts. */
.art-engines { list-style: none; margin: 18px 0 6px; padding: 0; display: grid; gap: 8px; }
.art-engine {
  display: grid;
  grid-template-columns: minmax(88px, auto) 1fr auto;
  align-items: center;
  gap: 10px;
}
.art-engine__name { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.9); }
.art-engine.is-current .art-engine__name { font-weight: 700; color: var(--eng-purple-soft); }
.art-engine__bar { display: flex; gap: 3px; }
.art-engine__bar i {
  flex: 1 1 0;
  height: 8px;
  border-radius: 3px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: transparent;
}
/* Lit segments come from real evidence only. Green is the action/correct
   channel and this IS progress that was earned, so it is the honest colour. */
.art-engine.is-started .art-engine__bar i { background: var(--eng-green-deep); border-color: var(--eng-green-deep); }
.art-engine__state { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.65); white-space: nowrap; }
.art-engine.is-started .art-engine__state { color: var(--eng-green); }
.art-engines__note { margin-top: 10px; }

/* The one link out of the neighbourhood, to the index that did not exist when
   the neighbourhood block was written. Quiet: it is a way out, not the page's
   action — the placement CTA above it is. */
.art-sib-all { margin-top: 14px; }
/* ⚠️ PURPLE, NOT GREEN. .art-vehicle is a WHITE card, and --eng-green is
   1.33:1 on white — it is the action channel on dark surfaces and unreadable
   here. Caught while writing the rule rather than by the audit, because the
   number is already on record. */
.art-sib-all a {
  font-size: var(--fs-body-sm);
  font-weight: 600;
  color: var(--eng-purple-soft);
  text-decoration: none;
}
.art-sib-all a:hover,
.art-sib-all a:focus-visible { text-decoration: underline; }

/* ---------- the gateway CTA at narrow widths ----------
   MEASURED, not anticipated: at 390 the new placement CTA rendered 446px wide
   against a 390 viewport — 93px of horizontal overflow on the article's own
   primary action, on the surface most of its readers arrive on.
   .eng-btn is inline-flex, so it sizes to max-content and the long Thai string
   simply ran off the edge.
   ⚠️ SCOPED TO THIS PAGE ON PURPOSE. The same latent behaviour exists on every
   .eng-btn in the repo; widening the fix to the component would change buttons
   on 50 surfaces this package never looked at. The article's CTA copy is the
   long one, so the article's stylesheet is where the accommodation belongs.
   The copy is not shortened to fit — a button that wraps is a smaller cost than
   a sentence trimmed to satisfy a layout. */
@media (max-width: 640px) {
  .art-vehicle > .eng-btn--primary,
  #gate > .eng-btn--primary,
  #article-preview-result > .eng-btn--primary,
  #article-result > .eng-btn--primary {
    display: flex;
    width: 100%;
    white-space: normal;
    text-align: center;
  }
}

/* The coverage reframe (item E). Quiet, factual, and NOT styled as a warning:
   it is equally true after a correct answer, and a red chip after a correct
   answer reads as a punishment for being right. */
.art-reframe {
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.68);
  margin: 0 0 14px;
}

/* The price of not starting — quiet, immediately before the ask. */
.art-gate__cost {
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.68);
  margin: 0 0 14px;
}
/* the gate card (UAT 3.1): was an inline lavender wash from the light era —
   on space it becomes the page's ONE purple-glow instrument panel, because
   this card carries the page's conversion ask. */
/* the read-scope glass rule below carries (0,3,1); this must beat it, so it
   wears the same scope prefix rather than an !important. */
.art-gate-card,
body.eng-bg-space .eng-main--read .eng-card.art-gate-card {
  position: relative;
  background: color-mix(in srgb, var(--eng-purple) 14%, transparent);
  border: 1.5px solid color-mix(in srgb, var(--eng-purple) 55%, transparent);
  box-shadow: 0 0 32px color-mix(in srgb, var(--eng-purple) 20%, transparent);
}
.art-gate-card::before,
.art-gate-card::after {
  content: ""; position: absolute; width: 20px; height: 20px;
  border: 2px solid color-mix(in srgb, var(--eng-purple) 75%, transparent);
  pointer-events: none;
}
.art-gate-card::before { top: -2px; left: -2px; border-right: 0; border-bottom: 0; border-top-left-radius: 14px; }
.art-gate-card::after { bottom: -2px; right: -2px; border-left: 0; border-top: 0; border-bottom-right-radius: 14px; }
.art-gate-card::before, .art-gate-card::after { animation: artGateBreathe 6s ease-in-out infinite; }
@keyframes artGateBreathe {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

/* ---------- the diagram plate ----------
   Named --diagram-- and not --figure--, and the reason outlived the thing that
   caused it. A held per-slug figure module owned an `.art-fig*` family; the
   first name here was `.art-figure-plate`, a grep-SUBSTRING of it. The two
   could never collide in CSS — but they collided in an investigation within
   minutes, when `grep art-fig` reported a producer in article.ejs that was in
   fact this class. That module has since been deleted, so the collision is
   gone; the law is not. A NAME THAT COLLIDES WITH A SEARCH IS A DEFECT IN THE
   INSTRUMENT, NOT A STYLE PREFERENCE — the tool most used to find things
   should not be the one that cannot tell them apart. 
   Was three inline styles on a div. It is a class now because it is the only
   thing on this light page that KNOWS it is a dark surface, and the figure
   caption needs that fact: .eng-figure__caption declares the light-surface ink,
   and anything that changes the ground underneath it owes the override. Same
   pixels as the inline rule it replaces. */
.art-diagram-plate {
  background: var(--eng-space);
  padding: 20px;
  border-radius: 16px;
}
/* The diagram was using 51% of the plate it sits on — 374px of a 736px column,
   with ~180px of empty black down each side. The plate is the figure's frame,
   not a stage it stands in the middle of. */
.art-diagram-plate .eng-figure { width: 100%; }
.art-diagram-plate .lrn-sc { width: 100%; margin-top: 0; }
.art-diagram-plate .eng-figure__caption {
  /* the value .lrn-sc__caption already used on this exact ground */
  color: rgba(255, 255, 255, 0.62);
}

/* ---------- the vehicle: which of the five carried you here ----------
   The product story is five vehicles to Saturn. It has been drawn (the landing
   ring markers) and badged ("ครบ 5 ทักษะ") but never SAID to a reader, and
   never on the surface a stranger actually lands on.

   Light surface on purpose — the dark plate belongs to the answer at the top of
   the page, and a second full-bleed dark band 3,000px down would read as a
   second beginning. It also makes the mark's gradient ids distinct from the
   hero's by construction (svg() mints them as `${slug}-${surface}`).

   ⚠️ NO PERCENTAGE, and that is a rule rather than an omission — see
   src/data/skill-vehicle.js. An anonymous reader has no data, so a number here
   would be fabricated on the most motivating element on the page. */
.art-vehicle {
  position: relative;
  border: 1.5px solid rgba(255, 255, 255, 0.14);
  border-radius: 20px;
  /* The mark MOVES inside this box (see .art-vehicle__markwrap). Without a clip
     it escapes the 20px radius at the extremes of its travel. */
  overflow: hidden;
  padding: 24px;
  background: rgba(255, 255, 255, 0.04);
}
/* FD chrome (UAT 3.1): HUD corner brackets in the article's own hue — the
   diagram plate is this page's central instrument. */
.art-vehicle::before,
.art-vehicle::after {
  content: ""; position: absolute; width: 20px; height: 20px;
  border: 2px solid rgb(var(--art-hue-rgb, 99 53 240) / 0.6);
  pointer-events: none; z-index: 1;
}
.art-vehicle::before { top: 0; left: 0; border-right: 0; border-bottom: 0; border-top-left-radius: 18px; }
.art-vehicle::after { bottom: 0; right: 0; border-left: 0; border-top: 0; border-bottom-right-radius: 18px; }
.art-vehicle__head { display: flex; align-items: flex-start; gap: 18px; margin-bottom: 16px; }
/* ⚠️ A WRAPPER, not the mark itself — the same reason the hero needed one.
   Any entrance animation declared with `both` fills its final keyframe, and a
   `to { transform: none }` would overwrite the parallax permanently AND invert
   under reduced motion, where the animation is removed and the declared
   transform is all that survives. The mark carries no animation today; the
   wrapper means it can gain one without silently killing this.

   --art-par defaults to 0px, so no JS, failed JS, or an old browser yields
   exactly zero movement — the same fail-open shape as .art-reveal-ready. */
.art-vehicle__markwrap {
  flex: 0 0 auto;
  will-change: transform;
  transform: translate3d(0, var(--art-par, 0px), 0);
}
.art-vehicle__mark { flex: 0 0 auto; width: 72px; }
.art-vehicle__mark svg,
.art-vehicle__mark img { display: block; width: 100%; height: auto; }
/* --eng-purple on --eng-white = 6.37:1 (measured at the hero's scope line). */
.art-vehicle__eyebrow {
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  /* --eng-purple measures 2.91:1 on the dark card tint (audit 2026-09-03);
     the soft tint carries the same channel and clears AA. */
  color: var(--eng-purple-soft);
  margin-bottom: 4px;
}
.art-vehicle__title { font-size: var(--fs-h3); line-height: 1.3; margin-bottom: 6px; }
/* Prose inside the vehicle card — measured for the same reason the article body
   is, and here rather than in the template because the card is built from a
   string in the view's scriptlet where a class list is harder to read. */
.art-vehicle__payoff { max-width: var(--container-flow); font-size: var(--fs-body); line-height: 1.6; color: rgba(255, 255, 255, 0.85); }
.art-vehicle__distance { max-width: var(--container-flow); font-size: var(--fs-body-sm); line-height: 1.6; margin-bottom: 12px; }
.art-vehicle__sub { font-size: var(--fs-h4); margin: 28px 0 4px; }

/* The neighbourhood — plain links, because a complete list read as cards would
   outweigh the ranked three directly above it. */
.art-sib-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 8px 18px;
}
.art-sib {
  color: var(--eng-purple-soft);
  text-decoration: underline;
  text-underline-offset: 3px;
  font-size: var(--fs-body-sm);
}
.art-sib:hover { text-decoration-thickness: 2px; }
.art-sib:focus-visible { outline: 3px solid var(--eng-purple); outline-offset: 2px; }

/* ---------- next steps: destinations, not a footnote ---------- */
.art-next-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.art-next {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 16px 18px;
  border: 1.5px solid rgba(255, 255, 255, 0.14);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.04);
  color: rgba(255, 255, 255, 0.92);
  text-decoration: none;
  transition: border-color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
.art-next:hover { border-color: var(--eng-purple-soft); transform: translateY(-1px); }
.art-next:focus-visible { outline: 3px solid var(--eng-purple); outline-offset: 2px; }
.art-next__title { font-weight: 600; line-height: 1.45; }
/* --eng-purple on --eng-purple-soft = 5.04:1. */
.art-next__chip {
  flex: 0 0 auto;
  font-size: 0.75rem;
  padding: 4px 10px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--eng-purple) 22%, transparent);
  color: var(--eng-purple-soft);
  white-space: nowrap;
}

/* ---------- the reading measure ----------
   MEASURED, not guessed: at the old --container-lg the rule body ran ~132
   characters per line — roughly double a comfortable reading measure, and the
   single loudest "nobody designed this" signal on the page. The article column
   is now --container-md (768px), an EXISTING token rather than an invented
   width.

   The option pills inherit the browser's <button> default text-align:center.
   In the quiz arena the pill is narrow enough that nobody notices; in a
   full-width article column the letter floats ~800px from its own A/B/C/D key.
   Scoped to this page ONLY — the arena has the same latent defect and fixing it
   there is the arena's decision, not this pass's. */
#article-preview-questions .lrn-opt,
#article-mission .lrn-opt { text-align: left; }
#article-preview-questions .lrn-opt__text,
#article-mission .lrn-opt__text { text-align: left; }

/* ---------- the preview is an INVITATION, not an exam ----------
   Measured before touching it: the section ran 767px = 19.1% of a 4,017px page
   at 1280, sitting second on the page — a fifth of the article read before a
   single rule. Two thirds of that was the option stack (4 pills x 52px
   min-height). Cutting to ONE question takes ~340px; these declarations take
   the rest off what remains.

   ID-scoped on purpose. `.lrn-arena .lrn-opt` is 0,0,2,0 = 20 and this is
   0,1,1,0 = 110, so the arena is unreachable from here and cannot reach back —
   the two subtrees never co-occur anyway. The arena keeps its 52px touch
   target because a quiz IS the task there; here the question is a doorway. */
#article-preview-questions .lrn-opt {
  min-height: 44px;                 /* still >= the 44px touch minimum */
  padding: 8px 14px;
  font-size: var(--fs-body);        /* 16px, down from --fs-body-lg 18px */
}
#article-preview-questions .lrn-opt__key {
  width: 26px;
  height: 26px;
}

/* ---------- the FAQ is reference, and must not read as a quiz ----------
   A reader counts question-shaped things by SHAPE, not by function: rendered
   flat, four FAQ entries plus one quiz item plus an interrogative section
   heading were counted as "six questions" on a page whose quiz is one item.

   So exactly one thing on this page is allowed to look like a question aimed at
   the reader — the preview island. These are disclosure rows: closed by
   default, no card, no pill, no option chrome, and no answer visible until
   asked for. The text stays in the DOM (and the FAQPage JSON-LD is emitted
   separately), so this costs nothing in search.

   ⚠️ A closed <details> has layout but is NOT painted, so its answer text is
   correctly skipped by audit-contrast's checkVisibility filter — the same
   documented behaviour as the journey map's collapsed sections. The SUMMARY is
   painted and is measured. */
.art-faq {
  border-bottom: 1.5px solid rgba(255, 255, 255, 0.12);
}
.art-faq:first-of-type { border-top: 1.5px solid rgba(255, 255, 255, 0.12); }
.art-faq__q {
  cursor: pointer;
  list-style: none;
  padding: 14px 28px 14px 0;
  position: relative;
  font-size: var(--fs-body);
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.92);
}
.art-faq__q::-webkit-details-marker { display: none; }
/* --eng-purple on the page ground = 6.37:1 (measured at the hero scope line). */
.art-faq__q::after {
  content: '+';
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--eng-purple-soft);
  font-size: 1.125rem;
  line-height: 1;
}
.art-faq[open] > .art-faq__q::after { content: '\2212'; }
.art-faq__q:focus-visible { outline: 3px solid var(--eng-purple); outline-offset: 2px; }
.art-faq__a {
  /* An answer is PROSE, and it was the widest text on the page: 50.6em, half
     again the hero lead's 35.1em. It is not a direct grid child (it lives inside
     a <details> inside a section), so it takes the measure here rather than via
     the .eng-measure class the template applies to the blocks that are. */
  max-width: var(--container-flow);
  font-size: var(--fs-body-sm);
  line-height: 1.7;
  color: rgba(255, 255, 255, 0.78);
  margin: 0 0 16px;
  padding-right: 28px;
}

/* ---------- keep it / pass it on ---------- */
.art-share__row { display: flex; flex-wrap: wrap; gap: 10px; }

/* ---------- reading-serving motion ----------
   The hiding rule is scoped under .art-reveal-ready, which ONLY the script
   adds. No JS, failed JS, or an old browser ⇒ nothing is ever hidden. A reveal
   that can swallow the page when its script does not arrive is not a reveal,
   it is an outage. Everything here rides the existing reduced-motion block
   above.

   ⚠️ This block used to end "Deliberately NOT parallax". That ruling was
   OVERTURNED on 2026-08-23 after the alternative was judged rendered; the
   parallax lives on .art-vehicle__markwrap (corrected 2026-08-25: three
   comments said .art-answer__markwrap, which exists nowhere) and touches nothing
   below the hero. */
.art-reveal-ready [data-reveal] {
  opacity: 0;
  transform: translateY(10px);
}
.art-reveal-ready [data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}

/* The structure card is mounted by structure-card.js; its parts arrive together,
   so they are staggered on entry — the card assembles rather than appearing. */
#article-structure-card > * { animation: art-sc-in 0.42s var(--ease-out) both; }
#article-structure-card > *:nth-child(2) { animation-delay: 0.07s; }
#article-structure-card > *:nth-child(3) { animation-delay: 0.14s; }
#article-structure-card > *:nth-child(4) { animation-delay: 0.21s; }
#article-structure-card > *:nth-child(n+5) { animation-delay: 0.28s; }
@keyframes art-sc-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}


/* ── (ข) ARTICLE INNER — texture, callouts, tints, meta, progress (2026-08-29)
   Every colour below is DERIVED: --art-hue-rgb is written onto <article> by the
   view from HUES[skill] (src/data/skill-mascots.js), the same table the hero
   mark is drawn from. Nothing here types a hex, so a hue that moves — grammar
   already did, #8C5CF5 → #2078E0 for ΔE against the parent purple — moves here
   too, once.

   ⚠️ rgb(var(--x) / a) RATHER THAN color-mix(), DELIBERATELY — and the reason is
   narrower than "the auditor cannot read it", which is worth stating exactly.
   Chrome computes color-mix(in srgb, …) to `color(srgb …)`, and
   scripts/audit-contrast.mjs parses only /rgba?\(/, returning BLACK AT FULL
   ALPHA for anything else, silently. That parse runs on the FOREGROUND only
   (`cs.color` / `cs.fill`); backgrounds are pixel-sampled from the composited
   screenshot, so a color-mix BACKGROUND would in fact grade correctly today.
   The shape is used anyway, because the moment the same derived hue is wanted
   on a chip's text or a border-as-text — the natural next step once a tint
   exists — it lands in `color` and silently reads as #000000. One derivation
   shape that is safe in every property beats one that is safe in this one. */

/* ---------- the meta line ---------- */
/* ---------- the meta chips ----------
   Three facts, all derived: reading time from the article's own characters, the
   rule count from the SAME `coreCount` the h2 below prints, and the practice
   count from the array the page is about to render.

   ⭐ WHY NOT A CEFR BADGE, since that is what a reader might expect here.
   Ruled out 2026-08-29 — see DECISIONS (118). The short version: the column
   does not exist, deriving one from the pinned questions spans 2-4 levels per
   article, and `src/models/article.js` already forbids printing an absolute
   level anywhere on this page. "3 กฎหลัก" also tells a Thai reader the SHAPE of
   what is coming, which "A2" does not.

   ⚠️ THE RULE COUNT MUST NOT CONTRADICT THE HEADING. It reuses `coreCount`
   rather than `a.rules.length`, because the h2 reads "N กฎหลัก (+ M จุดที่คนไทย
   พลาดบ่อย)" and a chip saying 4 over a heading saying 3 is the two-numbers-one-
   fact defect this page has hit before. One expression, two consumers. */
.art-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.72);
  margin: 0 0 20px;
}
.art-meta__chip { white-space: nowrap; }

/* Bookmark chip (UAT round 3 ③) — authed-only; two labels, one visible per
   state (CSS flips them off aria-pressed's class twin, so the words always
   match the server-confirmed state). */
.art-bookmark {
  cursor: pointer; font: inherit; color: inherit;
  background: transparent;
  border: 1.5px dashed rgba(255, 255, 255, 0.35);
  border-radius: var(--radius-pill);
  padding: 4px 12px;
  transition: border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.art-bookmark:hover { border-color: rgba(255, 255, 255, 0.6); }
.art-bookmark .art-bookmark__on { display: none; }
.art-bookmark.is-saved {
  border-style: solid;
  border-color: color-mix(in srgb, var(--eng-green) 55%, transparent);
  color: var(--eng-green);
}
.art-bookmark.is-saved .art-bookmark__off { display: none; }
.art-bookmark.is-saved .art-bookmark__on { display: inline; }

/* ---------- example sentences: a callout, not a code block ----------
   They were `font-mono`, which said "code" about a natural-language sentence —
   the one thing on the page a learner is meant to READ as English. The bar
   carries the skill hue so the examples belong to the same object the mark and
   the rule card do.

   🚫 NO SPEAKER ICON. An audio affordance on a page with no audio is UI that
   promises a feature that does not exist. */
.art-ex-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.art-ex {
  border-left: 3px solid rgb(var(--art-hue-rgb, 99 53 240) / 0.7);
  background: rgb(var(--art-hue-rgb, 99 53 240) / 0.12);
  border-radius: 0 8px 8px 0;
  padding: 7px 12px;
  font-size: var(--fs-body-sm);
  line-height: 1.6;
  /* ⚠️ EXPLICIT COLOUR, NOT AN INHERITED opacity. The old list carried
     `opacity-80`, and opacity scales the element AND every descendant AND
     compounds with ancestors — three separate contrast defects in this repo have
     come from exactly that. A colour with its own alpha composites once and the
     auditor can read it. */
  color: rgba(255, 255, 255, 0.88);
}

/* ---------- rule cards ----------
   A 4% wash of the skill hue and a slightly stronger edge. Enough to say "these
   four belong together and to this skill", not enough to compete with the hero.
   🚫 no drop-shadow: V2 is glow-on-dark, never a black offset shadow on white.
   🚫 no bg-gray-50 / bg-slate-50 — a neutral tint would say nothing. */
.art-rule {
  background: rgb(var(--art-hue-rgb, 99 53 240) / 0.1);
  border-color: rgb(var(--art-hue-rgb, 99 53 240) / 0.3);
}
/* Heavier, and tighter to the rule it names. The weight is the emphasis; the
   colour stays ink, because a hue-coloured heading on a tinted ground is a
   contrast question nobody needs to re-open for a 4% gain in prettiness. */
.art-rule__head {
  font-weight: 800;
  margin-bottom: 6px;
}

/* ---------- scroll progress ----------
   ⭐ THE ONLY HONEST PROGRESS BAR: it measures how far down the PAGE the reader
   has scrolled. It is not a measure of learning, of mastery, or of the mission —
   the product has real instruments for those and they live behind a login.
   Width is written by public/js/article.js; the default of 0 means a thrown
   script is a page with no bar rather than a broken one. */
.art-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 60;
  pointer-events: none;
  background: transparent;
}
.art-progress__bar {
  display: block;
  height: 100%;
  width: 0;
  background: var(--eng-green);
  transform-origin: left center;
}

/* ============================================================
   GET /grammar — the article hub.
   Light marketing chrome, the shared page grid at the multi-column tier, and
   the same five marks the article page serves. Every rule below is scoped to
   .ghub-*, so this block cannot reach the article page or any other surface
   that loads this stylesheet.
   ============================================================ */
/* ---------- ① THE DARK HERO BAND — the loud 10%, and the only one ----------
   The page STAYS LIGHT. One element changes ground, and everything else on this
   page is deliberately quiet, because the complaint that opened this package was
   "แห้ง" — no focal point — not "too little stuff". Six equally loud elements
   would answer it with "รก", which is worse: dryness is a missing decision,
   clutter is five competing ones.

   The budget, stated so a later edit can see what it is spending against:
     · this band  — ground #FFFFFF → #0B0716, a 21:1 change, with glow type
     · everything else — hue alphas 0.04-0.20 on white, and one 10px settle
   Nothing in ②-⑥ comes within an order of magnitude of the first line.

   ⚠️ IT BLEEDS BY ASKING FOR THE `full` COLUMN, not by a viewport unit or a
   negative margin — it is a direct child of .eng-main (see public/css/layout.css).
   That is why the markup wraps .ghub-head in this section rather than putting
   `eng-bleed` on the header: the header is also the anchor two tests slice on
   (`class="ghub-head"`) and the selector the 640px CTA override names, and a
   class added to it would have broken both for a layout reason. */
.ghub-band {
  background-color: var(--eng-space);
  /* ⚠️ background-COLOR, not the `background` shorthand — the shorthand resets
     background-image and would wipe ② from a line that looks unrelated. The
     article band carries the same warning for the same reason. */
  /* ---------- ② TEXTURE, AND ONLY HERE ----------
     The article's dot field at its own values: 1px on a 22px pitch is under 0.7%
     ink — a grain when you look at it, invisible when you read the h1 over it.
     Copied rather than re-derived, so its contrast consequence is already on
     record.
     🚩 IT IS NOT ON THE CARDS, and that is a measurement, not taste. The light
     analogue would be --hairline #E8E8E4 on --eng-white #FFFFFF — a 1.09:1
     pair. What makes a dot visible is its contrast with its own ground, not its
     area, so at that ratio the texture is either invisible or needs a colour
     outside the token set. Surface belongs to the loud element. */
  /* (UAT 3.1c) dot grid → nebula corners; same verdict as the article band */
  background-image:
    radial-gradient(ellipse 60% 70% at 90% 10%, color-mix(in srgb, var(--eng-purple) 18%, transparent), transparent 70%),
    radial-gradient(ellipse 40% 50% at 5% 90%, color-mix(in srgb, var(--eng-green) 6%, transparent), transparent 72%);
  border-block: 1px solid var(--hairline-dark);
  padding-block: clamp(32px, 5vw, 48px);
  /* READS the tier, does not re-derive it: --eng-container and --gutter are set
     on .eng-main--app and inherit down here, so the band's content lands on the
     same 1120 column the cards below use. 1280 → 80px, 390 → the 20px floor. */
  padding-inline: max(var(--gutter), (100% - var(--eng-container)) / 2);
  margin-bottom: 8px;
}
/* Every .ghub-* colour below this line was written for a white page. On
   --eng-space they invert; these four rules are the whole of that inversion. */
.ghub-band .ghub-lead { color: var(--eng-white); }
.ghub-band .ghub-sub { color: rgba(255, 255, 255, 0.72); }
/* ⚠️ Alpha in `color: rgba()`, NOT an `opacity` on the element. opacity scales
   the element AND every descendant AND compounds with any ancestor's; the
   contrast auditor composites an rgba text colour explicitly and cannot see a
   compounded opacity chain. Same choice, same reason, as the article's scope
   note. */

/* ⚠️ THE H1 CARRIES `.eng-glow-text` PLUS `--white`, AND THE MODIFIER IS THE
   POINT. The bare class colours text --eng-green (colors_and_type.css) with a
   green glow — and green is reserved for action and correct-answer in this
   product, so the largest heading on the page would have been speaking in the
   CTA's channel. The base class is still what supplies Prompt 800 and the
   display tracking; --white overrides only colour and shadow. Same pairing the
   quiz session uses.
   📌 The brief said this class was "currently unused". It is on eleven elements
   across nine views and pinned by a landing test — the premise was wrong, the
   class was still the right one, and the choice it hid (green h1 or white one)
   was real. */
.ghub-band .ghub-title { margin-bottom: 12px; }
.ghub-head { padding: 0 0 8px; }
.ghub-title { margin-bottom: 12px; }
/* The axis sentence. Larger than body and quieter than the h1 — it is the
   claim the page exists to make, so it is not a caption. */
.ghub-lead {
  font-size: var(--fs-body-lg);
  color: var(--fg-primary);
  max-width: 46ch;
  margin-bottom: 10px;
}
.ghub-sub {
  font-size: var(--fs-body);
  color: var(--fg-secondary);
  max-width: 58ch;
  margin-bottom: 20px;
}
.ghub-cta { margin-bottom: 8px; }

/* One column per engine at desktop width, stacking below it. auto-fit with a
   minimum rather than a fixed count: five groups across a 1120 column give
   three then two, and the same rule gives one at 390 with no media query. */
/* ── DARK-BODY NAV GUARD (UAT round 3.1) ─────────────────────────────────
   The hub is the FIRST page that pairs the shared marketing nav (a white bar,
   views/partials/nav.ejs) with `bodyClass: eng-bg-space text-eng-white` —
   and `text-eng-white` CASCADES: the nav's links inherit it and go white on
   the white bar (measured 1:1 by the audit the moment the body flipped).
   The nav partial itself is inside the 21 byte-frozen article pages, so the
   fix lives HERE as a scoped restatement, not in the partial: on any dark
   body, text inside the white header bar goes back to ink. `header.bg-eng-white`
   scopes it to the marketing nav bar exactly — the dark app-bar never carries
   bg-eng-white. */
body.eng-bg-space header.bg-eng-white,
body.eng-bg-space header.bg-eng-white a { color: var(--eng-ink); }

.ghub-groups {
  display: grid;
  gap: 20px;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  margin: 24px 0 56px;
  /* ⚠️ `start`, NOT the grid default. Left to stretch, the two empty engines were
     pulled to the height of an eight-article card — roughly 200px of nothing
     inside a bordered box, which reads as broken rather than as empty. A short
     card beside a tall one is ordinary, and "there is less here" is the truth.
     🚫 NEVER a min-height in its place: that would re-create the same dead space
     and take the natural height with it. The fix is to stop stretching, not to
     pad. */
  align-items: start;
}
/* ---------- ③ CARD TINT, DERIVED FROM THE ENGINE'S OWN HUE ----------
   `--ghub-hue` is set inline per card by the route, from HUES via hueChannels()
   — the same reader the article page uses, so one engine cannot end up two
   colours across two surfaces. Never a typed hex here: grammar has already
   moved once (#8C5CF5 → #2078E0, for ΔE against the parent purple) and a second
   copy would have survived that move.

   ⚠️ THE FALLBACK IS BRAND PURPLE AND IT IS A FAILURE STATE, NOT A DEFAULT. If
   --ghub-hue is ever absent, all five cards go purple together — which reads
   exactly like a design decision. hueChannels() logs an error on an unknown
   skill for that reason; the tint is the quiet half of a loud log line.

   0.06 is the article's own `.art-ex` alpha, inside the package's 4-8% band,
   and .art-rule sits at 0.04 — so hover has somewhere to go (see ⑥) without
   leaving the range. `rgb(var(--x) / a)` rather than color-mix(): the contrast
   auditor's parseColor matches only `rgba?(`, so a color-mix FOREGROUND grades
   as black at alpha 1, silently. Backgrounds are pixel-sampled and would be
   fine — but one derivation shape that is safe in every property beats two. */
.ghub-group {
  position: relative; /* anchors the HUD corner brackets (จัดเต็ม pass) */
  /* UAT round 3.1 dark re-skin: the page ground moved to --eng-space (Jeff:
     the hub must read Flight Deck). The hue tint survives verbatim — it reads
     as glass on dark — but every LIGHT-PAGE ink/hairline token below had to be
     restated for the dark ground (a --fg-* token on space is dark-on-dark). */
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 16px;
  padding: 20px;
  background: rgb(var(--ghub-hue, 99 53 240) / 0.08);
  /* ⑥ the tint step and the border are what "hover" moves; declared here so the
     transition exists before the state that uses it. */
  transition: background-color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
/* ---------- ⑥ HOVER: THE ENGINE'S COLOUR BLEEDS IN ----------
   One budget increment on the fill (0.06 → 0.10 — the same 0.04 delta that
   separates .art-rule from .art-ex), and the border goes from the neutral
   hairline to the hue at 0.55. 0.55 is the one alpha above 0.20 the repo has
   sanctioned for a hue, and only ever on a LINE (.art-ex's 3px stripe): a 1px
   line at 0.55 carries less ink than an area at 0.10, which is why the pairing
   holds the budget. `:focus-within` so a keyboard reader gets the same signal.
   🚫 Not `hover:bg-gray-50` — a grey hover says "interactive", the hue says
   WHICH engine, which is the thing this page exists to teach. */
.ghub-group:hover,
.ghub-group:focus-within {
  background-color: rgb(var(--ghub-hue, 99 53 240) / 0.14);
  border-color: rgb(var(--ghub-hue, 99 53 240) / 0.55);
}
.ghub-group__head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 14px;
  /* ⚠️ WRAP, because the empty engines' chip is a sentence, not a number.
     Without this the chip competes for the row and the engine NAME breaks — and it
     broke mid-word ("การ / ฟัง"), which Thai does not do. The name is the one
     thing on this card a reader scans for; it keeps the row, and the long chip
     drops beneath it. */
  flex-wrap: wrap;
}
.ghub-group__name { flex: 1 1 auto; }
/* ---------- ④ A CIRCULAR FRAME, AND A GLOW UNDER IT ----------
   ⚠️ THE MARK PNG ALREADY CARRIES THE HUE (the rasters derive from the same
   HUES table). So the glow is a SECOND statement of a fact the artwork has
   already made, and it has to sit below the tint's weight rather than above it
   — otherwise five card heads become five focal points and the page answers
   "แห้ง" with "รก".
   Peak 0.10: above the card's 0.06 fill so the mark reads as where the colour
   comes from, below the 0.20 border alpha so it never competes with an edge.
   0.20 is the highest alpha this repo has ever put on an AREA rather than a
   line, and a radial glow is an area.
   📌 If a render ever shows these circles pulling the eye before the band does,
   CUT THE GLOW AND KEEP THE FRAME — a 1px ring at 0.20 still says "this hue
   belongs to this engine", and it is a line, not an area. */
.ghub-mark {
  flex: 0 0 auto;
  line-height: 0;
  position: relative;
  display: grid;
  place-items: center;
  width: 76px;
  height: 76px;
  border-radius: 50%;
  border: 1px solid rgb(var(--ghub-hue, 99 53 240) / 0.20);
  background: radial-gradient(
    circle at 50% 50%,
    rgb(var(--ghub-hue, 99 53 240) / 0.10),
    rgb(var(--ghub-hue, 99 53 240) / 0) 70%
  );
}
/* The mark is a 256px raster asked for at 96 and painted at 48 — the same
   asset the article page uses, so it is already cached for a reader who
   arrived from one. */
.ghub-mark img { width: 48px; height: 48px; display: block; }
.ghub-group__name { font-size: var(--fs-h3); margin: 0; }
.ghub-group__count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: rgba(255, 255, 255, 0.72);
  white-space: nowrap;
}
.ghub-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.ghub-item {
  display: block;
  padding: 10px 0 11px;
  font-size: var(--fs-body);
  color: rgba(255, 255, 255, 0.92);
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.ghub-list li:last-child .ghub-item { border-bottom: 0; }
.ghub-item:hover,
.ghub-item:focus-visible { color: var(--eng-green); }
/* UAT round 3.1: a row sells the article — title, one clamped line of its own
   quick answer, and the page's real read-time in mono. */
.ghub-item__title { display: block; font-weight: 600; line-height: 1.45; overflow-wrap: anywhere; }
.ghub-item__ex {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1;
  overflow: hidden;
  margin-top: 3px; font-size: 0.84rem; line-height: 1.55;
  color: rgba(255, 255, 255, 0.62);
}
.ghub-item__meta {
  display: flex; align-items: baseline; gap: 8px;
  margin-top: 5px;
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.66);
}
.ghub-item__go {
  margin-left: auto; font-weight: 700; color: var(--eng-green);
  opacity: 0; transform: translateX(-4px);
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.ghub-item:hover .ghub-item__go,
.ghub-item:focus-visible .ghub-item__go { opacity: 1; transform: translateX(0); }
/* ---------- ⑥ (second half) THE ARROW SLIDES IN ----------
   On the ITEM, not on the card: the card has five destinations and an arrow
   that points nowhere is decoration. Each row has exactly one, so the arrow is
   an affordance. 4px is the smallest travel the eye reliably registers, and it
   arrives with the existing purple + underline rather than replacing them —
   the link affordance was already right. */
/* (the old .ghub-item::after slide-in arrow moved INTO the meta row as
   .ghub-item__go — one arrow per row, now beside the read-time instead of
   glued to the title's last word.) */

/* ---------- A · THE FIFTH ARTICLE ONWARD FOLDS AWAY ----------
   Five cards holding 8, 8, 5, 0 and 0 articles made one tall column beside four
   short ones. Four visible each settles them into a row.
   ⚠️ EVERY ARTICLE STILL RENDERS — the rest are inside this <details>, in the
   HTML, crawlable, and the hub test still finds each live path linked exactly
   once. Folding is a layout act here, never a filter.
   Zero JS: <details> is the browser's own disclosure. */
.ghub-more { margin-top: 2px; }
.ghub-more__toggle {
  cursor: pointer;
  padding: 8px 0;
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.66);
  list-style: none;
}
.ghub-more__toggle::-webkit-details-marker { display: none; }
.ghub-more__toggle::after { content: ' ▾'; }
.ghub-more[open] .ghub-more__toggle::after { content: ' ▴'; }
.ghub-more__toggle:hover,
.ghub-more__toggle:focus-visible { color: var(--eng-green); }
/* The first row inside the disclosure would otherwise sit flush against the
   summary with no rule between them. */
.ghub-more .ghub-list { border-top: 1px solid rgba(255, 255, 255, 0.08); }

/* The empty state is a sentence, never a list. It says the engine already
   exists in Mission and the guide is being written, which is the product
   claim rather than an apology for missing content. */
/* ---------- F · THE EMPTY ENGINES GET A CHIP TOO ----------
   Three heads had a right-hand element and two had a hole. Now all five line
   up, which is what F asked for.

   🚩 DEVIATION FROM THE SPECIFIED COPY, REPORTED RATHER THAN QUIETLY DONE.
   The package named the chip `0 บทความ · อยู่ใน Mission แล้ว`. Rendered, that
   put "อยู่ใน Mission แล้ว" twenty pixels above `ยานลำนี้อยู่ใน Mission แล้ว —
   บทความกำลังต่อ`, the sentence directly below it — the same claim twice, in one
   card. The sentence is the canonical half (it is the product claim, and a test
   pins it verbatim), so the newcomer is what changes.
   `ยังไม่มีบทความ` fills the slot, says the one thing the sentence does not,
   and prints no zero — which also keeps the standing rule that a BARE zero
   beside an engine name reads as a failure, without needing an exception. */
.ghub-group__count--empty {
  color: rgba(255, 255, 255, 0.62);
  font-family: var(--font-body);
  font-size: var(--fs-body-sm);
}
.ghub-empty {
  font-size: var(--fs-body);
  color: rgba(255, 255, 255, 0.68);
  margin: 0 0 10px;
}
.ghub-empty__cta {
  display: inline-block;
  font-size: var(--fs-body-sm);
  font-weight: 600;
  color: var(--eng-green);
  text-decoration: none;
}
.ghub-empty__cta:hover,
.ghub-empty__cta:focus-visible { text-decoration: underline; }

/* The hub's CTA carries the article page's long Thai copy verbatim, so it
   inherits the same overflow at narrow widths — measured there, and fixed the
   same way rather than by shortening the sentence. */
@media (max-width: 640px) {
  .ghub-head > .eng-btn--primary {
    display: flex;
    width: 100%;
    white-space: normal;
    text-align: center;
  }
}

/* ── HUB "จัดเต็ม" (UAT 3.1) — Flight-Deck instruments on the funnel page ──
   Everything here consumes the shared vocabulary: HUD corner brackets, mono
   instrument labels, per-engine hue via --ghub-hue (HUES → hueChannels; never
   retyped). One loud element per zone: the band's CTA, the featured panel's
   CTA, the closing band's CTA. */

.ghub-eyebrow,
.ghub-feat__eyebrow,
.ghub-status {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.78rem;
  letter-spacing: 0.16em;
  color: rgba(255, 255, 255, 0.78);
  margin: 0 0 10px;
}
.ghub-stats {
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.72);
  margin: 0 0 18px;
}
.ghub-stats__n {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-weight: 700;
  color: var(--eng-green);
}

/* featured panel — the newest article, sold big. Committed hero on the right
   (or the craft mark until Jeff commits one on the admin grid). */
.ghub-feat {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(220px, 1fr);
  gap: 24px;
  align-items: center;
  margin: 28px 0 8px;
  padding: 24px 28px;
  border-radius: 18px;
  background: rgb(var(--ghub-hue, 99 53 240) / 0.10);
  border: 1.5px solid rgb(var(--ghub-hue, 99 53 240) / 0.45);
  box-shadow: 0 0 36px rgb(var(--ghub-hue, 99 53 240) / 0.14);
}
.ghub-feat::before,
.ghub-feat::after {
  content: ""; position: absolute; width: 22px; height: 22px;
  border: 2px solid rgb(var(--ghub-hue, 99 53 240) / 0.7);
  pointer-events: none;
}
.ghub-feat::before { top: -2px; left: -2px; border-right: 0; border-bottom: 0; border-top-left-radius: 16px; }
.ghub-feat::after { bottom: -2px; right: -2px; border-left: 0; border-top: 0; border-bottom-right-radius: 16px; }
.ghub-feat__eyebrow { color: rgba(255, 255, 255, 0.85); margin-bottom: 6px; }
.ghub-feat__engine {
  display: inline-block;
  font-size: var(--fs-body-sm);
  font-weight: 600;
  color: rgba(255, 255, 255, 0.85);
  border: 1px solid rgb(var(--ghub-hue, 99 53 240) / 0.5);
  border-radius: 999px;
  padding: 2px 12px;
  margin: 0 0 10px;
}
.ghub-feat__title { font-size: var(--fs-h2); line-height: 1.3; margin: 0 0 10px; }
.ghub-feat__ex {
  color: rgba(255, 255, 255, 0.78);
  margin: 0 0 10px;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.ghub-feat__meta { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.72); margin: 0 0 16px; }
.ghub-feat__visual {
  display: flex; align-items: center; justify-content: center;
  min-height: 200px;
  border-radius: 14px;
  overflow: hidden;
  background: radial-gradient(circle at 50% 45%, rgb(var(--ghub-hue, 99 53 240) / 0.18), rgb(var(--ghub-hue, 99 53 240) / 0) 72%);
}
.ghub-feat__img { display: block; width: 100%; height: 100%; max-height: 280px; object-fit: cover; }
.ghub-feat__visual img:not(.ghub-feat__img) { width: 160px; height: 160px; }
@media (max-width: 880px) {
  .ghub-feat { grid-template-columns: 1fr; padding: 20px; }
  .ghub-feat__visual { order: -1; min-height: 150px; }
  .ghub-feat__visual img:not(.ghub-feat__img) { width: 110px; height: 110px; }
  .ghub-feat__cta { display: flex; width: 100%; justify-content: center; white-space: normal; text-align: center; }
}

/* engine cards join the bracket language — hue at rest, brighter on hover */
.ghub-group::before,
.ghub-group::after {
  content: ""; position: absolute; width: 18px; height: 18px;
  border: 2px solid rgb(var(--ghub-hue, 99 53 240) / 0.5);
  pointer-events: none;
  transition: border-color var(--dur-base, 200ms) var(--ease-out, ease-out);
}
.ghub-group::before { top: -2px; left: -2px; border-right: 0; border-bottom: 0; border-top-left-radius: 14px; }
.ghub-group::after { bottom: -2px; right: -2px; border-left: 0; border-top: 0; border-bottom-right-radius: 14px; }
.ghub-group:hover::before,
.ghub-group:hover::after { border-color: rgb(var(--ghub-hue, 99 53 240) / 0.9); }
.ghub-status {
  font-size: 0.68rem;
  color: rgba(255, 255, 255, 0.78);
  margin: 0 0 6px;
}

/* ── DISCOVERY GRID (UAT 3.1c) — uniform cards over unbalanced columns ──
   Every card = committed hero thumbnail (or the craft mark), engine + subskill
   tags, honest date + NEW badge. The engine hue rides --ghub-hue per card. */
.ghub-disc { margin: 26px 0 18px; }
.ghub-disc__row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.ghub-search {
  flex: 1 1 320px;
  max-width: 460px;
  padding: 11px 16px;
  border-radius: 999px;
  border: 1.5px solid rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.05);
  color: var(--eng-white);
  font-size: var(--fs-body);
}
.ghub-search::placeholder { color: rgba(255, 255, 255, 0.55); }
.ghub-search:focus-visible {
  outline: none;
  border-color: var(--eng-green);
  box-shadow: 0 0 16px color-mix(in srgb, var(--eng-green) 25%, transparent);
}
.ghub-disc__count {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.78);
  margin: 0;
}
.ghub-disc__none {
  color: rgba(255, 255, 255, 0.78);
  margin: 12px 0 0;
}
.ghub-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.ghub-chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 14px;
  border-radius: 999px;
  border: 1.5px solid rgb(var(--ghub-hue, 255 255 255) / 0.4);
  background: rgb(var(--ghub-hue, 255 255 255) / 0.07);
  color: var(--eng-white);
  font-size: var(--fs-body-sm);
  cursor: pointer;
  transition: border-color var(--dur-base, 200ms) var(--ease-out, ease-out),
              background-color var(--dur-base, 200ms) var(--ease-out, ease-out);
}
.ghub-chip:hover { border-color: rgb(var(--ghub-hue, 255 255 255) / 0.8); }
.ghub-chip.is-on {
  border-color: var(--eng-green);
  background: color-mix(in srgb, var(--eng-green) 12%, transparent);
  box-shadow: 0 0 14px color-mix(in srgb, var(--eng-green) 20%, transparent);
}
.ghub-chip__n {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.72rem;
  color: rgba(255, 255, 255, 0.72);
}
.ghub-chip.is-on .ghub-chip__n { color: var(--eng-green); }

.ghub-grid {
  list-style: none;
  margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 18px;
}
.ghub-card {
  position: relative;
  display: flex; flex-direction: column;
  height: 100%;
  border-radius: 16px;
  overflow: hidden;
  text-decoration: none;
  color: var(--eng-white);
  background: rgb(var(--ghub-hue, 99 53 240) / 0.07);
  border: 1.5px solid rgb(var(--ghub-hue, 99 53 240) / 0.30);
  transition: transform var(--dur-base, 200ms) var(--ease-out, ease-out),
              border-color var(--dur-base, 200ms) var(--ease-out, ease-out),
              box-shadow var(--dur-base, 200ms) var(--ease-out, ease-out);
}
.ghub-card:hover,
.ghub-card:focus-visible {
  transform: translateY(-3px);
  border-color: rgb(var(--ghub-hue, 99 53 240) / 0.75);
  box-shadow: 0 8px 32px rgb(var(--ghub-hue, 99 53 240) / 0.22);
}
.ghub-card__visual {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: radial-gradient(circle at 50% 45%, rgb(var(--ghub-hue, 99 53 240) / 0.20), rgb(var(--ghub-hue, 99 53 240) / 0.02) 75%);
}
.ghub-card__img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ghub-card__visual img:not(.ghub-card__img) { width: 84px; height: 84px; }
.ghub-card__new {
  position: absolute; top: 10px; right: 10px;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--eng-green);
  color: var(--eng-ink);
  font-weight: 700;
}
.ghub-card__body { display: flex; flex-direction: column; gap: 8px; padding: 14px 16px 16px; flex: 1; }
.ghub-card__tags { display: flex; flex-wrap: wrap; gap: 6px; }
.ghub-card__tag {
  font-size: 0.72rem;
  padding: 2px 10px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.28);
  color: rgba(255, 255, 255, 0.85);
}
.ghub-card__tag--engine {
  border-color: rgb(var(--ghub-hue, 99 53 240) / 0.6);
  background: rgb(var(--ghub-hue, 99 53 240) / 0.14);
}
.ghub-card__title { font-weight: 700; font-size: var(--fs-body-lg); line-height: 1.4; overflow-wrap: anywhere; }
.ghub-card__ex {
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.72);
  line-height: 1.55;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.ghub-card__meta {
  margin-top: auto;
  font-size: 0.78rem;
  color: rgba(255, 255, 255, 0.66);
}

.ghub-mission { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; margin: 26px 0 8px; }
.ghub-mission__item {
  display: flex; gap: 14px; align-items: center;
  padding: 16px 18px;
  border-radius: 16px;
  border: 1.5px dashed rgb(var(--ghub-hue, 99 53 240) / 0.45);
  background: rgb(var(--ghub-hue, 99 53 240) / 0.05);
}
.ghub-mission__mark img { width: 56px; height: 56px; display: block; }
.ghub-mission__name { color: rgba(255, 255, 255, 0.85); margin: 0 0 6px; font-size: var(--fs-body-sm); }

/* (.ghub-card's reduced-motion kill lives in the FILE-BOTTOM rm tail — the
   (131) lesson: a new rm block outside the tail is how the rm pin goes red.) */

/* closing band — the funnel page ends at an action, not at a list */
.ghub-close {
  text-align: center;
  margin: 40px 0 24px;
  padding: 32px 24px;
  border-radius: 18px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--eng-purple) 16%, transparent), transparent 70%);
}
.ghub-close__title { font-size: var(--fs-h2); line-height: 1.35; margin: 0 0 10px; }
.ghub-close__sub { color: rgba(255, 255, 255, 0.72); margin: 0 0 18px; }
@media (max-width: 640px) {
  .ghub-close .eng-btn--primary { display: flex; width: 100%; justify-content: center; white-space: normal; text-align: center; }
}

/* =================================================================
   FLIGHT DECK — learner-surface instrument vocabulary (2026-09-02).

   The landing page defined the system (docs/design-reference/
   FLIGHT-DECK-SYSTEM.md); this block brings the LEARNER surfaces into it.
   Nothing here mints a colour: every value resolves from var(--eng-*) /
   var(--glow-accent) or an rgba of a brand channel already used above.

   What each addition SAYS (Law 1 — every graphic is a sentence):
   · star depth layer      "you are in the same sky the landing promised"
   · instrument eyebrow    "this label is a live instrument, not a heading"
   · mono numerals         "this value was measured, not typeset"
   · destination breathe   "Saturn is live and waiting — the journey is on"
   · flame flicker         "this craft is under power right now"
   · order-card breathe    "today's mission order is the active instruction"

   Law 3: each section owns ONE accent via --glow-accent, set per section
   class below — children derive, never hardcode.
   Law 2: every loop here oscillates AROUND its rest state (the 0% frame is
   the static appearance), so reduced-motion = animation:none leaves the
   arrived frame. That block sits at the TAIL of this file — position is
   load-bearing (later rule wins the specificity tie).
   ================================================================= */

/* ── Space depth: a second star layer behind every .lrn-home surface
      (dashboard · messages · subscribe). Same recipe as .lp-map::before,
      slower — an app surface is calmer than a sales page. */
.lrn-home { position: relative; }
.lrn-home::before {
  content: "";
  position: absolute; inset: 0;
  z-index: -1;                    /* above the ground, below every card */
  pointer-events: none;
  background-image:
    radial-gradient(circle at 16% 28%, rgba(255, 255, 255, 0.22) 1px, transparent 1.7px),
    radial-gradient(circle at 78% 56%, rgba(255, 255, 255, 0.16) 1px, transparent 1.5px),
    radial-gradient(circle at 40% 82%, rgba(201, 178, 255, 0.20) 1px, transparent 1.7px);
  background-size: 230px 230px, 290px 290px, 350px 350px;
  animation: lrn-sky-drift 110s linear infinite;
}
@keyframes lrn-sky-drift {
  to { background-position: 230px -230px, -290px 290px, 350px 350px; }
}

/* ── Instrument eyebrow (shared pill; began as the landing's .lp-eyebrow,
      which the landing dropped 2026-09-18 — this learner copy stays). Mono stack — Thai glyphs fall through to the system
      Thai face exactly as they do on the landing eyebrows, which shipped
      and walked clean. The dot is the instrument light; it blinks on the
      section's own accent, staggered per instance via --i. */
.lrn-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 13px 5px 10px;
  border: 1px solid color-mix(in srgb, var(--glow-accent) 28%, transparent);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--glow-accent) 7%, transparent);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.82);
}
.lrn-eyebrow::before {
  content: "";
  flex: none;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--glow-accent);
  box-shadow: 0 0 8px color-mix(in srgb, var(--glow-accent) 80%, transparent);
  /* de-dialect 2026-09-03: static — a blinking dot claims live status that
     does not exist (same real feedback that de-dialected the landing). */
}

/* ── Section accents (Law 3). One knob per section; every glow below reads it. */
.lrn-profile  { --glow-accent: var(--eng-purple); }
.lrn-plan     { --glow-accent: var(--eng-purple); }
.lrn-goalcard { --glow-accent: var(--eng-purple); }
.lrn-msgcard  { --glow-accent: var(--eng-purple); }
.lrn-streak   { --glow-accent: var(--eng-flame); }
/* .lrn-today + .lrn-trend + .lrn-badges ride the token default (green = the
   action/earned channel — the doc's own semantic for what those cards say). */

/* ── Mono numerals (the material palette: a measured value wears the
      instrument face). Digits go mono; Thai inside these labels falls to the
      system Thai face — the shipped landing-eyebrow behaviour. */
.lrn-sec-note,
.lrn-ring__cap,
.lrn-lane__val,
.lrn-plan__slot,
.lrn-trend__axis,
.lrn-streak__num b,
.lrn-streak__best { font-family: var(--font-mono); }
.lrn-ring__lvl { font-family: var(--font-mono); font-weight: 700; }
.lrn-ring__lbl { font-family: var(--font-mono); }
.lrn-lane__val { font-weight: 600; }

/* ── Today = the console's ACTIVE ORDER (the page's one loud element, so it
      is the one card that wears HUD chrome). Corner brackets say "framed on
      the console"; the breathe says "live instruction". The base box-shadow
      is repeated in the 0% frame so rest === the static card. */
.lrn-today__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 13px 5px 10px;
  border: 1px solid color-mix(in srgb, var(--eng-green) 30%, transparent);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--eng-green) 8%, transparent);
  font-family: var(--font-mono);
}
.lrn-today__eyebrow::before {
  content: "";
  flex: none;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--eng-green);
  box-shadow: 0 0 8px rgba(57, 255, 137, 0.8);
  /* de-dialect 2026-09-03: static, same reason as .lrn-eyebrow above. */
}
.lrn-today::before,
.lrn-today::after {
  content: "";
  position: absolute;
  width: 18px; height: 18px;
  border: 1px solid color-mix(in srgb, var(--eng-green) 45%, transparent);
  pointer-events: none;
}
.lrn-today::before { top: 10px; left: 10px; border-right: 0; border-bottom: 0; border-top-left-radius: 6px; }
.lrn-today::after  { bottom: 10px; right: 10px; border-left: 0; border-top: 0; border-bottom-right-radius: 6px; }
@keyframes lrn-order-breathe {
  0%, 100% { box-shadow: 0 0 48px rgba(99, 53, 240, 0.30),
                         0 0 0 1px color-mix(in srgb, var(--eng-green) 16%, transparent); }
  50%      { box-shadow: 0 0 48px rgba(99, 53, 240, 0.30),
                         0 0 0 1px color-mix(in srgb, var(--eng-green) 42%, transparent),
                         0 0 24px color-mix(in srgb, var(--eng-green) 20%, transparent); }
}
.lrn-today { animation: lrn-order-breathe 4.2s ease-in-out infinite; }

/* ── The destination is LIVE (fleet card). Saturn's body breathes its halo;
      rest = the static drop-shadow it always had. */
@keyframes lrn-dest-breathe {
  0%, 100% { filter: drop-shadow(0 0 8px rgba(99, 53, 240, 0.55)); }
  50%      { filter: drop-shadow(0 0 16px rgba(99, 53, 240, 0.85)); }
}
.lrn-fleet__body { animation: lrn-dest-breathe 4.6s ease-in-out infinite; }

/* ── A craft under power flickers its exhaust — only .is-started lanes have
      the flame at all, so this cannot animate a drifted craft into looking
      powered. Staggered per lane (lockstep reads as one mechanism twitching). */
@keyframes lrn-flame-flicker {
  0%, 100% { opacity: 1; }
  45%      { opacity: 0.6; }
  62%      { opacity: 0.9; }
}
.lrn-lane.is-started .lrn-lane__craft::before { animation: lrn-flame-flicker 2.2s ease-in-out infinite; }
.lrn-lane:nth-child(2) .lrn-lane__craft::before { animation-delay: 0.5s; }
.lrn-lane:nth-child(3) .lrn-lane__craft::before { animation-delay: 0.9s; }
.lrn-lane:nth-child(4) .lrn-lane__craft::before { animation-delay: 1.4s; }
.lrn-lane:nth-child(5) .lrn-lane__craft::before { animation-delay: 1.8s; }

/* ── A live streak breathes in the flame channel; a dead one already reads
      OUT (colour + filter stripped above) and this selector never lights it. */
@keyframes lrn-flame-breathe {
  0%, 100% { filter: drop-shadow(0 0 12px rgba(255, 107, 44, 0.5)); }
  50%      { filter: drop-shadow(0 0 18px rgba(255, 107, 44, 0.8)); }
}
.lrn-streak--today .lrn-streak__flame svg,
.lrn-streak--alive .lrn-streak__flame svg,
.lrn-streak--at_risk .lrn-streak__flame svg { animation: lrn-flame-breathe 3s ease-in-out infinite; }

/* ── QUIZ ARENA (Batch 2). The arena is where the mission is FLOWN, so it
      wears the same sky and the same instrument faces as the deck above.
      What each addition says:
      · star layer        "the arena is inside the same sky"
      · eyebrow accents   idle/question = purple (mission identity) ·
                          complete = green (earned) · subscribe = yellow
                          (highlight, not alarm) — one accent per state,
                          and states are mutually exclusive on screen
      · mono numerals     the question counter, momentum count and the
                          three summary stats are MEASURED values
      · progress shimmer  "energy is flowing along the session" — the rest
                          frame (0%) is a lit fill with a static sheen, so
                          reduced-motion still reads as progress, not a bug
      · stats brackets    the summary readout is telemetry, framed like
                          the dashboard's order card */
.lrn-arena::before {
  content: "";
  position: absolute; inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(circle at 22% 24%, rgba(255, 255, 255, 0.20) 1px, transparent 1.7px),
    radial-gradient(circle at 72% 62%, rgba(255, 255, 255, 0.14) 1px, transparent 1.5px),
    radial-gradient(circle at 46% 88%, rgba(201, 178, 255, 0.18) 1px, transparent 1.7px);
  background-size: 240px 240px, 300px 300px, 360px 360px;
  animation: lrn-sky-drift 120s linear infinite;
}

/* Law 3 — one accent knob per state (states never co-display). */
.lrn-arena [data-state="idle"],
.lrn-arena [data-state="question"] { --glow-accent: var(--eng-purple); }
.lrn-arena [data-state="subscribe"] { --glow-accent: var(--eng-yellow); }
/* [data-state="complete"] rides the token default green — earned channel. */

/* Measured values wear the instrument face. */
.lrn-arena .lrn-momentum { font-family: var(--font-mono); font-weight: 700; }
.lrn-stats-glass b { font-family: var(--font-mono); font-weight: 700; }

/* Momentum bar: light travels along the fill. The gradient's 0% frame is a
   lit green fill with a fixed sheen near its head — the arrived state. */
.lrn-progress--arena .eng-progress__fill {
  background-image: linear-gradient(100deg,
    var(--eng-green) 42%,
    rgba(255, 255, 255, 0.5) 50%,
    var(--eng-green) 58%);
  background-size: 200% 100%;
  animation: eng-shimmer 3.6s ease-in-out infinite;
}

/* Summary readout = telemetry: HUD corner brackets in the earned channel. */
.lrn-stats-glass { position: relative; }
.lrn-stats-glass::before,
.lrn-stats-glass::after {
  content: "";
  position: absolute;
  width: 18px; height: 18px;
  pointer-events: none;
  border: 1px solid color-mix(in srgb, var(--glow-accent) 45%, transparent);
}
.lrn-stats-glass::before {
  top: -5px; left: -5px;
  border-right: none; border-bottom: none;
  border-top-left-radius: 6px;
}
.lrn-stats-glass::after {
  bottom: -5px; right: -5px;
  border-left: none; border-top: none;
  border-bottom-right-radius: 6px;
}

/* ── PLACEMENT FLOW — Flight Deck pass (UAT free@ 2026-09-03; was the Batch-3
      light-surface vocabulary). The test is now flown on the SPACE ground
      (body.eng-bg-space + eng-main--app sky) as 6 legs of 5 questions —
      src/services/placement-chrome.js draws the HUD, this block dresses it.
      · .plc-arena           page wrapper; carries --leg-hue from the server
      · .plc-hud / .plc-wp   route strip: 6 waypoints, each in its skill hue
      · .plc-card            dark glass instrument panel (lrn-card recipe) with
                             HUD corner brackets in the leg's hue
      · .plc-ask / .plc-doc  structured stem typography (stem-render.js): loud
                             question, quiet English instruction, document panel
      · .plc-input/.plc-option  dark instrument controls (both consumers —
                             question + result — fly on space now)
      · .plc-pass__*         the REAL boarding pass (report page) — stays
                             LIGHT: the pass is also the PDF (puppeteer prints
                             this page), and paper prints white.
      · .lrn-eyebrow--light  kept: the instrument pill for LIGHT grounds. */
.lrn-eyebrow--light { color: color-mix(in srgb, var(--eng-ink) 82%, transparent); }
.lrn-eyebrow--light {
  border-color: color-mix(in srgb, var(--glow-accent) 45%, transparent);
  background: color-mix(in srgb, var(--glow-accent) 9%, transparent);
}

/* Measured values wear the instrument face (landing stats row). */
.plc-stats b { font-family: var(--font-mono); font-weight: 700; }

/* Inputs: dark glass rest → leg-hue focus ring. focus-visible ONLY would skip
   mouse focus on a text field, where the ring is the "you are typing here"
   signal — plain :focus is correct for inputs (unlike buttons). */
.plc-input {
  width: 100%;
  padding: 12px 14px;
  border: 1.5px solid var(--hairline-dark);
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.06);
  color: var(--eng-white);
  font-size: var(--fs-body);
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.plc-input::placeholder { color: rgba(255, 255, 255, 0.45); }
.plc-input:hover { border-color: rgba(255, 255, 255, 0.28); }
.plc-input:focus {
  outline: none;
  border-color: var(--leg-hue, var(--eng-purple));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 28%, transparent);
}
/* a <select> needs an opaque popover-friendly ink on some platforms; options
   render on the UA's own panel, so only the closed control needs dressing */
select.plc-input option { color: var(--eng-ink); background: var(--eng-white); }

/* Answer options: a choice is a control, and a chosen one says so. */
.plc-option {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px;
  border: 1.5px solid var(--hairline-dark);
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.045);
  cursor: pointer;
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out), background-color var(--dur-base) var(--ease-out);
}
.plc-option:hover {
  border-color: color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 55%, transparent);
  background: rgba(255, 255, 255, 0.07);
}
.plc-option:has(input:focus-visible),
.plc-option:has(input:checked) {
  border-color: var(--leg-hue, var(--eng-purple));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 24%, transparent);
  background: color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 12%, transparent);
}
.plc-option input { accent-color: var(--leg-hue, var(--eng-purple)); }
.plc-option__key {
  flex: none;
  width: 28px; height: 28px;
  display: grid; place-items: center;
  border: 1.5px solid var(--hairline-dark);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono); font-weight: 700; font-size: 12px;
  color: rgba(255, 255, 255, 0.75);
}
.plc-option:has(input:checked) .plc-option__key {
  border-color: var(--leg-hue, var(--eng-purple));
  color: var(--eng-white);
}
.plc-option__text { color: var(--eng-white); }

/* ── THE FLIGHT (plc-arena): page wrapper for all four placement surfaces.
      --leg-hue arrives inline from placement-chrome.js (skill identity map —
      one source, never retyped); everything below reads it with a purple
      fallback so the probe leg + landing render without one. */
.plc-arena { --glow-accent: var(--leg-hue, var(--eng-purple)); }

/* ── HUD: route strip + leg line. The strip is the ANSWER to "โห 30 ข้อ" —
      six waypoints you can count on one hand, each in its skill's own hue. */
.plc-hud { margin-bottom: 18px; }
.plc-hud__route {
  display: flex; align-items: center; justify-content: center;
  gap: 0;
  margin-bottom: 14px;
}
.plc-wp {
  flex: none;
  display: grid; place-items: center;
  width: 38px; height: 38px;
  border: 1.5px solid var(--hairline-dark);
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.04);
}
.plc-wp img { border-radius: 50%; opacity: 0.38; filter: grayscale(0.6); transition: opacity var(--dur-base) var(--ease-out), filter var(--dur-base) var(--ease-out); }
.plc-wp__pip { font-size: 13px; color: rgba(255, 255, 255, 0.4); }
.plc-wp.is-done,
.plc-wp.is-now {
  border-color: color-mix(in srgb, var(--wp-hue, var(--eng-purple)) 65%, transparent);
  background: color-mix(in srgb, var(--wp-hue, var(--eng-purple)) 14%, transparent);
}
.plc-wp.is-done img, .plc-wp.is-now img { opacity: 1; filter: none; }
.plc-wp.is-done .plc-wp__pip, .plc-wp.is-now .plc-wp__pip { color: var(--eng-white); }
.plc-wp.is-now {
  box-shadow: 0 0 12px color-mix(in srgb, var(--wp-hue, var(--eng-purple)) 45%, transparent);
  animation: plc-wp-pulse 2.6s ease-in-out infinite;
}
.plc-wp__track {
  flex: 1 1 10px;
  max-width: 42px;
  height: 2px;
  background: repeating-linear-gradient(90deg,
    rgba(255, 255, 255, 0.22) 0 4px, transparent 4px 9px);
}
.plc-wp__track.is-done {
  background: repeating-linear-gradient(90deg,
    color-mix(in srgb, var(--eng-green) 65%, transparent) 0 4px, transparent 4px 9px);
}
@keyframes plc-wp-pulse {
  0%, 100% { box-shadow: 0 0 8px  color-mix(in srgb, var(--wp-hue, var(--eng-purple)) 30%, transparent); }
  50%      { box-shadow: 0 0 18px color-mix(in srgb, var(--wp-hue, var(--eng-purple)) 60%, transparent); }
}

.plc-hud__row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap;
}
.plc-leg { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.plc-leg__craft { border-radius: 50%; flex: none; }
.plc-leg__pip { color: var(--leg-hue, var(--eng-purple)); }
.plc-leg__id {
  font-family: var(--font-mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--eng-white);
  padding: 3px 8px;
  border: 1px solid color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 55%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 16%, transparent);
}
.plc-leg__name { font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--eng-white); }
.plc-leg__pos { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.78); }
.plc-hud__meta {
  font-family: var(--font-mono); font-size: 11.5px; letter-spacing: 0.06em;
  color: rgba(255, 255, 255, 0.62);
}

/* Leg-start moment: a banner, not a page — one breath of "new engine". */
.plc-legstart {
  display: flex; align-items: center; gap: 14px;
  max-width: 640px; margin: 0 auto 16px;
  padding: 12px 16px;
  border: 1.5px solid color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 55%, transparent);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 12%, transparent);
  animation: plc-slidein 480ms var(--ease-out) both;
}
.plc-legstart img { border-radius: 50%; flex: none; }
.plc-legstart__pip { font-size: 26px; color: var(--leg-hue, var(--eng-purple)); }
.plc-legstart__kick {
  font-family: var(--font-mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.75);
}
.plc-legstart__name { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--eng-white); }
@keyframes plc-slidein {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}

/* ── The instrument panel (question card): lrn-card glass + HUD corner
      brackets in the leg's hue (Flight Deck signature). */
.plc-card {
  position: relative;
  max-width: 640px; margin: 0 auto;
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-xl);
  padding: clamp(18px, 4vw, 28px);
  box-shadow: 0 0 40px color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 12%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  animation: plc-slidein 420ms var(--ease-out) both;
}
.plc-card::before,
.plc-card::after {
  content: ""; position: absolute;
  width: 16px; height: 16px;
  border: 2px solid color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 70%, transparent);
  pointer-events: none;
}
.plc-card::before { top: -2px; left: -2px; border-right: none; border-bottom: none; border-top-left-radius: 8px; }
.plc-card::after  { bottom: -2px; right: -2px; border-left: none; border-top: none; border-bottom-right-radius: 8px; }

/* The Thai task line — the command, in the reader's language, first. */
.plc-task {
  display: flex; align-items: center; gap: 8px;
  font-size: var(--fs-body-sm); font-weight: 700;
  color: rgba(255, 255, 255, 0.92);
  margin-bottom: 12px;
}
.plc-task::before {
  content: ""; flex: none;
  width: 8px; height: 8px; border-radius: 2px;
  background: var(--leg-hue, var(--eng-purple));
  box-shadow: 0 0 8px color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 60%, transparent);
}

/* ── Structured stem typography (stem-render.js). The QUESTION is the loud
      element; the English instruction is quiet; a document is a document. */
.plc-stem-intro {
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.78);
  margin-bottom: 10px;
}
.plc-ask {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-h3);
  line-height: 1.5;
  color: var(--eng-white);
  margin-bottom: 6px;
}
.plc-doc {
  margin: 12px 0;
  padding: 14px 16px;
  border: 1px solid var(--hairline-dark);
  border-left: 3px solid color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 70%, transparent);
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.05);
}
.plc-doc p { color: rgba(255, 255, 255, 0.92); margin-bottom: 6px; }
.plc-doc p:last-child { margin-bottom: 0; }
.plc-doc__head {
  font-family: var(--font-mono); font-weight: 700; font-size: 11.5px;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.68);
  padding-bottom: 8px; margin-bottom: 8px;
  border-bottom: 1px dashed rgba(255, 255, 255, 0.18);
}
.plc-doc__msg {
  font-family: var(--font-mono); font-size: 12px;
  color: rgba(255, 255, 255, 0.66);
  margin-top: 10px;
}
.plc-doc__list { list-style: none; margin: 6px 0; }
.plc-doc__list li {
  position: relative; padding-left: 18px; margin-bottom: 4px;
  color: rgba(255, 255, 255, 0.92);
}
.plc-doc__list li::before { content: "•"; position: absolute; left: 4px; color: var(--leg-hue, var(--eng-purple)); }

/* Speaking: the read-aloud SCRIPT panel + task-clarity hint. */
.plc-doc--script { border-left-color: color-mix(in srgb, var(--eng-green) 60%, transparent); }
.plc-script { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-h3); line-height: 1.55; color: var(--eng-white); }
.plc-script-hint { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.72); }
.plc-voice-note {
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.75);
  margin-bottom: 14px;
}
.plc-voice-note a { color: var(--eng-purple-soft); text-decoration: underline; }
.plc-mic-status { font-family: var(--font-mono); font-size: 12px; color: rgba(255, 255, 255, 0.7); }
.plc-fb-label { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.72); margin-bottom: 4px; }
.plc-fb-score { font-family: var(--font-mono); font-weight: 700; font-size: var(--fs-h3); color: var(--eng-white); margin-bottom: 12px; }
.plc-fb-transcript { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.65); }

/* ── Audio instrument row (listening leg). */
.plc-audio {
  display: flex; flex-direction: column; gap: 8px;
  margin: 12px 0;
  padding: 12px 14px;
  border: 1px solid var(--hairline-dark);
  border-left: 3px solid color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 70%, transparent);
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.05);
}
.plc-audio__label {
  font-family: var(--font-mono); font-weight: 700; font-size: 11.5px;
  letter-spacing: 0.1em; color: rgba(255, 255, 255, 0.7);
}
.plc-audio audio { width: 100%; }
.plc-audio__fail { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.85); }

/* ── Momentum line under the submit control: the next click's meaning. */
.plc-momentum {
  margin-top: 10px;
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.72);
}
.plc-momentum b { color: var(--eng-white); }

/* ── Landing: the flight plan preview. */
.plc-hero-sub { color: rgba(255, 255, 255, 0.85); }
.plc-plan {
  max-width: 560px;
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-xl);
  background: rgba(255, 255, 255, 0.04);
  padding: 18px 20px;
  text-align: left;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.plc-plan__head {
  font-family: var(--font-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.1em;
  color: rgba(255, 255, 255, 0.75);
  padding-bottom: 10px; margin-bottom: 10px;
  border-bottom: 1px dashed rgba(255, 255, 255, 0.16);
}
.plc-plan__legs { list-style: none; }
.plc-plan__leg {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 0;
}
.plc-plan__leg img { border-radius: 50%; flex: none; }
/* (133) lesson re-applied: hue-as-TEXT fails the audit (color-mix in `color:`
   parses as black) and fails AA anyway — the label stays white mono, the HUE
   lives on the chip's border/tint, same recipe as .plc-leg__id. */
.plc-plan__id {
  font-family: var(--font-mono); font-weight: 700; font-size: 10.5px;
  letter-spacing: 0.1em;
  color: var(--eng-white);
  padding: 2px 7px;
  border: 1px solid color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 55%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--leg-hue, var(--eng-purple)) 16%, transparent);
}
.plc-plan__name { font-family: var(--font-display); font-weight: 700; font-size: 14.5px; color: var(--eng-white); flex: 1; }
.plc-plan__len { font-family: var(--font-mono); font-size: 11px; color: rgba(255, 255, 255, 0.6); }
.plc-plan__foot {
  margin-top: 10px; padding-top: 10px;
  border-top: 1px dashed rgba(255, 255, 255, 0.16);
  font-size: var(--fs-body-sm);
  color: rgba(255, 255, 255, 0.75);
}
.plc-stat-num { color: var(--eng-white); }
.plc-stat-cap { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.68); }

/* ── Result: what-you-earned list + form dressing. */
.plc-earned { list-style: none; max-width: 560px; margin-left: auto; margin-right: auto; }
.plc-earned li {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 12px 14px; margin-bottom: 8px;
  border: 1px solid var(--hairline-dark);
  border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, 0.04);
}
.plc-earned li b { display: block; color: var(--eng-white); margin-bottom: 2px; }
.plc-earned li span { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.75); }
.plc-form-head {
  font-family: var(--font-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.1em;
  color: rgba(255, 255, 255, 0.75);
}
.plc-label { display: block; font-size: var(--fs-body-sm); font-weight: 700; color: var(--eng-white); margin-bottom: 4px; }
.plc-hint { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.7); }
.plc-consent { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.88); }

/* the report page rides the space ground (UAT free@ round 3 — the spaceport);
   this sub-line sits on dark ON SCREEN and goes back to slate at print, where
   print.css has already put the body on white paper */
.plc-report__sub { font-size: var(--fs-body-sm); color: rgba(255, 255, 255, 0.78); }
/* the INK TRAP (UAT 3.1b lesson, third surface): colors_and_type.css paints
   bare <p> in --fg-primary ink, which beats the body's white INHERIT on the
   dark ground — the audit caught the exit sub-copy at 1.07:1. :where() keeps
   this at (0,0,1): it wins the tie with the token rule on source order
   (learner.css loads after the token sheets) and still LOSES to every classed
   color — so the light cards' own ink (.plc-reco sets it on the parent, and
   inherit resolves to the NEAREST ancestor) survives untouched. */
:where(body.eng-bg-space .plc-report) p { color: inherit; }

/* ── EARTH SPACEPORT scenery (screen-only; the whole set hides at print).
      A departures board above the pass + a launch pad under the exit. Every
      string on the board echoes data already on the page — the board is
      terminal furniture, not a new instrument. Status text is STATIC: a
      blinking light would claim live state this page does not have. */
/* a real terminal board, not a metadata strip (Jeff round 3.1: "ยังไม่ถึง"):
   hangs slightly WIDER than the pass below it, bigger LED-style values */
.plc-port {
  max-width: 660px; margin: 0 auto 22px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: var(--radius-lg);
  background: rgba(0, 0, 0, 0.35);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}
.plc-port__head {
  margin: 0; padding: 10px 20px;
  font-family: var(--font-mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.22em;
  color: rgba(255, 255, 255, 0.82);
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.05);
}
.plc-port__row {
  display: flex; flex-wrap: wrap; gap: 12px 34px;
  padding: 16px 20px;
}
.plc-port__cell { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.plc-port__k {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.16em;
  color: rgba(255, 255, 255, 0.6);
}
.plc-port__v {
  font-family: var(--font-mono); font-weight: 700; font-size: 15px;
  letter-spacing: 0.03em;
  color: var(--eng-white);
}
/* BOARDING wears the action green — state colour, held STATIC (a soft glow is
   surface, not a blink) */
.plc-port__v--go {
  color: var(--eng-green);
  text-shadow: 0 0 12px color-mix(in srgb, var(--eng-green) 55%, transparent);
}

/* the launch pad: craft (the EXIT's own destination skill) hovering over a
   hue-lit pad ring on a ground line — sized to be a SCENE, not an icon
   (Jeff round 3.1); the Earth limb below is what makes it จุดเริ่มต้นบนโลก */
.plc-launch { position: relative; }
.plc-pad {
  position: relative;
  width: 280px; height: 170px;
  margin: 0 auto 14px;
}
.plc-pad__craft {
  /* top 22px LANDS the craft: its glyph base sits INSIDE the pad ellipse
     (past the ring's far rim, near its centre) so "จอดรอ" reads as parked —
     perched on the far rim reads as hovering a ship-height above the pad
     (Jeff's gap, 2026-09-05). The ±8px idle bob stays within the ellipse. */
  position: absolute; left: 50%; top: 22px;
  width: 128px; height: 128px;
  transform: translateX(-50%);
  filter: drop-shadow(0 14px 24px color-mix(in srgb, var(--pad-hue, var(--eng-purple)) 45%, transparent));
  animation: plc-pad-hover 6s var(--ease-in-out) infinite alternate;
}
.plc-pad__ring {
  position: absolute; left: 50%; bottom: 16px;
  width: 210px; height: 46px;
  transform: translateX(-50%);
  border-radius: 50%;
  border: 1.5px solid color-mix(in srgb, var(--pad-hue, var(--eng-purple)) 60%, transparent);
  box-shadow:
    0 0 34px color-mix(in srgb, var(--pad-hue, var(--eng-purple)) 38%, transparent),
    inset 0 0 26px color-mix(in srgb, var(--pad-hue, var(--eng-purple)) 26%, transparent);
}
.plc-pad__ground {
  position: absolute; left: 50%; bottom: 12px;
  width: 280px; height: 2px;
  transform: translateX(-50%);
  border-radius: var(--radius-pill);
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, var(--pad-hue, var(--eng-purple)) 50%, rgba(255, 255, 255, 0.32)),
    transparent);
}
@keyframes plc-pad-hover {
  from { transform: translateX(-50%) translateY(0); }
  to { transform: translateX(-50%) translateY(-8px); }
}
.plc-pad__cap {
  font-family: var(--font-mono); font-size: 12.5px; letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.85);
  margin: 0 0 14px;
}

/* ── THE EARTH LIMB — the planet's curved edge rising under the launch pad,
      so the exit section STANDS ON GROUND instead of floating in the void.
      Full-bleed via viewport units, clipped: the element clips its own giant
      ellipse (overflow hidden) and .plc-main clips the x-overflow at <main>
      (overflow-x: clip — no scroll container minted). Static scenery; the
      whole .plc-launch zone is already display:none at print. */
.plc-main { overflow-x: clip; }
.plc-earth {
  position: absolute;
  left: 50%; top: 0; bottom: -150px;
  width: 220vw;
  transform: translateX(-50%);
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
}
.plc-earth::before {
  /* the planet body — a huge ellipse whose top arc is the horizon line,
     passing just under the pad's ground line. The fill is a GRADIENT, not a
     flat 9% mix (Jeff's gap, 2026-09-05): flat-9% was near-invisible against
     the sky, so the ground read as a thin stripe and the exit copy "fell off
     the planet". Bright at the limb → fades toward space by ~600px down, so
     the element's clipped bottom edge never shows as a seam either. */
  content: ""; position: absolute; left: 50%; top: 152px;
  width: 260vw; height: 2400px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--eng-purple) 26%, var(--eng-space)) 0,
    color-mix(in srgb, var(--eng-purple) 13%, var(--eng-space)) 200px,
    var(--eng-space) 330px);
  /* the 330px end is LOAD-BEARING: .plc-earth clips ~340px below the ellipse
     top (launch height − 152 + the 150px overhang), so the fade must reach
     space colour BEFORE the clip or the cut edge paints its own seam —
     measured at y=1721 on the first draft of this gradient. */
  border-top: 1.5px solid color-mix(in srgb, var(--eng-purple) 55%, rgba(255, 255, 255, 0.25));
  box-shadow: inset 0 30px 70px color-mix(in srgb, var(--eng-purple) 20%, transparent);
}
.plc-earth::after {
  /* atmosphere glow CENTRED ON the limb apex (152px), fading out in every
     direction — the old box ended 88px BELOW the horizon with the radial at
     its maximum on that edge, painting a razor-straight bright-to-dark cut
     right under the exit h2 (the seam Jeff saw). A glow that fades on all
     sides cannot have an edge. */
  content: ""; position: absolute; left: 0; right: 0; top: 32px; height: 240px;
  background: radial-gradient(ellipse 46% 60% at 50% 50%,
    color-mix(in srgb, var(--eng-purple) 22%, transparent), transparent 70%);
}

/* ── THE PASS. Geometry mirrors the landing sample (.lp-report__*) so sample
      and real agree in FORM as the drift pins hold them to in WORDS; adapted
      where the grounds differ (notches bite the PAGE colour here, not space).
      One copy per surface is deliberate: the sample's reveal states live in
      landing.css and would drag .lp-reveal semantics in here. */
.plc-pass {
  position: relative;
  max-width: 600px;
  margin: 0 auto;
  background: var(--eng-white);
  color: var(--eng-ink);
  border: 2px solid var(--eng-black);
  border-radius: var(--radius-xl);
  padding: 0 0 24px;
  box-shadow: var(--shadow-sticker-lg);
}
.plc-pass__strip {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 14px 22px;
  border-bottom: 2px dashed color-mix(in srgb, var(--eng-ink) 20%, transparent);
  border-radius: calc(var(--radius-xl) - 2px) calc(var(--radius-xl) - 2px) 0 0;
  background: color-mix(in srgb, var(--eng-purple) 7%, var(--eng-white));
}
.plc-pass__brand {
  font-family: var(--font-display); font-weight: 800;
  font-size: 14px; letter-spacing: 0.14em; color: var(--eng-ink);
}
.plc-pass__tag {
  font-family: var(--font-mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.12em; color: var(--eng-slate);
}
.plc-pass__body { display: grid; grid-template-columns: 1fr auto 152px; }
.plc-pass__main { padding: 20px 22px 0; min-width: 0; }
.plc-pass__route {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px;
  padding-bottom: 16px; margin-bottom: 14px;
  border-bottom: 1px solid color-mix(in srgb, var(--eng-ink) 12%, transparent);
}
.plc-pass__field { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.plc-pass__field--to { text-align: right; }
.plc-pass__flabel { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.12em; color: var(--eng-slate); }
.plc-pass__fval { font-family: var(--font-display); font-weight: 700; font-size: 15px; color: var(--eng-ink); overflow-wrap: anywhere; }
.plc-pass__mval { font-family: var(--font-mono); font-weight: 700; font-size: 12.5px; color: var(--eng-ink); }
.plc-pass__leg {
  position: relative; height: 2px;
  background: repeating-linear-gradient(90deg,
    color-mix(in srgb, var(--eng-ink) 28%, transparent) 0 4px, transparent 4px 9px);
}
.plc-pass__leg::after {
  content: ""; position: absolute; top: 50%; left: 0;
  width: 7px; height: 7px; margin-top: -3.5px;
  border-radius: 50%;
  background: var(--eng-purple);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--eng-purple) 22%, transparent);
  animation: plc-pass-leg 5.5s var(--ease-in-out) infinite;
}
@keyframes plc-pass-leg { 0%, 100% { left: 0; } 50% { left: calc(100% - 7px); } }
.plc-pass__meta {
  display: flex; gap: 26px;
  padding-bottom: 16px; margin-bottom: 16px;
  border-bottom: 1px solid color-mix(in srgb, var(--eng-ink) 12%, transparent);
}
.plc-pass__h3 { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--eng-ink); margin: 0 0 14px; }
.plc-pass__bars { display: flex; flex-direction: column; gap: 14px; padding-bottom: 4px; }
.plc-pass__bar { position: relative; padding-left: 20px; }
.plc-pass__bar::before {
  content: ""; position: absolute; left: 4px; top: 4px; bottom: -14px; width: 1px;
  background: color-mix(in srgb, var(--eng-ink) 14%, transparent);
}
.plc-pass__bar:last-child::before { bottom: auto; height: 8px; }
.plc-pass__node {
  position: absolute; left: 0; top: 3px;
  width: 9px; height: 9px; border-radius: 50%;
  border: 2px solid var(--eng-purple);
  background: var(--eng-purple);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--eng-purple) 18%, transparent);
}
/* a skipped skill's checkpoint stays HOLLOW — the report must not claim a
   measurement it does not have (Law 5) */
.plc-pass__bar--na .plc-pass__node { background: var(--eng-white); border-color: color-mix(in srgb, var(--eng-ink) 35%, transparent); box-shadow: none; }
.plc-pass__bar--na .plc-pass__bar-val { color: var(--eng-slate); }
.plc-pass__bar-row { display: flex; justify-content: space-between; gap: 10px; margin-bottom: 5px; }
.plc-pass__bar-name { font-size: 13.5px; font-weight: 700; color: var(--eng-ink); text-transform: capitalize; }
.plc-pass__bar-val { font-family: var(--font-mono); font-size: 12.5px; font-weight: 700; color: var(--eng-ink); }
.plc-pass__track { height: 10px; border-radius: var(--radius-pill); background: color-mix(in srgb, var(--eng-ink) 7%, var(--eng-white)); overflow: hidden; }
.plc-pass__fill {
  display: block; height: 100%;
  border-radius: var(--radius-pill);
  background: var(--eng-purple);
  transform-origin: left center;
  animation: plc-draw 1.1s var(--ease-out) both;
}
@keyframes plc-draw { from { transform: scaleX(0); } }
.plc-pass__bar:nth-child(1) .plc-pass__fill { animation-delay: .10s; }
.plc-pass__bar:nth-child(2) .plc-pass__fill { animation-delay: .22s; }
.plc-pass__bar:nth-child(3) .plc-pass__fill { animation-delay: .34s; }
.plc-pass__bar:nth-child(4) .plc-pass__fill { animation-delay: .46s; }
.plc-pass__bar:nth-child(5) .plc-pass__fill { animation-delay: .58s; }
/* the tear line — the notches bite the SPACE ground on screen (the page went
   dark at the spaceport pass) and flip back to paper white at print */
.plc-pass__perf {
  position: relative; width: 0;
  border-left: 2px dashed color-mix(in srgb, var(--eng-ink) 22%, transparent);
  margin: 14px 0;
}
.plc-pass__notch {
  position: absolute; left: -10px; width: 18px; height: 18px;
  border-radius: 50%;
  background: var(--eng-space);
  border: 2px solid var(--eng-black);
}
.plc-pass__notch--top { top: -23px; }
.plc-pass__notch--bot { bottom: -23px; }
.plc-pass__stub {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: 26px 14px 0; text-align: center;
}
.plc-pass__level-label { display: block; font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; color: var(--eng-slate); }
.plc-pass__level-val { font-family: var(--font-display); font-weight: 800; font-size: 46px; color: var(--eng-purple); line-height: 1; }
.plc-pass__level-conf { display: block; font-size: 11px; color: var(--eng-slate); }
.plc-pass__code {
  margin-top: 14px; width: 100%; height: 42px;
  /* stripes 2-3px minimum: the PDF rasterises this page and 1px bars
     aliased to near-invisible on paper */
  background: repeating-linear-gradient(90deg,
    var(--eng-ink) 0 3px, transparent 3px 6px,
    var(--eng-ink) 6px 8px, transparent 8px 13px,
    var(--eng-ink) 13px 17px, transparent 17px 20px);
  opacity: .85;
}
/* narrow: the stub folds under the main half; the tear line goes horizontal */
@media (max-width: 560px) {
  .plc-pass__body { display: block; }
  .plc-pass__perf {
    width: auto; height: 0;
    border-left: none;
    border-top: 2px dashed color-mix(in srgb, var(--eng-ink) 22%, transparent);
    margin: 6px 14px;
  }
  .plc-pass__notch { top: -10px; }
  .plc-pass__notch--top { left: -23px; }
  .plc-pass__notch--bot { left: auto; right: -23px; bottom: auto; }
  .plc-pass__stub { padding-top: 18px; }
  .plc-pass__meta { flex-wrap: wrap; gap: 12px 22px; }
}

/* จุดที่ต้องพัฒนา — the one loud element after the pass (same treatment as the
   landing sample's weakness sticker, so sample and real stay one artifact) */
.plc-weak {
  background: var(--eng-yellow);
  border: 2px solid var(--eng-black);
  border-radius: var(--radius-lg);
  padding: 16px 18px;
  transform: rotate(-1deg);
}
.plc-weak__h { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--eng-ink); margin: 0 0 8px; }
.plc-weak__list { margin: 0; padding-left: 18px; }
.plc-weak__list li { font-size: 13.5px; color: var(--eng-ink); line-height: 1.7; }

/* Recommendation — green as a TINT with a deep-green rail, per the brand
   table's own rule (green full fields are for CTAs, not paragraphs) */
.plc-reco {
  background: color-mix(in srgb, var(--eng-green) 14%, var(--eng-white));
  border: 1.5px solid color-mix(in srgb, var(--eng-green-deep) 45%, transparent);
  border-left: 4px solid var(--eng-green-deep);
  border-radius: var(--radius-lg);
  padding: 16px 18px;
  /* the card is a light tint on the dark spaceport ground — its own ink,
     never the page's white (which the body now inherits down) */
  color: var(--eng-ink);
}
.plc-reco__h { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--eng-ink); margin: 0 0 10px; }

/* The PDF is a print of this page (puppeteer renders with print media):
   nothing may be caught mid-animation on paper, and the SPACEPORT is
   screen-only scenery — print.css already forces the body back to white
   paper (background/color !important), so this block removes everything
   that only makes sense on the dark ground: the star-sky pseudo-layers,
   the departures board, and the launch-pad zone (its CTA is print-hidden
   in print.css; the pad + copy pointing at a hidden button go with it). */
@media print {
  .plc-pass__fill { animation: none; }
  .plc-pass__leg::after { animation: none; }
  .plc-pass { box-shadow: none; }
  body.eng-bg-space .eng-main--app::before,
  body.eng-bg-space .eng-main--app::after { content: none; }
  .plc-port,
  .plc-launch { display: none !important; }
  .plc-report__sub { color: var(--eng-slate); }
  .plc-pass__notch { background: var(--eng-white); }
}

/* ── MISSION MAP · BRIEF · MESSAGES · ONBOARDING (Batch 4).
      Same vocabulary, four more rooms of the same ship:
      · sky on the standalone shells (journey + brief mount their column
        INSIDE <main>, so the layer rides main:has(...) — stars confined to
        a 720px column would read as a wallpaper strip, not a sky)
      · mono numerals on every measured value (mission counts, timing line,
        XP/badge/position chips, message timestamps, the CEFR badge)
      · the journey's "ภารกิจถัดไปของคุณ" card wears the SAME HUD corner
        brackets as the dashboard's order card — one idiom, one meaning:
        "this is the console's active instruction" */
main:has(> .jny),
main:has(> .msn-brief) { position: relative; }
main:has(> .jny)::before,
main:has(> .msn-brief)::before,
.lrn-onb::before {
  content: "";
  position: absolute; inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(circle at 18% 22%, rgba(255, 255, 255, 0.20) 1px, transparent 1.7px),
    radial-gradient(circle at 76% 58%, rgba(255, 255, 255, 0.14) 1px, transparent 1.5px),
    radial-gradient(circle at 42% 86%, rgba(201, 178, 255, 0.18) 1px, transparent 1.7px);
  background-size: 240px 240px, 300px 300px, 360px 360px;
  animation: lrn-sky-drift 120s linear infinite;
}
.lrn-onb { position: relative; }

/* Law 3 accents */
.jny { --glow-accent: var(--eng-purple); }
.msn-brief { --glow-accent: var(--eng-purple); }
.lrn-msgs { --glow-accent: var(--eng-purple); }

/* Measured values wear the instrument face. */
.jny-count,
.msn-timing,
.lf-meta__chip,
.lrn-msg__meta,
.lrn-cefr b { font-family: var(--font-mono); }

/* The journey's next-order card: same brackets, same sentence as .lrn-today. */
.jny-next { position: relative; --glow-accent: var(--eng-green); }
.jny-next::before,
.jny-next::after {
  content: "";
  position: absolute;
  width: 18px; height: 18px;
  pointer-events: none;
  border: 1px solid color-mix(in srgb, var(--glow-accent) 45%, transparent);
}
.jny-next::before {
  top: -5px; left: -5px;
  border-right: none; border-bottom: none;
  border-top-left-radius: 6px;
}
.jny-next::after {
  bottom: -5px; right: -5px;
  border-left: none; border-top: none;
  border-bottom-right-radius: 6px;
}

/* ── AUTH + COMMERCE (Batch 5). The eyebrows above already carry these
      pages; what is left is the instrument face on the money numbers —
      a PRICE is the most consequential measured value in the product. */
.prc-price { font-family: var(--font-mono); font-weight: 700; letter-spacing: -0.01em; }
.sub-head__price { font-family: var(--font-mono); font-weight: 700; }
.sub-step { font-family: var(--font-mono); font-weight: 700; }
/* the old eyebrow line is now a wrapper for the shared pill */
.sub-head__eyebrow { margin-bottom: 10px; }

/* ══ GIFT — แผนที่เวลา 12 Tenses (secret thank-you page, /gift/12-tenses) ══
   Flight Deck dark surface; every color token-derived. The timeline SVGs
   (.gtl-*) carry the pedagogy: dot=point event, span=continuous, hollow
   ref=the event a perfect tense positions against. Reveal styles arm ONLY
   under .gft-js (added by gift-tenses.js when motion is allowed) — no JS or
   reduced motion means nothing is ever hidden. */
.gft {
  position: relative;
  width: 100%;
  max-width: 1120px;
  margin: 0 auto;
  padding: 48px 20px 72px;
}
.gft::before {
  content: '';
  position: absolute; inset: 0;
  pointer-events: none;
  background:
    radial-gradient(560px 360px at 8% 4%, color-mix(in srgb, var(--eng-purple) 16%, transparent), transparent 70%),
    radial-gradient(520px 340px at 94% 30%, color-mix(in srgb, var(--eng-green) 7%, transparent), transparent 72%);
}
.gft > * { position: relative; }
/* the 133 INK-TRAP fix, structural: the token sheet paints bare <p> dark;
   on this dark surface every unclassed paragraph inherits the body's white
   instead. :where() keeps specificity at (0,0,1) so every classed color in
   this block still wins. */
:where(.gft) p { color: inherit; }

/* hero */
.gft__hero { text-align: center; padding: 24px 0 40px; }
.gft__eyebrow {
  display: inline-flex; align-items: center; gap: 8px;
  margin: 0 0 18px;
  padding: 6px 14px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: var(--radius-pill);
  font-size: 0.8rem; letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.82);
}
.gft__title {
  margin: 0 0 16px;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2.1rem, 6vw, 3.4rem);
  line-height: 1.15;
}
.gft__sub {
  max-width: 34em;
  margin: 0 auto 16px;
  font-size: 1.02rem; line-height: 1.75;
  color: rgba(255, 255, 255, 0.85);
}
.gft__secret {
  display: inline-block;
  margin: 0;
  padding: 6px 14px;
  border: 1px solid var(--eng-gold-edge);
  border-radius: var(--radius-pill);
  font-size: 0.8rem;
  color: var(--eng-gold);
}

/* legend */
.gft__legend {
  max-width: 560px;
  margin: 0 auto 48px;
  padding: 18px 20px 16px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius-xl, 16px);
  background: rgba(255, 255, 255, 0.04);
}
.gft__legend-head {
  margin: 0 0 10px;
  font-family: var(--font-mono);
  font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: rgba(255, 255, 255, 0.6);
}
.gft__axis {
  display: flex; justify-content: space-between;
  margin-top: 4px;
  font-size: 0.78rem; color: rgba(255, 255, 255, 0.65);
}
.gft__axis-now { color: var(--eng-green); font-weight: 700; }
.gft__legend-list {
  margin: 12px 0 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 6px;
  font-size: 0.85rem; color: rgba(255, 255, 255, 0.8);
}
.gft__legend-list li { display: flex; align-items: center; gap: 8px; }
.gft__key { flex: none; display: inline-block; }
.gft__key--dot { width: 10px; height: 10px; border-radius: 50%; background: var(--eng-green); }
.gft__key--span { width: 22px; height: 8px; border-radius: 4px; background: color-mix(in srgb, var(--eng-green) 70%, transparent); }
.gft__key--ref { width: 10px; height: 10px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.7); }

/* timeline svg */
.gtl { display: block; width: 100%; height: 44px; }
.gtl-zone { fill: rgba(255, 255, 255, 0.07); }
.gtl-now { stroke: var(--eng-green); stroke-width: 2.5; stroke-linecap: round; animation: gftNowBreathe 3.4s ease-in-out infinite; }
.gtl-dot { fill: var(--eng-green); }
.gtl-span { fill: color-mix(in srgb, var(--eng-green) 68%, transparent); }
.gtl-tip { fill: var(--eng-green); }
.gtl-arrow { stroke: rgba(255, 255, 255, 0.45); stroke-width: 2; stroke-dasharray: 4 4; }
.gtl-ref { fill: none; stroke: rgba(255, 255, 255, 0.7); stroke-width: 2; }
@keyframes gftNowBreathe {
  0%, 100% { opacity: 0.65; }
  50% { opacity: 1; }
}

/* bands */
.gft__band { margin-bottom: 48px; }
.gft__band-head { margin-bottom: 18px; }
.gft__band-eyebrow {
  margin: 0 0 6px;
  font-family: var(--font-mono);
  font-size: 0.72rem; letter-spacing: 0.18em;
  color: var(--eng-green);
}
.gft__band-title {
  margin: 0 0 6px;
  font-family: var(--font-display); font-weight: 700;
  font-size: 1.5rem;
}
.gft__band-intro { margin: 0; font-size: 0.95rem; color: rgba(255, 255, 255, 0.72); }
.gft__grid { display: grid; grid-template-columns: 1fr; gap: 14px; }
@media (min-width: 720px) { .gft__grid { grid-template-columns: 1fr 1fr; } }

/* tense card */
.gft__card {
  padding: 16px 18px 18px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius-xl, 16px);
  background: rgba(255, 255, 255, 0.04);
  transition: border-color var(--dur-base) var(--ease-out);
}
.gft__card:hover { border-color: color-mix(in srgb, var(--eng-green) 55%, transparent); }
.gft__card .gtl { margin-bottom: 10px; }
.gft__card-name {
  margin: 0;
  font-family: var(--font-display); font-weight: 700;
  font-size: 1.08rem;
}
.gft__card-handle { margin: 2px 0 10px; font-size: 0.9rem; font-weight: 700; color: var(--eng-green); }
.gft__formula {
  margin: 0 0 10px;
  padding: 6px 10px;
  border-radius: var(--radius-lg, 12px);
  background: rgba(255, 255, 255, 0.06);
  font-family: var(--font-mono);
  font-size: 0.82rem;
  color: rgba(255, 255, 255, 0.92);
}
/* explicit ink: the token sheet paints bare <p> dark (the 133 INK TRAP) —
   every gft paragraph must carry its own color on this dark surface */
.gft__ex-en { margin: 0 0 2px; font-size: 0.98rem; line-height: 1.6; color: rgba(255, 255, 255, 0.94); }
.gft__ex-th { margin: 0 0 10px; font-size: 0.88rem; color: rgba(255, 255, 255, 0.72); }
.gft__use { margin: 0; font-size: 0.88rem; line-height: 1.65; color: rgba(255, 255, 255, 0.85); }
.gft__use-label {
  display: inline-block;
  margin-right: 6px;
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--eng-green) 16%, transparent);
  font-size: 0.75rem; font-weight: 700;
  color: var(--eng-green);
}
.gft__trap { margin: 8px 0 0; font-size: 0.85rem; line-height: 1.6; color: var(--eng-gold); }

/* aspect hooks (the mental model) */
.gft__hooks {
  max-width: 640px;
  margin: 0 auto 48px;
  padding: 22px 22px 20px;
  border: 1.5px solid color-mix(in srgb, var(--eng-green) 40%, transparent);
  border-radius: var(--radius-xl, 16px);
  background: color-mix(in srgb, var(--eng-green) 6%, transparent);
}
.gft__hooks-title {
  margin: 0 0 14px;
  font-family: var(--font-display); font-weight: 700;
  font-size: 1.3rem; text-align: center;
}
.gft__hooks-list { margin: 0 0 14px; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 10px; }
.gft__hook { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 12px; }
.gft__hook-name {
  flex: none;
  min-width: 9.5em;
  font-family: var(--font-mono);
  font-size: 0.8rem; font-weight: 700;
  color: var(--eng-green);
}
.gft__hook-th { font-size: 0.95rem; color: rgba(255, 255, 255, 0.9); }
.gft__punchline {
  margin: 0;
  padding-top: 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  font-size: 0.95rem; line-height: 1.7;
  color: rgba(255, 255, 255, 0.88);
}

/* closing CTA */
.gft__cta { text-align: center; padding: 8px 0 0; }
.gft__cta-head { margin: 0 0 6px; font-family: var(--font-display); font-weight: 700; font-size: 1.25rem; }
.gft__cta-sub { margin: 0 0 18px; font-size: 0.95rem; color: rgba(255, 255, 255, 0.75); }
.gft__cta-btn { display: inline-block; }

/* read-next shelf — the exit becomes an entrance (round-2: finish reading →
   the free article library, not the close button) */
.gft__more { max-width: 760px; margin: 48px auto 0; text-align: center; }
.gft__more-title { margin: 0 0 6px; font-family: var(--font-display); font-weight: 700; font-size: 1.3rem; }
.gft__more-sub { margin: 0 0 18px; font-size: 0.92rem; color: rgba(255, 255, 255, 0.72); }
.gft__more-grid { display: grid; grid-template-columns: 1fr; gap: 12px; text-align: left; }
@media (min-width: 720px) { .gft__more-grid { grid-template-columns: repeat(3, 1fr); } }
.gft__more-card {
  display: flex; flex-direction: column; gap: 8px;
  padding: 14px 16px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius-xl, 16px);
  background: rgba(255, 255, 255, 0.04);
  text-decoration: none;
  transition: border-color var(--dur-base) var(--ease-out);
}
.gft__more-card:hover { border-color: color-mix(in srgb, var(--eng-green) 55%, transparent); }
.gft__more-name { font-family: var(--font-display); font-weight: 700; font-size: 0.98rem; color: var(--eng-white); }
.gft__more-excerpt {
  font-size: 0.85rem; line-height: 1.6; color: rgba(255, 255, 255, 0.72);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.gft__more-go { margin-top: auto; font-size: 0.85rem; font-weight: 700; color: var(--eng-green); }
.gft__more-all {
  display: inline-block; margin-top: 18px;
  font-size: 0.92rem; font-weight: 700;
  color: var(--eng-green); text-decoration: underline; text-underline-offset: 3px;
}

/* page entrance — the other half of the gift card's fade-out (the unwrap
   lands somewhere that fades IN); one-shot, ends at rest */
.gft { animation: gftPageIn 0.5s var(--ease-out); }
@keyframes gftPageIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

/* scroll reveal — armed only by gift-tenses.js (.gft-js) */
.gft-js [data-gft-reveal] {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.45s var(--ease-out), transform 0.45s var(--ease-out);
}
.gft-js [data-gft-reveal].is-revealed { opacity: 1; transform: none; }

/* ── FLIGHT DECK reduced-motion tail (Law 2). Every animation THIS block's
      section introduced, forced to its arrived frame. All of them loop
      around their rest state, so animation:none IS the end frame — the
      eyebrow dot rests LIT (its natural opacity is 1; only eng-blip dips it).
      ⚠️ This block stays at the very BOTTOM of the file: these selectors tie
      on specificity with the rules they silence, and source order decides. */
@media (prefers-reduced-motion: reduce) {
  .lrn-home::before,
  body.eng-bg-space .eng-main--read::before,
  body.eng-bg-space .eng-main--app::before,
  .lrn-arena::before,
  main:has(> .jny)::before,
  main:has(> .msn-brief)::before,
  .lrn-onb::before,
  .lrn-progress--arena .eng-progress__fill,
  .plc-pass__fill,
  .plc-pass__leg::after,
  .lrn-eyebrow::before,
  .lrn-today,
  .lrn-today__eyebrow::before,
  .lrn-fleet__body,
  .lrn-lane.is-started .lrn-lane__craft::before,
  .lrn-streak--today .lrn-streak__flame svg,
  .lrn-streak--alive .lrn-streak__flame svg,
  .lrn-streak--at_risk .lrn-streak__flame svg { animation: none; }
  /* flagship flow (lf-*) — relocated here from mid-file when this tail became
     the file's single trailing block; same rules, stronger position */
  .lf-enter, .lf-enter-back, .lf-predict__reveal { animation: none; }
  .lf-dot { transition: none; }
  .lf-progress__bar, .lf-choice__btn, .lf-iv__chip { transition: none; }
  /* pre-flight checklist: the next-step breathe dies; a static hue border
     keeps "this one is next" legible without motion */
  .lf-chk__step.is-next:not(.is-checked) .lf-chk__node {
    animation: none;
    border-color: color-mix(in srgb, var(--skill-hue, var(--eng-purple)) 70%, transparent);
  }
  .lf-iv__zone.is-glow { animation: none; box-shadow: 0 0 0 1.5px var(--eng-green); }
  .lf-tm__tile { transition: none; }
  /* FD drag affordance: nudge + ghost die entirely; the armed ring stays as
     a STATIC green border so "where do I drop this" survives without motion */
  .lf-iv--dnd:not(.is-touched):not(.is-complete) .lf-iv__pool .lf-iv__chip:first-child,
  .lf-tm:not(.is-touched):not(.is-complete) .lf-iv__pool .lf-iv__chip:first-child,
  .lf-iv--dnd:not(.is-touched):not(.is-complete) .lf-iv__pool::after,
  .lf-iv.has-armed .lf-iv__zone,
  .lf-tm.has-armed .lf-tm__tile:not(.is-done) { animation: none; }
  .lf-iv--dnd:not(.is-touched):not(.is-complete) .lf-iv__pool::after { content: none; }
  .lf-iv.has-armed .lf-iv__zone,
  .lf-tm.has-armed .lf-tm__tile:not(.is-done) { border-color: var(--eng-green); }
  /* UAT round 2 FD polish: corner brackets appear instantly; the GO-light
     pulse dies but the static green glow stays (state, not motion) */
  .lf-enter::before, .lf-enter::after,
  .lf-enter-back::before, .lf-enter-back::after { animation: none; }
  .lf-nav .eng-btn--primary:not(:disabled) {
    animation: none;
    box-shadow: 0 0 14px color-mix(in srgb, var(--eng-green) 32%, transparent);
  }
  /* boarding pass (dashboard): route leg + shimmer die; static positions */
  .lrn-pass__leg::after, .lrn-pass__craft, .lrn-pass::after { animation: none; }
  /* article dark pass (UAT 3.1): gate brackets stop breathing but stay at full
     strength; the hero drift dies to a static frame */
  .art-gate-card::before, .art-gate-card::after { animation: none; opacity: 1; }
  .art-answer__hero img { animation: none; }
  /* discovery grid (UAT 3.1c): the card lift dies; hover speaks through the
     border colour alone */
  html .ghub-card { transition: none; }
  html .ghub-card:hover, html .ghub-card:focus-visible { transform: none; }

  /* placement Flight Deck (UAT free@): waypoint pulse + card/leg-start
     slide-ins die; the current waypoint keeps a STATIC hue glow so "you are
     here" survives without motion */
  html .plc-wp.is-now {
    animation: none;
    box-shadow: 0 0 12px color-mix(in srgb, var(--wp-hue, var(--eng-purple)) 45%, transparent);
  }
  html .plc-card, html .plc-legstart { animation: none; }
  /* spaceport report (UAT free@ round 3): the pad craft stops hovering and
     rests at its arrived frame; the pad ring glow is already static state */
  html .plc-pad__craft { animation: none; }
  /* gift 12-tenses: the NOW line rests lit (breathe dips FROM 1); reveal
     never arms under reduced motion (gift-tenses.js checks the media query
     before adding .gft-js) — the belt below covers a stale class anyway */
  html .gtl-now { animation: none; opacity: 1; }
  html .gft-js [data-gft-reveal] { transition: none; opacity: 1; transform: none; }
  html .gft__card { transition: none; }
  /* round 2: page-entrance fade + read-next hover arrive at rest */
  html .gft { animation: none; }
  html .gft__more-card { transition: none; }
  /* ask-AI chip (round 7.1): the nudge breathe dies; the purple border +
     tint stay — the affordance survives as static state */
  html .lrn-askai__btn { animation: none; }

}
