/* ==========================================================================
   LearnTree stylesheet
   ==========================================================================
   Organised top to bottom as: reset, primitives, shell, then components in
   the order a learner meets them. Every value comes from tokens.css.
   ========================================================================== */

/* --- Reset ------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  min-height: 100vh;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;

  /* The signature texture: engineering graph paper, at the very edge of
     perception. Two repeating gradients form a 24px grid whose lines are
     barely a shade off the background. You do not consciously see it; you
     notice that the page feels like a surface rather than a void. */
  background-image:
    repeating-linear-gradient(
      to right,
      color-mix(in srgb, var(--rule) 34%, transparent) 0 1px,
      transparent 1px 24px
    ),
    repeating-linear-gradient(
      to bottom,
      color-mix(in srgb, var(--rule) 34%, transparent) 0 1px,
      transparent 1px 24px
    );
}

img, svg { display: block; max-width: 100%; }
button, input, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; padding: 0; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--accent-2); }

::selection { background: var(--accent-wash); color: var(--ink); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- Typography primitives ---------------------------------------------- */

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.005em;
}

h1 { font-size: var(--fs-2xl); }
h2 { font-size: var(--fs-lg); }
h3 { font-size: var(--fs-base); }

/* Every number in the interface is monospaced and tabular, so digits line up
   in columns and a changing value does not make its neighbours jump. This is
   the single cheapest thing that makes data look professionally handled. */
.num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

/* Eyebrow: a small uppercase Latin label paired with a Thai one. The pairing
   is the house device — Thai carries the meaning, English carries the
   structure. */
.eyebrow {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  margin-bottom: var(--s-3);
}
.eyebrow b {
  font-family: var(--font-display);
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--ink);
}
.eyebrow span {
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--faint);
}

.lede { font-size: var(--fs-md); color: var(--ink-2); max-width: var(--measure); }
.small { font-size: var(--fs-sm); color: var(--muted); }
.tiny  { font-size: var(--fs-xs);  color: var(--muted); }

/* --- Shell -------------------------------------------------------------- */

.shell {
  background: var(--shell);
  color: var(--shell-ink);
  border-bottom: 1px solid color-mix(in srgb, var(--shell-3) 70%, transparent);
}

.shell-inner {
  max-width: 84rem;
  margin: 0 auto;
  padding: var(--s-3) var(--s-5);
  display: flex;
  align-items: center;
  gap: var(--s-5);
}

.brand {
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  font-family: var(--font-display);
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--shell-ink);
  text-decoration: none;
  letter-spacing: 0.01em;
}
.brand:hover { color: var(--shell-ink); }
.brand em {
  font-family: var(--font-body);
  font-style: normal;
  font-size: var(--fs-xs);
  font-weight: 400;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--shell-muted);
}

.shell-nav { display: flex; gap: var(--s-1); margin-left: auto; }
.shell-nav a {
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  color: var(--shell-muted);
  text-decoration: none;
  transition: background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.shell-nav a:hover { background: var(--shell-2); color: var(--shell-ink); }
.shell-nav a[aria-current="page"] { background: var(--shell-3); color: var(--shell-ink); }

/* --- Demo banner --------------------------------------------------------
   Present on every page, deliberately impossible to miss and impossible to
   dismiss. While the seeded content is unreviewed, saying so everywhere is
   the only honest option — a trust product that overstates its own status on
   day one has already failed. */

.demo-banner {
  background: var(--accent-wash);
  border-bottom: 1px solid var(--accent-edge);
  color: var(--accent-2);
  font-size: var(--fs-sm);
  text-align: center;
  padding: var(--s-2) var(--s-4);
}

/* --- Layout ------------------------------------------------------------- */

.page {
  max-width: 84rem;
  margin: 0 auto;
  padding: var(--s-7) var(--s-5) var(--s-9);
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--rail);
  gap: var(--s-7);
  align-items: start;
}
.page--single { grid-template-columns: minmax(0, 1fr); }

@media (max-width: 60rem) {
  .page { grid-template-columns: minmax(0, 1fr); }
}

/* The spine: a hairline running the length of the content with a tick at each
   section. Borrowed from a technical drawing's dimension line. It is what
   makes a long page feel measured rather than merely long, and it is the one
   ornament in the system that earns its place by aiding navigation. */
.spine {
  position: relative;
  padding-left: var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-7);
}
.spine::before {
  content: "";
  position: absolute;
  left: 0;
  top: var(--s-2);
  bottom: var(--s-2);
  width: 1px;
  background: linear-gradient(
    to bottom,
    transparent,
    var(--rule-strong) 4%,
    var(--rule-strong) 96%,
    transparent
  );
}

.spine > section { position: relative; }
.spine > section::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--s-6));
  top: 0.65em;
  width: var(--s-3);
  height: 1px;
  background: var(--rule-strong);
}
/* The current section's tick is a filled square in the accent — the "you are
   here" marker, matching the map's current-node treatment. */
.spine > section.is-current::before {
  left: calc(-1 * var(--s-6) - 2px);
  top: calc(0.65em - 2px);
  width: 5px;
  height: 5px;
  background: var(--accent);
  border-radius: var(--r-sm);
}

@media (max-width: 40rem) {
  .spine { padding-left: 0; }
  .spine::before, .spine > section::before { display: none; }
}

/* --- Cards -------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: var(--s-5);
}
.card--flush { padding: 0; overflow: hidden; }
.card--well  { background: var(--surface-2); border-color: transparent; }

/* --- Trust badges --------------------------------------------------------
   The most important component in the application. Three tiers, three
   distinct visual treatments, readable at a glance and without colour alone:
   verified is solid sage, community is outlined, AI-draft is dashed. The
   dashed border survives greyscale printing and colour-blindness, which a
   colour difference would not. */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-2);
  border-radius: var(--r-md);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1.3;
  white-space: nowrap;
}
.badge svg { flex: none; }
.badge small { display: block; font-weight: 400; opacity: 0.85; }

.badge--verified {
  background: var(--sage-wash);
  color: var(--sage-2);
  border: 1px solid var(--sage-edge);
}
.badge--community {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--rule-strong);
}
.badge--draft {
  background: var(--accent-wash);
  color: var(--accent-2);
  border: 1px dashed var(--accent-edge);
}

/* --- Chips and buttons --------------------------------------------------- */

.chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }

.chip {
  padding: var(--s-1) var(--s-3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-pill);
  background: var(--surface);
  font-size: var(--fs-sm);
  color: var(--ink-2);
  transition: border-color var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.chip:hover { border-color: var(--accent-edge); }
.chip[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.chip--static { cursor: default; background: var(--surface-2); border-color: transparent; }
.chip--static:hover { border-color: transparent; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-5);
  border-radius: var(--r-md);
  font-size: var(--fs-base);
  font-weight: 600;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.btn:active { transform: translateY(1px); }

.btn--primary { background: var(--accent); color: #fff; }
.btn--primary:hover { background: var(--accent-2); }

.btn--ghost {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--rule-strong);
}
.btn--ghost:hover { border-color: var(--ink-2); }

.btn--small { padding: var(--s-2) var(--s-3); font-size: var(--fs-sm); }

/* --- Node header --------------------------------------------------------- */

.crumb {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-bottom: var(--s-4);
}
.crumb a { color: var(--muted); text-decoration: none; }
.crumb a:hover { color: var(--accent); }
.crumb svg { color: var(--faint); }

.node-title { margin-bottom: var(--s-2); }
.node-title-en {
  font-family: var(--font-body);
  font-size: var(--fs-base);
  font-weight: 400;
  color: var(--muted);
  margin-bottom: var(--s-4);
}

/* --- Trail: the prerequisite path ---------------------------------------- */

.trail {
  display: flex;
  align-items: stretch;
  gap: 0;
  overflow-x: auto;
  padding-bottom: var(--s-2);
}
.trail-step {
  flex: 1 1 0;
  min-width: 8rem;
  padding: var(--s-3) var(--s-3) var(--s-3) 0;
  border-top: 2px solid var(--rule-strong);
  position: relative;
  text-decoration: none;
  display: block;
}
.trail-step::before {
  content: "";
  position: absolute;
  top: -5px;
  left: 0;
  width: 8px;
  height: 8px;
  border-radius: var(--r-sm);
  background: var(--paper);
  border: 2px solid var(--rule-strong);
}
.trail-step.is-done { border-top-color: var(--sage); }
.trail-step.is-done::before { background: var(--sage); border-color: var(--sage); }
.trail-step.is-current { border-top-color: var(--accent); }
.trail-step.is-current::before {
  background: var(--accent);
  border-color: var(--accent);
  width: 10px; height: 10px; top: -6px;
}
.trail-step b {
  display: block;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink);
  line-height: var(--lh-snug);
}
.trail-step.is-current b { color: var(--accent-2); }
.trail-step span { font-size: var(--fs-xs); color: var(--muted); }

/* --- Method card ---------------------------------------------------------
   The centrepiece. Three bands: formula, controls, results. The formula band
   is inverted so the eye lands there first — it is the thing being taught. */

.method { border-color: var(--accent-edge); }

.method-formula {
  background: var(--shell);
  color: var(--shell-ink);
  padding: var(--s-6) var(--s-5);
  text-align: center;
}
.method-formula .f {
  font-family: var(--font-mono);
  font-size: var(--fs-xl);
  letter-spacing: 0.01em;
}
.method-formula .ref {
  margin-top: var(--s-3);
  font-size: var(--fs-xs);
  color: var(--shell-muted);
  letter-spacing: 0.02em;
}

.method-controls {
  padding: var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
}

.field { display: flex; flex-direction: column; gap: var(--s-2); }
.field-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-4);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
}
.field-label .val { color: var(--ink); }

input[type="range"] {
  width: 100%;
  accent-color: var(--accent);
  cursor: pointer;
}
.field-scale {
  display: flex;
  justify-content: space-between;
  font-size: var(--fs-xs);
  color: var(--faint);
}

.method-results {
  background: var(--surface-2);
  border-top: 1px solid var(--rule);
  padding: var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

.outputs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--s-4);
}
.output-label { font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }
.output-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-lg);
  line-height: 1.2;
  color: var(--ink);
}
.output--emphasis .output-value { font-size: var(--fs-2xl); color: var(--accent); }
.output-unit { font-size: var(--fs-xs); color: var(--muted); }

/* The claim that the whole product rests on, given its own treatment. */
.computed-note {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  background: var(--surface);
  border-left: 2px solid var(--sage);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  padding: var(--s-3) var(--s-4);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
}
.computed-note svg { flex: none; margin-top: 2px; color: var(--sage); }

/* Working: the steps a lecturer would write on a board. */
.steps { display: flex; flex-direction: column; gap: var(--s-3); }
.step {
  display: grid;
  grid-template-columns: 1.6rem 1fr;
  gap: var(--s-3);
  align-items: start;
}
.step-n {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--faint);
  padding-top: 0.35em;
}
.step-label { font-size: var(--fs-sm); color: var(--muted); }
.step-math {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm);
  color: var(--ink-2);
  overflow-x: auto;
  padding: var(--s-2) 0;
}
.step-result { color: var(--accent); font-weight: 500; }

/* --- Source list ---------------------------------------------------------- */

.sources { display: flex; flex-direction: column; }
.source {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--s-4);
  padding: var(--s-4) 0;
  border-bottom: 1px solid var(--rule);
}
.source:first-child { border-top: 1px solid var(--rule); }
.source-title { font-weight: 600; font-size: var(--fs-base); line-height: var(--lh-snug); }
.source-note { font-size: var(--fs-sm); color: var(--muted); margin-top: var(--s-1); }

/* --- Practice ------------------------------------------------------------- */

.practice-q {
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  margin-bottom: var(--s-2);
}
.practice-en { font-size: var(--fs-sm); color: var(--muted); margin-bottom: var(--s-4); }

.hint {
  border-left: 2px solid var(--accent-edge);
  background: var(--accent-wash);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  padding: var(--s-3) var(--s-4);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  margin-top: var(--s-2);
  animation: reveal var(--dur-slow) var(--ease);
}
.hint b { color: var(--accent-2); }

@keyframes reveal {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

.solution { margin-top: var(--s-4); animation: reveal var(--dur-slow) var(--ease); }

/* --- Tangents -------------------------------------------------------------
   The invitation to wander. Styled as an offer rather than an obligation:
   no border by default, a warm tint on hover, and the accent reserved for the
   little branch mark. It should read as a door standing open, not as another
   item on a list of things you have not done yet. */

.tangent {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--s-3);
  align-items: start;
  padding: var(--s-3) var(--s-4);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  background: var(--surface-2);
  text-decoration: none;
  color: inherit;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.tangent:hover {
  background: var(--accent-wash);
  border-color: var(--accent-edge);
  color: inherit;
  transform: translateX(2px);
}
.tangent-mark { color: var(--accent); padding-top: 2px; }
.tangent-title { font-weight: 600; font-size: var(--fs-base); line-height: var(--lh-snug); }
.tangent-why {
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: var(--lh-snug);
  margin-top: var(--s-1);
  max-width: 52ch;
}
.tangent-time { white-space: nowrap; padding-top: 3px; }

/* Detour edges on the map: dashed, recessive, clearly not the main path. */
.map-edge--related {
  stroke: var(--accent-edge);
  stroke-width: 1.2;
  stroke-dasharray: 4 4;
  fill: none;
}

/* --- Lesson player ---------------------------------------------------------
   The stage is a wide dark surface with the slide on the left and the
   character on the right. Dark because a lesson is something you watch, and a
   watching surface should recede so the content comes forward. */

.proto-banner {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  background: var(--accent-wash);
  border: 1px dashed var(--accent-edge);
  border-radius: var(--r-md);
  padding: var(--s-3) var(--s-4);
  color: var(--accent-2);
  line-height: var(--lh-snug);
}
.proto-banner svg { flex: none; margin-top: 2px; }

.stage {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--s-5);
  align-items: end;
  background: var(--shell);
  border-radius: var(--r-lg);
  padding: var(--s-6);
  min-height: 26rem;
  overflow: hidden;
  position: relative;
}
@media (max-width: 52rem) {
  .stage { grid-template-columns: 1fr; align-items: start; }
}

.stage-slide { align-self: center; color: var(--shell-ink); }
.stage-slide.is-entering { animation: slide-in var(--dur-slow) var(--ease); }

@keyframes slide-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

.slide { max-width: 34rem; }
/* No `text-transform: uppercase` here, unlike .eyebrow elsewhere. Slide
   headings carry technical symbols, and uppercasing turned "Kc" into "KC" —
   a different quantity. Thai has no case, so the transform only ever affected
   the embedded Latin, which is exactly the part that must not change. */
.slide-eyebrow {
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-eyebrow);
  color: var(--accent);
  margin-bottom: var(--s-3);
}
.slide-body {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  line-height: var(--lh-tight);
  color: var(--shell-ink);
}
.slide--title h2 { font-size: var(--fs-2xl); color: var(--shell-ink); }
.slide--title p, .slide--closing p {
  color: var(--shell-muted);
  margin-top: var(--s-3);
  font-size: var(--fs-md);
}
.slide--closing h2 { font-size: var(--fs-xl); color: var(--shell-ink); }

.slide-formula {
  font-family: var(--font-mono);
  font-size: var(--fs-2xl);
  color: var(--shell-ink);
  letter-spacing: 0.02em;
}
.slide-caption {
  margin-top: var(--s-3);
  font-size: var(--fs-xs);
  color: var(--shell-muted);
}

.slide-calc { display: flex; flex-direction: column; gap: var(--s-3); }
.slide-step-label { font-size: var(--fs-xs); color: var(--shell-muted); }
.slide-step-math {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-base);
  color: var(--shell-ink);
}
.slide-step-math b { color: var(--accent); }

/* The character. Idle sway runs always; a faster, subtler bob is layered on
   while speaking so the figure reads as animated rather than pasted on. */
.stage-avatar { display: flex; flex-direction: column; align-items: center; gap: var(--s-2); }
.avatar { animation: sway 5.5s ease-in-out infinite; transform-origin: 50% 100%; }
.stage.is-speaking .avatar { animation: sway 5.5s ease-in-out infinite, bob 1.6s ease-in-out infinite; }

/* ---------------------------------------------------------------------------
   Avatar expressions
   ---------------------------------------------------------------------------
   The whole emotional range of the character lives in this block, as custom
   properties set per expression. JavaScript writes one attribute —
   `data-expression` — and everything below follows.

   Why CSS rather than JavaScript: transitions come free, so a face *changes*
   rather than snapping; a ninth expression is a block here, not a code change;
   and the animation keeps running if a script fails.

   `--lid` is the eyelid position, and blinking is a separate class rather than
   an inline style, so a blink returns to whatever the current expression's
   eyelid was instead of snapping the eyes fully open. That bug is invisible
   until an expression uses half-lidded eyes, which several do.

   The sway/bob keyframes above own `.avatar`'s transform, so the head tilt is
   applied to `#av-figure` inside it. Two animations on one element fight; two
   elements do not.
   --------------------------------------------------------------------------- */

.avatar {
  /* Defaults, so a character with no expression set still looks alive. */
  --lid: 0;
  --brow-l-y: 0px;  --brow-r-y: 0px;
  --brow-l-rot: 0deg; --brow-r-rot: 0deg;
  --blush: .25;
  --sweat: 0;
  --arc: 0;
  --iris-x: 0px; --iris-y: 0px;
  --tilt: 0deg;
  --nod: 0px;
}

.avatar #av-head {
  transform: rotate(var(--tilt)) translateY(var(--nod));
  transform-box: fill-box;
  transform-origin: 50% 100%;
  transition: transform 420ms cubic-bezier(.22, .61, .36, 1);
}

.avatar #av-brow-l,
.avatar #av-brow-r,
.avatar #av-iris-l,
.avatar #av-iris-r,
.avatar #av-arc-l,
.avatar #av-arc-r,
.avatar #av-blush,
.avatar #av-sweat {
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 260ms ease, opacity 260ms ease;
}

.avatar #av-brow-l { transform: translateY(var(--brow-l-y)) rotate(var(--brow-l-rot)); }
.avatar #av-brow-r { transform: translateY(var(--brow-r-y)) rotate(var(--brow-r-rot)); }

.avatar #av-iris-l,
.avatar #av-iris-r {
  transform: translate(var(--iris-x), var(--iris-y));
  opacity: calc(1 - var(--arc));
}

.avatar #av-arc-l, .avatar #av-arc-r { opacity: var(--arc); }
.avatar #av-blush { opacity: var(--blush); }
.avatar #av-sweat { opacity: var(--sweat); }

/* Eyelids. scaleY(0) is open, scaleY(1) is shut. */
.avatar #av-eye-l,
.avatar #av-eye-r {
  transform-box: fill-box;
  transform-origin: 50% 0;
  transform: scaleY(var(--lid));
  transition: transform 90ms ease;
}
.avatar.is-blinking #av-eye-l,
.avatar.is-blinking #av-eye-r { transform: scaleY(1); }

/* The closed-eye lash curve. The lid alone gives a blank skin-coloured gap;
   the curve is what makes a blink read as an eye closing. */
.avatar #av-shut-l,
.avatar #av-shut-r { opacity: 0; transition: opacity 60ms ease; }
.avatar.is-blinking #av-shut-l,
.avatar.is-blinking #av-shut-r { opacity: 1; }

/* Resting mouths: exactly one is visible, chosen by mood. */
.avatar .m-rest { display: none; }
.avatar .m-flat { display: block; }

/* ---- the eight expressions ------------------------------------------------
   Brow rotation sign, since it is easy to get backwards: in SVG a positive
   angle turns clockwise. The LEFT brow's inner end is its right end, so
   raising it (worry, apology) is a NEGATIVE rotation; the right brow's inner
   end is its left end, so the same feeling is POSITIVE there. Furrowing —
   inner ends down, for concern — is the reverse of both.
   -------------------------------------------------------------------------- */

.avatar[data-expression="neutral"] {
  --lid: 0; --blush: .22;
}
.avatar[data-expression="neutral"] .m-flat { display: block; }

.avatar[data-expression="warm"] {
  --lid: .16; --blush: .5;
  --brow-l-y: -1px; --brow-r-y: -1px;
}
.avatar[data-expression="warm"] .m-flat { display: none; }
.avatar[data-expression="warm"] .m-smile { display: block; }

.avatar[data-expression="bright"] {
  --lid: 0; --blush: .8; --arc: 1; --tilt: -1.5deg;
  --brow-l-y: -4px; --brow-r-y: -4px;
}
.avatar[data-expression="bright"] .m-flat { display: none; }
.avatar[data-expression="bright"] .m-smile { display: block; }

.avatar[data-expression="curious"] {
  --lid: 0; --blush: .35; --tilt: 4deg; --iris-y: -1.5px;
  --brow-l-y: -5px; --brow-l-rot: -6deg;   /* one brow up: the whole signal */
  --brow-r-y: -1px;
}
.avatar[data-expression="curious"] .m-flat { display: none; }
.avatar[data-expression="curious"] .m-smile { display: block; }

.avatar[data-expression="thinking"] {
  --lid: .3; --blush: .3; --tilt: 2.5deg;
  --iris-x: -2px; --iris-y: -2.5px;        /* eyes up and away: recalling */
  --brow-l-y: 1px; --brow-r-y: 1px;
}

.avatar[data-expression="unsure"] {
  --lid: .22; --blush: .38; --sweat: 1; --tilt: -2.5deg;
  --brow-l-y: -2px; --brow-l-rot: -9deg;
  --brow-r-y: -2px; --brow-r-rot: 9deg;
}
.avatar[data-expression="unsure"] .m-flat { display: none; }
.avatar[data-expression="unsure"] .m-wavy { display: block; }

.avatar[data-expression="concerned"] {
  --lid: 0; --blush: .2;
  --brow-l-y: 2px; --brow-l-rot: 11deg;
  --brow-r-y: 2px; --brow-r-rot: -11deg;
}
.avatar[data-expression="concerned"] .m-flat { display: none; }
.avatar[data-expression="concerned"] .m-frown { display: block; }

.avatar[data-expression="sorry"] {
  --lid: .38; --blush: .45; --sweat: .85; --tilt: -3deg; --nod: 3px;
  --iris-y: 2px;                           /* eyes down */
  --brow-l-y: -3px; --brow-l-rot: -11deg;
  --brow-r-y: -3px; --brow-r-rot: 11deg;
}
.avatar[data-expression="sorry"] .m-flat { display: none; }
.avatar[data-expression="sorry"] .m-frown { display: block; }

/* Respect the system setting. The expression still changes — it is
   information, not decoration — it simply stops sliding. */
@media (prefers-reduced-motion: reduce) {
  .avatar,
  .stage.is-speaking .avatar { animation: none; }
  .avatar #av-figure,
  .avatar #av-brow-l, .avatar #av-brow-r,
  .avatar #av-iris-l, .avatar #av-iris-r,
  .avatar #av-blush, .avatar #av-sweat { transition: none; }
}

/* The character's name and role under the figure. The role line is the
   expectation-setting sentence — see docs/PERSONA.md — so it is not
   decoration and should not be trimmed away on small screens. */
.avatar-card { display: grid; gap: 2px; text-align: center; max-width: 17rem; }
.avatar-name { font-weight: 650; font-size: var(--fs-md); color: var(--ink); }
.avatar-role { font-size: var(--fs-xs, .78rem); color: var(--muted); line-height: 1.45; }


@keyframes sway {
  0%, 100% { transform: rotate(-1.1deg) translateY(0); }
  50%      { transform: rotate(1.1deg) translateY(-3px); }
}
@keyframes bob {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -3px; }
}

.stage-name { text-align: center; color: var(--shell-muted); line-height: 1.3; }
.stage-name b { display: block; color: var(--shell-ink); font-size: var(--fs-sm); }

/* Entry point to a lesson from a node page. */
.lesson-cta {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--s-4);
  align-items: center;
  padding: var(--s-4);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  text-decoration: none;
  color: inherit;
  transition: border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.lesson-cta:hover { border-color: var(--accent); color: inherit; transform: translateY(-1px); }
.lesson-cta--proto { border-style: dashed; border-color: var(--accent-edge); }
.lesson-cta-play {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
}
.lesson-cta-title { font-weight: 600; font-size: var(--fs-base); }

/* --- Transport ------------------------------------------------------------- */

.transport { display: flex; flex-direction: column; gap: var(--s-3); }
.transport-bar {
  height: 4px;
  background: var(--surface-2);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.transport-fill {
  height: 100%;
  width: 0;
  background: var(--accent);
  border-radius: var(--r-pill);
  transition: width 120ms linear;
}
.transport-row { display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap; }

.caption {
  background: var(--surface-2);
  border-left: 2px solid var(--rule-strong);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  padding: var(--s-3) var(--s-4);
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  color: var(--ink-2);
  min-height: 4.5rem;
}

/* --- Beat list -------------------------------------------------------------- */

.beats { display: flex; flex-direction: column; }
.beat {
  display: grid;
  grid-template-columns: 2.4rem 1fr auto;
  gap: var(--s-3);
  align-items: center;
  padding: var(--s-3) var(--s-2);
  border: none;
  border-bottom: 1px solid var(--rule);
  background: none;
  text-align: left;
  width: 100%;
  transition: background var(--dur-fast) var(--ease);
}
.beat:hover { background: var(--surface-2); }
.beat.is-current { background: var(--accent-wash); }
.beat.is-current .beat-text { font-weight: 600; color: var(--accent-2); }
.beat-n { font-size: var(--fs-xs); color: var(--faint); }
.beat-text { font-size: var(--fs-sm); }
.beat-cite { color: var(--sage); display: flex; align-items: center; gap: var(--s-1); }

/* --- Tutor rail ----------------------------------------------------------- */

.rail {
  position: sticky;
  top: var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}
@media (max-width: 60rem) { .rail { position: static; } }

.tutor-log {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  max-height: 26rem;
  overflow-y: auto;
  padding-right: var(--s-1);
}

.bubble { font-size: var(--fs-sm); line-height: var(--lh-snug); }
.bubble--me {
  align-self: flex-end;
  background: var(--surface-2);
  border-radius: var(--r-lg) var(--r-lg) var(--r-sm) var(--r-lg);
  padding: var(--s-3);
  max-width: 92%;
}
.bubble--tutor {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg) var(--r-lg) var(--r-lg) var(--r-sm);
  padding: var(--s-4);
}
.bubble--tutor p { margin-bottom: var(--s-3); }
.bubble--tutor p:last-child { margin-bottom: 0; }
.bubble--declined { border-color: var(--rule-strong); border-style: dashed; }

/* The citation marker: a superscript button that swaps the source panel. */
.cite {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.15em;
  height: 1.15em;
  padding: 0 0.28em;
  margin: 0 0.12em;
  vertical-align: super;
  border-radius: var(--r-sm);
  background: var(--sage-wash);
  border: 1px solid var(--sage-edge);
  color: var(--sage-2);
  font-family: var(--font-mono);
  font-size: 0.62em;
  font-weight: 600;
  transition: background var(--dur-fast) var(--ease);
}
.cite:hover, .cite[aria-pressed="true"] {
  background: var(--sage);
  border-color: var(--sage);
  color: #fff;
}

.mode-line {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding-top: var(--s-3);
  margin-top: var(--s-3);
  border-top: 1px solid var(--rule);
  font-size: var(--fs-xs);
  color: var(--muted);
}

.ask {
  display: flex;
  gap: var(--s-2);
  align-items: flex-end;
}
.ask textarea {
  flex: 1;
  resize: none;
  min-height: 2.75rem;
  max-height: 8rem;
  padding: var(--s-3);
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
}
.ask textarea:focus { border-color: var(--accent); outline: none; }

/* --- Source viewer -------------------------------------------------------- */

.scan {
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  overflow: hidden;
}
.scan img { width: 100%; height: auto; }
.scan-missing {
  padding: var(--s-6) var(--s-4);
  text-align: center;
  font-size: var(--fs-sm);
  color: var(--muted);
}
.scan-meta {
  border-top: 1px solid var(--rule);
  padding: var(--s-3);
  font-size: var(--fs-xs);
  color: var(--muted);
  background: var(--surface);
}
.scan-meta b { color: var(--ink); }

/* --- Knowledge map --------------------------------------------------------- */

.map-wrap {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: var(--s-4);
  overflow-x: auto;
}
.map-node rect {
  fill: var(--surface);
  stroke: var(--rule-strong);
  stroke-width: 1.5;
  transition: stroke var(--dur-fast) var(--ease);
}
.map-node:hover rect { stroke: var(--accent); }
.map-node text { fill: var(--ink); font-family: var(--font-body); }
.map-node .sub { fill: var(--muted); }
.map-node.is-visited rect { fill: var(--sage-wash); stroke: var(--sage-edge); }
.map-edge { stroke: var(--rule-strong); stroke-width: 1.5; fill: none; }

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  font-size: var(--fs-xs);
  color: var(--muted);
  margin-top: var(--s-3);
}
.legend i {
  display: inline-block;
  width: 10px; height: 10px;
  border-radius: var(--r-sm);
  margin-right: var(--s-1);
  vertical-align: -1px;
}

/* --- Data display (dashboard, review) --------------------------------------- */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--s-4);
}
.tile-label { font-size: var(--fs-xs); font-weight: 600; color: var(--muted); }
.tile-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-3xl);
  line-height: 1.1;
  margin: var(--s-1) 0;
}
.tile-note { font-size: var(--fs-xs); color: var(--muted); }

/* Horizontal bars. A single hue at five steps — sequential data, so one hue
   light to dark, never a rainbow. Bars are anchored to the baseline with only
   the data end rounded. */
.bar-row {
  display: grid;
  grid-template-columns: minmax(7rem, 12rem) 1fr auto;
  gap: var(--s-3);
  align-items: center;
  padding: var(--s-2) 0;
}
.bar-label { font-size: var(--fs-sm); }
.bar-track { background: var(--surface-2); border-radius: 0 var(--r-sm) var(--r-sm) 0; height: 1.25rem; }
.bar-fill { height: 100%; border-radius: 0 var(--r-sm) var(--r-sm) 0; background: var(--accent); }
.bar-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
  color: var(--muted);
  text-align: right;
  min-width: 5rem;
}
.bar-row.is-withheld .bar-label { color: var(--faint); }
.bar-row.is-withheld .bar-value { font-style: italic; }

.privacy-note {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  background: var(--sage-wash);
  border: 1px solid var(--sage-edge);
  border-radius: var(--r-md);
  padding: var(--s-3) var(--s-4);
  font-size: var(--fs-sm);
  color: var(--sage-2);
  line-height: var(--lh-snug);
}
.privacy-note svg { flex: none; margin-top: 2px; }

/* ---------------------------------------------------------------------------
   Video
   ---------------------------------------------------------------------------
   The facade is a button, not a div with a click handler: it is focusable,
   activates on Enter and Space, and announces itself to a screen reader
   without any ARIA. Free correctness from picking the right element.

   It is sized by aspect-ratio rather than the old padding-top trick, so the
   iframe that replaces it lands in exactly the same box and the page does not
   jump on click.
   --------------------------------------------------------------------------- */

.video {
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--surface);
}

.video-facade,
.video-frame {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
}

.video-facade {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  padding: var(--s-5);
  /* A quiet slate field rather than a black one: this is a placeholder the
     student is meant to read, not a video that has failed to load. */
  background: var(--shell);
  color: var(--shell-ink);
  text-align: center;
  cursor: pointer;
  transition: background 120ms ease;
}
.video-facade:hover { background: var(--shell-2); }
.video-facade:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

.video-play {
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.12);
  color: var(--shell-ink);
}

.video-facade-text { display: grid; gap: var(--s-1); }
.video-title { font-weight: 600; font-size: var(--fs-md); }
.video-hint { color: var(--shell-muted); }

.video-meta {
  padding: var(--s-4);
  border-top: 1px solid var(--rule);
}

/* A video that may not be embedded. Rendered as a link card so the difference
   from an embeddable one is obvious at a glance — that visible difference is
   the point of the whole freshness mechanism. */
.video--link {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4);
  background: var(--surface-2);
}

.video-reason {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2);
  margin-top: var(--s-3);
  color: var(--muted);
}
.video-reason svg { flex: none; margin-top: 0.1em; }

.review-item--video .scan img { width: 100%; display: block; }


/* --- Review queue ----------------------------------------------------------- */

.review-item {
  display: grid;
  grid-template-columns: 16rem 1fr;
  gap: var(--s-5);
  padding: var(--s-5) 0;
  border-bottom: 1px solid var(--rule);
}
@media (max-width: 55rem) { .review-item { grid-template-columns: 1fr; } }

.review-form { display: flex; flex-direction: column; gap: var(--s-3); }
.review-form textarea {
  width: 100%;
  min-height: 7rem;
  padding: var(--s-3);
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-md);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
}
.review-actions { display: flex; gap: var(--s-2); flex-wrap: wrap; align-items: center; }
.review-actions input[type="text"] {
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  font-size: var(--fs-sm);
}

/* --- Utilities --------------------------------------------------------------- */

.stack   { display: flex; flex-direction: column; gap: var(--s-4); }
.stack-s { display: flex; flex-direction: column; gap: var(--s-2); }
.row     { display: flex; gap: var(--s-3); align-items: center; flex-wrap: wrap; }
.between { display: flex; gap: var(--s-4); align-items: flex-start; justify-content: space-between; }
.hidden  { display: none !important; }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Voice switch. Sits in the transport row, so choosing a voice is as ordinary
   as pressing play rather than buried in a settings panel. */
.voices { display: flex; gap: var(--s-1); }
.voice-option { font-size: var(--fs-xs); padding: var(--s-1) var(--s-3); }

/* The character in the tutor rail. Small: it is company, not the subject. */
.tutor-face {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--rule);
}
.tutor-face .avatar { flex: none; animation: none; }
.tutor-face .avatar-card { max-width: none; }

/* ---------------------------------------------------------------------------
   Volume
   --------------------------------------------------------------------------- */

.volume { display: flex; align-items: center; gap: var(--s-2); }

.volume-btn {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-md);
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
}
.volume-btn:hover { background: var(--surface-2); }
.volume-btn[aria-pressed="true"] { color: var(--accent); border-color: var(--accent-edge); }

/* A short track: this is a secondary control and should not compete with the
   transport buttons for width. */
.volume-slider { width: 6.5rem; }
@media (max-width: 40rem) { .volume-slider { width: 4.5rem; } }

/* One character and its name card, stacked and centred on each other.
   Without this the slot is a plain block: the figure sits left while the name
   underneath fills the column, and the two do not line up. */
.avatar-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
}

/* `display: flex` above beats the `hidden` attribute's user-agent rule, so
   without this the character who is NOT speaking stays on screen and both
   appear at once. Any stylesheet that sets `display` on an element that is
   ever hidden needs this line; the global reset only covers what it can see. */
.avatar-slot[hidden] { display: none; }


/* A VRM character: a canvas, with the drawn character underneath as fallback.

   EXACTLY ONE IS VISIBLE, and the rule is driven by `data-avatar-status` rather
   than by the `hidden` property. Relying on `hidden` alone put both on screen at
   once — a 3D head above a drawn one — because a single missed assignment has no
   symptom until you look. Keyed on the status attribute the driver already
   writes, the markup cannot get into that state at all: no status means no
   model, which means show the drawing. */
.avatar-stage { position: relative; display: block; width: 100%; max-width: 20rem; }
.avatar-stage > canvas { display: block; width: 100%; height: auto; }
.avatar-stage > [hidden] { display: none !important; }

.avatar-stage:not([data-avatar-status="model"]) > canvas { display: none; }
.avatar-stage[data-avatar-status="model"] > svg.avatar { display: none; }
