/* Everything is carved from one stone. Color values live in tokens.css. */

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--stone);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
}

/* Ambient room light. One faint pool, not a gradient treatment. */
.roomlight {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(58% 46% at 50% 8%, var(--sheen), transparent 72%);
  opacity: 0.5;
}

#app {
  max-width: 58rem;
  margin: 0 auto;
  padding: var(--sp-xl) var(--sp-md);
  /* Clear the floating rail. env() is 0 on desktop, so this one rule covers the
     phone's home indicator too and needs no media query. */
  padding-bottom: calc(var(--sp-xl) + var(--rail) + env(safe-area-inset-bottom, 0px));
}

/* Display type is cut into the stone, not printed on it. */
h1, h2, h3, .prompt, .card-title, .tile-title, .banner-title, .formula, .eyebrow {
  text-shadow: var(--incised);
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.15;
  margin: 0;
}
h1 { font-size: var(--step-3); letter-spacing: 0.005em; }
h2 { font-size: var(--step-1); }
p { margin: 0 0 var(--sp-sm); }

.eyebrow {
  font-family: var(--font-label);
  font-size: var(--step-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--sp-xs);
}

.lede { font-size: var(--step-1); color: var(--ink-soft); max-width: 38ch; }
.sub { color: var(--ink-faint); font-size: var(--step--1); max-width: 62ch; }

/* The second paragraph under the landing title. Sits between .lede and .sub on
   purpose: a step down in size so it reads as support rather than a second
   headline, and a wider measure because .lede's 38ch would run it to five or
   six lines and make the screen taller than it already is. */
.lede-more {
  font-size: var(--step-0);
  color: var(--ink-soft);
  max-width: 54ch;
  margin-top: var(--sp-sm);
  line-height: 1.6;
}

button { font: inherit; cursor: pointer; }

:focus-visible {
  outline: 2px solid var(--glow);
  outline-offset: 4px;
  border-radius: var(--r-pebble);
}

/* ── Carved primitives ───────────────────────────────────────────────────── */

.primary {
  display: inline-block;
  margin-top: var(--sp-md);
  padding: 16px 40px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  color: var(--glow-text);
  font-weight: 600;
  letter-spacing: 0.01em;
  box-shadow: var(--raised);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.primary:hover,
.primary:active { box-shadow: var(--pressed); color: var(--glow-deep); }

.link {
  background: none;
  border: none;
  padding: 4px 2px;
  color: var(--ink-faint);
  font-size: var(--step--1);
  transition: color var(--dur-warm) var(--ease-settle);
}
.link:hover { color: var(--glow-text); }
.link.centred { display: block; margin: var(--sp-md) auto 0; }

/* ── Welcome ─────────────────────────────────────────────────────────────── */

/* --sp-lg, not --sp-xl. The welcome screen asks three questions and then offers
   the one button that matters; 56px of air under the masthead was 22px of the
   124 that pushed Begin off a 900px desktop screen. */
.welcome-head { margin-bottom: var(--sp-lg); }
.welcome-head h1 { margin-bottom: var(--sp-sm); }
/* Only here. .lede's 38ch suits the one-line sentence it carries on the
   dashboard and runs this two-sentence opening to seven lines at --step-1,
   which is a lot of large text directly under the title on a screen that was
   already measured at 1763px tall. Six lines at 46ch, and still inside the
   65-75 character band that stays comfortable to read. */
.welcome-head .lede { max-width: 46ch; }

/* The same Begin, kept in reach.
   Measured, not assumed: the welcome screen ran 1763px on a 375px viewport, so
   the inline button sat 841px below the fold and a first-time visitor scrolled
   two and a half screens to reach the one control that matters. Three questions
   do not fit in 667px, so tightening alone could never fix it and the button
   follows instead.

   No width gate. The condition was never "is this a phone", it is "has the real
   button gone off screen" - which the observer already knows, and which is just
   as true of a 900px laptop window where Begin sits 100px below the fold.
   One shape at every width: a full-bleed bar with the button centred in it.
   A bare floating pill was tried on wide screens and was wrong - with no ground
   of its own it landed in the middle of the body copy and covered the sentence
   behind it, reading as a rendering fault rather than a control. The bar brings
   its own stone, so nothing can ever show through it. */
/* Two ways in, side by side, wrapping to a column when they no longer fit.
   .primary carries its own margin-top, which would double up between two of
   them once they stack, so the row owns the spacing instead. */
.welcome-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm);
  margin-top: var(--sp-md);
}
.welcome-actions .primary { margin-top: 0; }

.welcome-cta {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  display: flex;
  justify-content: center;
  gap: var(--sp-sm);
  padding: var(--sp-sm) var(--sp-md);
  padding-bottom: calc(var(--sp-sm) + env(safe-area-inset-bottom, 0px));
  background: var(--stone);
  /* Not a border: the same stone lifted off the page, so its edge is shadow
     cast upward - the one direction the room light never comes from. */
  box-shadow: 0 -8px 20px var(--shade);
  transform: translateY(110%);
  transition: transform var(--dur-warm) var(--ease-settle);
}
.welcome-cta.showing { transform: translateY(0); }
/* Two buttons in a bar the width of a phone. The generous 40px of side padding
   a lone Begin could afford runs the pair off both edges at 375px, so inside
   the bar they share the width rather than claiming it. */
.welcome-cta .primary {
  margin-top: 0;
  flex: 1 1 0;
  min-width: 0;
  padding-inline: var(--sp-sm);
  text-align: center;
}

/* The bar spans the whole edge, so the two corner pills have nowhere to sit.
   They stand down while it is up - and on THIS screen they are the least useful
   they ever are: the material picker is already in the form, and the tutor has
   no drill or concept to look at until the learner has begun. Both return the
   moment the inline button is back on screen. */
body:has(.welcome-cta.showing) .theme-toggle,
body:has(.welcome-cta.showing) .tutor-toggle {
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .welcome-cta { transition-duration: 1ms; }
}

.field { margin-bottom: var(--sp-lg); }
.field h2 { margin-bottom: var(--sp-sm); }

.cards {
  display: grid;
  gap: var(--sp-md);
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  margin-top: var(--sp-md);
}

.card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  text-align: left;
  padding: var(--sp-md);
  border: none;
  border-radius: var(--r-slab);
  background: var(--stone);
  color: inherit;
  box-shadow: var(--pressed);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
/* Choosing is a rise, not a sink.
   These three sat raised and inverted to pressed when picked, with a warm bloom
   added because hover pressed them too and depth alone could no longer say
   which was chosen. Reversed: the group rests recessed into the stone and the
   chosen one is the single thing lifted out of it, so the answer to "which did
   I pick" is the one card standing above the page rather than a halo you have
   to look for. It also frees hover to be a real intermediate - the card rises
   part way under the pointer, which is a preview of the move rather than a
   state that mimics the destination.
   Three depths on one axis, no colour doing the structural work:
     resting  --pressed      cut into the stone
     hover    --pressed-sm   shallower, on its way up
     chosen   --raised       standing proud
   The bloom is gone, as asked. The amber title stays, and is not decoration:
   PRODUCT.md makes WCAG AA a requirement, and shadow is a low-contrast channel
   that some displays and most high-contrast modes flatten to nothing. Amber is
   what still answers the question when the depth does not survive. */
.card:hover { box-shadow: var(--pressed-sm); }
.card.selected { box-shadow: var(--raised); }
.card.selected .card-title { color: var(--glow-text); }
/* What the choice costs you in screens, under what it means.
   Same treatment as .tile-stat on the dashboard, and for the same reason: it is
   a number you check rather than a sentence you read, so it takes the smaller
   label size and tabular figures instead of repeating .card-detail's weight,
   which would have made the card two lines of prose at one volume.
   .card is already a flex column with a gap, so this needs no layout. */
.card-count {
  color: var(--ink-faint);
  font-size: var(--step-label);
  font-variant-numeric: tabular-nums;
}

/* Consequence of a choice already made on screen, shown before it is committed.
   Deliberately quiet: no amber, no red, no icon. The Light Means Live Rule keeps
   amber for live and earned state, and this is neither an error nor a danger,
   it is the app being straight about what the next press will do. Dressing it as
   an alarm is how a caution stops being read. */
.notice {
  margin-top: var(--sp-md);
  padding: var(--sp-sm) var(--sp-md);
  border-radius: var(--r-pebble);
  background: var(--stone);
  box-shadow: var(--pressed-sm);
  color: var(--ink-soft);
  font-size: var(--step--1);
  line-height: 1.55;
  max-width: 54ch;
}

.card-title { font-family: var(--font-display); font-size: var(--step-1); }
.card-detail { color: var(--ink-faint); font-size: var(--step--1); line-height: 1.5; }

.chips { display: flex; flex-wrap: wrap; gap: var(--sp-sm); margin-top: var(--sp-md); }

.chip {
  padding: 10px 20px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  color: var(--ink-soft);
  font-size: var(--step--1);
  box-shadow: var(--pressed-sm);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
/* The same rise as .card, one size down, so the two questions on this screen
   answer the pointer identically. */
.chip:hover { color: var(--ink); box-shadow: var(--pressed-xs); }
.chip.selected { color: var(--glow-text); box-shadow: var(--raised-sm); }

/* ── Reading surfaces ────────────────────────────────────────────────────── */

.concept-head { margin-bottom: var(--sp-lg); position: relative; }

/* The two ways out of a screen, where there used to be one inline-block link.
   Separated by space rather than by a drawn dot: a ::before on the second link
   sits inside that button's hit area, so the divider becomes part of the
   control it is dividing, and some screen readers read it out. The gap does the
   same job and answers to nobody. */
.screen-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  margin-bottom: var(--sp-md);
}
/* Tighter on the dashboard, where the row sits above an eyebrow rather than
   above a whole concept header. */
.dash-head .screen-nav { margin-bottom: var(--sp-sm); }
/* TEMP-SKIP: test-only control, corner-parked and muted so it never competes
   with the drill. Delete this block along with the other TEMP-SKIP spots. */
.drill-skip {
  position: absolute; top: 0; right: 0; margin: 0;
  padding: 2px var(--sp-xs);
  border: 1px dashed var(--rule);
  border-radius: var(--r-pebble);
  opacity: 0.6;
}
.drill-skip:hover { opacity: 1; }

.slide {
  padding: var(--sp-lg);
  border: none;
  border-radius: var(--r-slab);
  background: var(--stone);
  box-shadow: var(--settled);
  color: var(--ink-soft);
  font-size: var(--step-0);
  line-height: 1.68;
  /* The CARD is as wide as its widest figure; the PROSE keeps the 68ch measure
     it has always had (below). A keyboard drawn beside a staff at matching size
     needs ~760px, and 68ch is ~480px of it - held to the text measure, the
     staff had to shrink to roughly half the keyboard to fit, which is the one
     thing the pair exists to avoid. Text still wraps at 68ch, so nothing about
     reading changes; only the pictures are allowed past it. */
  max-width: 58rem;
  margin-bottom: var(--sp-md);
}
/* Everything but a figure is prose, and prose is measured for reading. Left
   rather than centred, so the paragraphs sit exactly where they always did and
   only the diagrams reach further right. */
.slide > * { max-width: 68ch; }
.slide > .diagram { max-width: 100%; }
/* A concept shown inside a drill has a drill's width to live in, not a page's,
   and its diagrams are illustrations beside a question rather than the subject
   of the screen. It keeps the old measure for both. */
.slide.inline { padding: var(--sp-md); max-width: 68ch; }
.slide h2 { color: var(--ink); margin-bottom: var(--sp-sm); }
.slide ul { margin: 0 0 var(--sp-sm); padding-left: 1.15rem; }
.slide li { margin-bottom: var(--sp-xs); }
/* The note-duration legend (durations concept): each fact gets the actual
   engraved shape it is describing rather than asking "quarter note" to
   conjure one. The glyph is decoration for a sighted reader on top of text
   that already says everything ("Quarter note: 1 beat"), so it is
   aria-hidden and costs a screen reader nothing. Same font stack as .clef,
   for the same reason: none of those faces actually carry the Musical
   Symbols block, and the browser falls back past them to one that does. */
.note-values { list-style: none; padding-left: 0; }
.note-values li { display: flex; align-items: center; gap: var(--sp-sm); }
.note-glyph {
  font-family: var(--font-display);
  font-size: 1.8em;
  color: var(--ink);
  line-height: 1;
  min-width: 1.4em;
  text-align: center;
}
/* Emphasis is weight and brightness, not light. Prose is inert; amber is for
   what the learner can act on (The Light Means Live Rule). */
.slide strong { color: var(--ink); font-weight: 600; }
.slide em { color: var(--ink); font-style: italic; }

/* A formula is engraved into the slab, not printed on it. */
.formula {
  font-family: var(--font-display);
  font-size: var(--step-1);
  letter-spacing: 0.14em;
  text-align: center;
  padding: var(--sp-md);
  margin: var(--sp-md) 0;
  border-radius: var(--r-well);
  background: var(--stone);
  box-shadow: var(--pressed-sm);
  color: var(--ink);
}

/* ── Question ────────────────────────────────────────────────────────────── */

.question {
  padding: var(--sp-lg);
  border-radius: var(--r-slab);
  background: var(--stone);
  box-shadow: var(--raised);
  text-align: center;
  animation: settle var(--dur-warm) var(--ease-settle) both;
}

/* The Question Is Display Rule. */
.prompt {
  font-family: var(--font-display);
  font-size: var(--step-2);
  line-height: 1.25;
  margin: 0 auto var(--sp-md);
  max-width: 24ch;
  text-wrap: balance;
}

.visual { display: flex; justify-content: center; margin-bottom: var(--sp-md); }

.choices {
  display: grid;
  gap: var(--sp-md);
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  margin-top: var(--sp-md);
}

.choice {
  padding: 18px 20px;
  border: none;
  border-radius: var(--r-pebble);
  background: var(--stone);
  color: var(--ink);
  font-size: var(--step-0);
  box-shadow: var(--raised);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.choice:hover:not(:disabled) { color: var(--glow-text); box-shadow: var(--pressed); }
/* Spent choices stop advertising themselves as actionable. */
.choice:disabled { cursor: default; color: var(--ink-faint); box-shadow: var(--pressed-xs); }

/* Verdict blooms and presses. It never changes the surface color. */
.choice.correct { color: var(--right); box-shadow: var(--pressed), 0 0 26px var(--right-halo); }
.choice.wrong { color: var(--wrong); box-shadow: var(--pressed), 0 0 26px var(--wrong-halo); }
.choice.reveal { color: var(--right); box-shadow: var(--pressed); }

.play {
  padding: 12px 26px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  color: var(--ink-soft);
  font-size: var(--step--1);
  box-shadow: var(--raised-sm);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.play:hover,
.play:active { color: var(--glow-text); box-shadow: var(--pressed-sm); }
.play-icon { font-size: 1.15em; }

.hint {
  margin: var(--sp-md) auto 0;
  padding: var(--sp-md);
  max-width: 52ch;
  border-radius: var(--r-well);
  background: var(--stone);
  box-shadow: var(--pressed-sm);
  color: var(--ink-soft);
  font-size: var(--step--1);
  text-align: left;
}

/* Finish the measure. Sits between the readout and the keyboard - the last thing
   under the notes you entered. Pressed and faint while the bar is incomplete, so
   it reads as "not yet" rather than as a button that does nothing; it rises and
   takes the candlelight once every column is in, which is also when it takes
   focus so Enter submits without tabbing. */
.entry-submit {
  display: block;
  margin: var(--sp-md) auto 0;
  padding: 12px 30px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  color: var(--ink);
  font-weight: 600;
  letter-spacing: 0.01em;
  box-shadow: var(--raised);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.entry-submit:disabled {
  cursor: default;
  color: var(--ink-faint);
  box-shadow: var(--pressed-xs);
}
.entry-submit:not(:disabled) { color: var(--glow-text); }
.entry-submit:not(:disabled):hover,
.entry-submit:not(:disabled):focus-visible { box-shadow: var(--pressed); color: var(--glow-deep); }

/* Per-column verdicts inside the readback, so a bar that was mostly right says
   so rather than going uniformly red. */
.entry-read span.correct { color: var(--right); }
.entry-read span.wrong { color: var(--wrong); }

/* The step to the next question - manual now, not a timer. A raised pill centred
   under the answer, resting in ink and lighting amber only on hover/focus, so the
   candlelight still marks the live thing rather than every button. It gets focus
   the moment an answer lands, so Enter carries straight on. */
.next-question {
  display: block;
  margin: var(--sp-lg) auto 0;
  padding: 14px 34px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  color: var(--ink);
  font-weight: 600;
  letter-spacing: 0.01em;
  box-shadow: var(--raised);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.next-question:hover,
.next-question:focus-visible { color: var(--glow-text); box-shadow: var(--pressed); }

/* ── Progress ────────────────────────────────────────────────────────────── */

.progress-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-sm);
  margin-bottom: var(--sp-md);
  flex-wrap: wrap;
}
.progress-label { color: var(--ink-faint); font-size: var(--step--1); }

/* The pip's own material language, drawn long instead of round: a hole floored
   in --shade and pressed inward, filled with solid light. Not a gradient - the
   fill is one colour with a --glow-deep lip inside its lower edge, the same
   two-part treatment .pip.on uses, so the bar reads as the same object family
   rather than as a widget borrowed from somewhere else. */
.bar-wrap {
  position: relative;
  width: 100%;
  max-width: 20rem;
}
/* Its own inset rather than --pressed-xs. That token's diagonal 2px and 5px
   offsets are sized for a button; in a 10px-tall trough they nearly meet in the
   middle, and on a dark material the sheen half read as a bright silver tube
   round the empty bar. A horizontal groove is lit from above anyway, so the
   shadow belongs on the top edge and the sheen on the bottom, vertical only. */
.bar {
  position: relative;
  width: 100%;
  height: 10px;
  border-radius: var(--r-full);
  background: var(--shade);
  box-shadow: inset 0 2px 3px var(--shade), inset 0 -1px 1px var(--sheen);
}
.bar-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: var(--fill, 0%);
  border-radius: var(--r-full);
  background: var(--glow);
  box-shadow: 0 0 10px var(--glow-halo), inset 0 -1px 2px var(--glow-deep);
  /* No transition on width. It would be a layout property to animate, and here
     it could not even pay for that: renderDashboard replaces the whole section
     with innerHTML, so this element is built fresh at its final width with no
     previous value to travel from. Measured - the width is identical at 0ms and
     700ms and no CSSTransition is ever in flight. Growing the bar as a concept
     is mastered would need the mount to animate it deliberately, not a
     declaration that quietly never fires. */
}

/* The exact figure, on hover, sitting at the tip of the fill so it points at
   where you actually are. Clamped off both ends: at 0% or 100% an uncorrected
   translate would hang the chip half outside the bar. */
.bar-pct {
  position: absolute;
  bottom: calc(100% + 7px);
  left: clamp(1.6rem, var(--fill, 0%), calc(100% - 1.6rem));
  transform: translateX(-50%);
  padding: 3px 10px;
  border-radius: var(--r-full);
  background: var(--stone);
  box-shadow: var(--raised-sm);
  color: var(--ink-soft);
  font-size: var(--step-label);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-warm) var(--ease-settle);
}
.bar-wrap:hover .bar-pct { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .bar-pct { transition-duration: 1ms; }
}

.pips { display: flex; gap: 9px; }
/* An unlit pip is a hole cut down to the darker floor, like the keybed. A pip
   floored in --stone is invisible on a pale ground, and the distance to mastery
   has to be readable at a glance — that's a product principle, not a nicety. */
.pip {
  width: 11px;
  height: 11px;
  border-radius: var(--r-full);
  background: var(--shade);
  box-shadow: var(--pressed-xs);
  transition: box-shadow var(--dur-warm) var(--ease-settle), background var(--dur-warm);
}
.pip.on {
  background: var(--glow);
  box-shadow: 0 0 10px var(--glow-halo), inset 0 -1px 2px var(--glow-deep);
}

.drill-status { margin-bottom: var(--sp-md); text-align: center; }
.drill-status .pips { justify-content: center; flex-wrap: wrap; }
.next-rank { color: var(--ink-faint); font-size: var(--step--1); margin-bottom: var(--sp-sm); }
/* The method, sitting under the target it answers. Same quiet ramp as the line
   above - it is not something to act on this second, so it takes no light - but
   held to a sentence width and a step brighter, so it reads as advice and not
   as a second statistic. */
.next-rank.coach { max-width: 46ch; margin: 0 auto var(--sp-sm); color: var(--ink-soft); }

/* ── Rank marks ──────────────────────────────────────────────────────────── */

.rank {
  display: inline-block;
  padding: 5px 14px;
  border-radius: var(--r-full);
  background: var(--stone);
  font-family: var(--font-label);
  font-size: var(--step-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--ink-faint);
  box-shadow: var(--pressed-sm);
}
.rank-iron { color: var(--iron); }
.rank-bronze { color: var(--bronze); }
.rank-silver { color: var(--silver); }
.rank-gold { color: var(--gold); box-shadow: var(--pressed-sm), var(--warm); }
.rank-platinum { color: var(--platinum); }
.rank-diamond { color: var(--diamond); }
/* Master is the pinnacle - it carries the warm glow, like Gold, to read as earned. */
.rank-master { color: var(--master); box-shadow: var(--pressed-sm), var(--warm); }

/* ── Dashboard ───────────────────────────────────────────────────────────── */

.dash-head { margin-bottom: var(--sp-lg); }
/* Tucked under the sentence it measures, not floating between it and the next
   heading - it belongs to "4 of 10 concepts mastered", not to the page. */
.dash-head .bar-wrap { margin-top: var(--sp-sm); }
.section { margin-bottom: var(--sp-xl); }
.section h2 { margin-bottom: var(--sp-xs); }

.tiles {
  display: grid;
  gap: var(--sp-md);
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  margin-top: var(--sp-md);
}

.tile {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  text-align: left;
  padding: var(--sp-md);
  border: none;
  border-radius: var(--r-slab);
  background: var(--stone);
  color: inherit;
  box-shadow: var(--raised);
  transition: box-shadow var(--dur-warm) var(--ease-settle);
}
/* Resting states first, hovers after - and that order is load-bearing, not
   tidiness. Every modifier here re-declares box-shadow at the SAME specificity
   as .tile:hover (two classes versus a class plus a pseudo-class), so the one
   written last wins. A `.tile.earned` sitting below `.tile:hover` ate the hover
   outright and the top-ranked tiles went dead to the pointer; `.tile.review`
   had been doing it quietly for longer. Anything added below must be a hover.

   A drill at the top rank is the one thing on a tile that was EARNED, which is
   what The Light Means Live Rule permits amber for. Without it a finished drill
   looked identical to one never opened, and the six ranks of work in between
   showed up only as a small badge. Tiles outside the chosen focus sit back
   instead, so the dashboard is not nine equally-shouting cards. */
.tile.review,
.tile.earned { box-shadow: var(--raised), var(--warm); }
/* `.tile.muted` sat here, softening tiles outside the learner's chosen focus
   areas. That question is gone from the welcome screen and so is the class:
   every tile now sits at one elevation, and the drill groups do the sorting
   work the dimming used to gesture at. */

/* The heading over each drill group. A rung between the section h2 and a tile
   title, and outside .tiles so it never takes a grid cell. */
.group-title {
  margin: var(--sp-md) 0 var(--sp-sm);
  font-size: var(--step-0);
  color: var(--ink-soft);
}
.section .group-title:first-of-type { margin-top: var(--sp-sm); }

.tile:hover,
.tile:active { box-shadow: var(--pressed); }
/* Pressed but still lit: the amber says what the tile IS, not how high it sits,
   so letting hover strip it read as the drill being un-earned under the cursor. */
.tile.review:hover,
.tile.review:active,
.tile.earned:hover,
.tile.earned:active { box-shadow: var(--pressed), var(--warm); }

.tile-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-sm); }
.tile-head .tile-title { min-width: 0; }
.tile-head .rank { flex: 0 0 auto; }
.tile-title { font-family: var(--font-display); font-size: var(--step-1); }
/* A step up from --step--1. The blurb is the sentence that says what the drill
   actually trains, and it was set smaller than body text while being the thing
   most worth reading on the card. */
.tile-blurb { color: var(--ink-soft); font-size: var(--step-0); line-height: 1.55; }

/* Title, blurb, goal, stat - four rungs, not four things at one weight.
   The goal and the average time used to share a row at the same size and
   colour, so "Next: Bronze, needs 5 in a row" read as a second statistic
   rather than as the thing to aim at. */
.tile-foot {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: auto;
  padding-top: var(--sp-xs);
}
/* No amber here, deliberately: a rank not yet reached is neither live nor
   earned, so the goal is raised by WEIGHT and its own line, never by light. */
.tile-goal { color: var(--ink-soft); font-size: var(--step--1); }
.tile-stat {
  color: var(--ink-faint);
  font-size: var(--step-label);
  font-variant-numeric: tabular-nums;
}

/* Asking is available, not earned and not in progress, so the card stays in the
   calm register: the same slab as any tile and no light on it. */
.tutor-card { max-width: 28rem; }

.banner {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  width: 100%;
  text-align: left;
  padding: var(--sp-md);
  margin-bottom: var(--sp-lg);
  border: none;
  border-radius: var(--r-slab);
  background: var(--stone);
  color: inherit;
  box-shadow: var(--raised), var(--warm);
  transition: box-shadow var(--dur-warm) var(--ease-settle);
}
.banner:hover,
.banner:active { box-shadow: var(--pressed); }
.banner-title { font-family: var(--font-display); font-size: var(--step-1); color: var(--glow-text); }
.banner-detail { color: var(--ink-soft); font-size: var(--step-0); line-height: 1.55; }

/* Two unrelated things live here: an offer (the account shelf) and the one
   destructive escape hatch. They were touching edge to edge, which grouped them
   by proximity and made "Start over" read as part of signing in. The gap is
   what separates them - no divider, nothing added. */
.dash-foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-lg);
  margin-top: var(--sp-xl);
  text-align: center;
}
/* The shelf carries its own top margin for the welcome screen, where it is the
   last thing on the page. Inside this footer the gap owns the rhythm. */
.dash-foot .account { margin-top: 0; }

/* ── Account ─────────────────────────────────────────────────────────────── */

/* Signing in is an offer, not a gate, so the strip sits at the calm end of the
   register: a settled shelf, no lift, nothing competing with the work above. */
.account {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-sm);
  margin: var(--sp-lg) auto 0;
  padding: var(--sp-md);
  max-width: 46ch;
  border-radius: var(--r-well);
  background: var(--stone);
  box-shadow: var(--settled);
}

.account-note,
.account-name {
  color: var(--ink-soft);
  font-size: var(--step--1);
}
.account-name { color: var(--ink); font-weight: 600; }

/* Google renders its own button into here; it brings its own styling, so this
   only reserves the space and keeps it centred. */
.gsi { display: flex; justify-content: center; min-height: 40px; width: 100%; }

.sync {
  flex-basis: 100%;
  color: var(--ink-faint);
  font-size: var(--step-label);
  text-align: center;
}
/* Saving is transient and unremarkable; only a real failure earns attention,
   and even then it says the work is safe (The Light Means Live Rule). */
.sync-error { color: var(--glow-text); }

.account-error {
  flex-basis: 100%;
  margin: 0;
  color: var(--glow-text);
  font-size: var(--step--1);
  text-align: center;
}

/* ── Mastered ────────────────────────────────────────────────────────────── */

/* The payoff. The one place the whole slab is allowed to warm, and the only
   place the authored moment runs at full length. */
.mastered {
  position: relative;
  text-align: center;
  padding: var(--sp-xl) var(--sp-md) var(--sp-xl);
}
.mastered::before {
  content: "";
  position: absolute;
  inset: -10% -20% auto;
  height: 70%;
  z-index: -1;
  background: radial-gradient(50% 60% at 50% 40%, var(--glow-halo), transparent 70%);
  animation: rise-warm 1400ms var(--ease-settle) both;
}
.mastered .lede { margin: var(--sp-sm) auto 0; }
.mastered h1 { color: var(--glow-text); }
.mastered .pips { justify-content: center; margin-bottom: var(--sp-md); }

@keyframes rise-warm {
  from { opacity: 0; transform: translateY(26px) scale(0.9); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .mastered::before { animation-duration: 1ms; }
}

/* ── The one authored moment: stone warming, then settling ───────────────── */

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

/* ── Instruments ─────────────────────────────────────────────────────────── */

/* Both instruments sit in a well cut into the stone. */
.instrument {
  display: inline-flex;
  padding: var(--sp-md) var(--sp-md) calc(var(--sp-md) + 2px);
  border-radius: var(--r-well);
  background: var(--stone);
  box-shadow: var(--pressed);
}

/* ── Diagrams in the teaching ────────────────────────────────────────────── */

/* A picture and the sentence that explains it. The instrument keeps its own
   pressed well, so the figure adds spacing and the caption and nothing else -
   a second surface around it would be a well inside a well. */
.diagram {
  margin: var(--sp-md) 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-xs);
}
.diagram .keyboard,
.diagram .staff { max-width: 100%; height: auto; }
.diagram figcaption {
  color: var(--ink-faint);
  font-size: var(--step--1);
  line-height: 1.55;
  max-width: 54ch;
}

/* A piano diagram beside its staff counterpart, sharing one caption below
   both. Row rather than the plain .diagram's column, and wraps on a narrow
   screen rather than shrinking either picture past legibility.

   --pair-h is the height BOTH pictures are cut to, and it is the whole of how
   they line up. */
.diagram-pair-row {
  --pair-h: 8.5rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-md);
}
/* The pair is anchored to a shared HEIGHT, not a shared width.
   This used to pin both to width:min(100%, 260px), which lines up the left and
   right edges and nothing else - an svg with a viewBox scales its contents to
   whatever box you give it, so fixing the width hands each picture a different
   height and a different internal scale. The keyboard's box is always 308x176,
   but the staff's grows with the note count, so the same 260px rendered a
   two-note staff 198px tall at 149% scale and a seven-note scale 83px tall at
   63% - beside a keyboard that was 149px tall in both. The staff was a
   different size in every lesson and matched the keyboard in none of them.
   Cutting both to one height instead keeps the notation at one size throughout
   and puts the two pictures on the same top and bottom edge, which is what
   "beside" is supposed to mean.
   Widths follow from the height, so a wide diagram is wide rather than
   squashed: a triad's staff comes out ~228px and sits next to the keyboard, a
   seven-note scale comes out ~424px and takes the next line at full size,
   which is the wrap this row already allowed for. */
.diagram-pair .keyboard,
.diagram-pair .staff {
  flex: none;
  width: auto;
  max-width: 100%;
}
/* Sized and centred on the KEYBED, not on the svg box.
   A keyboard that reserves a callout bubble above itself (CALLOUT_SPACE in
   ui.js) carries that empty band inside its own viewBox: the box is 284 units
   tall where the keys are only 176 of them, sitting at the bottom. Height on
   the box therefore drew the keys at 62% of a plain keyboard's size, and
   align-items:center lined up the two BOXES - which hangs the keys half the
   reserved band lower than the staff beside them, the picture sitting visibly
   below its own caption's other half. Dividing by --kb-units gives the keybed
   the full --pair-h whether a bubble is reserved or not, and the translate
   lifts it by half the band so the keys, not the padding, sit on the staff's
   centre line. Both are no-ops on the keyboards that reserve nothing. */
.diagram-pair .keyboard {
  height: calc(var(--pair-h) * var(--kb-units, 176) / 176);
  transform: translateY(calc(var(--pair-h) * var(--kb-pad, 0) / -352));
}
/* Sized and centred on the five LINES, not on the svg box - the staff's half of
   the same correction the keyboard gets above.
   Scaling by the box (--staff-units / 132) makes the box --pair-h tall, and
   only 60 of those 132 units are lines: the notation came out 62px beside 136px
   of keys, which read as a small picture next to a big one even though the two
   boxes matched exactly. The rest of the box is ledger room staffSvg reserves
   whether or not any note uses it. Dividing by --staff-lines gives the LINES
   the full --pair-h instead, so a staff line block and a keybed are the same
   height, and --staff-mid re-centres on the lines because ledger room is not
   added symmetrically - a low note grows the box downward only, which would
   otherwise hang the staff above the keyboard by half of what it added. */
.diagram-pair .staff {
  height: calc(var(--pair-h) * var(--staff-units, 132) / var(--staff-lines, 60));
  transform: translateY(calc(
    var(--pair-h) * var(--staff-mid, 0) / var(--staff-lines, 60) * -1));
}

/* A note the slide names gets its own sentence (or an animated swap of two
   spellings - see calloutContent() in ui.js) in a bubble above it, on hover
   only - see the callout() comment in ui.js for why this stays a mouse
   affordance rather than a focusable one. Shared by the keyboard and the
   chromatic strip, hence "note" rather than "key". Positioned in reserved
   viewBox space above the note (CALLOUT_SPACE in ui.js), so it scales with
   the diagram and needs no separate layout of its own.

   Two elements, not one: the slot is the full reserved space and carries no
   chrome - it just pins the bubble to the bottom-center of that space, next
   to the note. Sizing the bubble itself to the slot (as one div used to do)
   painted its background/border across the whole reserved height even when
   the text was one short line, leaving a tall blank slab above the words.
   Splitting layout (slot) from chrome (bubble) lets the bubble stay
   content-sized while the slot still anchors it where it belongs. */
.note-callout-slot {
  height: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  pointer-events: none;
}
.note-callout {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--dur-warm) var(--ease-settle), transform var(--dur-warm) var(--ease-settle);
  text-align: center;
  background: var(--stone);
  color: var(--ink);
  font: 500 12px var(--font-body);
  line-height: 1.35;
  padding: 8px 10px;
  border-radius: var(--r-pebble);
  /* The foreignObject that hosts this bubble is sized to the slot, not the
     bubble (see CALLOUT_SPACE in ui.js), which clips the blur the raised
     shadow needs to read - against a diagram that's the same stone as the
     bubble, the clipped shadow left no visible edge at all. A --rule border
     draws inside the box instead of relying on overflow, so it survives the
     clip - same fix, same token, as the staff-line bug noted on --rule above. */
  border: 1px solid var(--rule);
  box-shadow: var(--raised-sm);
}
.note-callout-target { cursor: help; }
.note-callout-target:hover .note-callout { opacity: 1; transform: translateY(0); }

/* The two spellings in a swap callout (calloutContent()'s { swap } branch)
   share one grid cell so the crossfade never reflows the bubble around it. */
.note-callout-cycle { display: grid; }
.note-callout-a,
.note-callout-b { grid-area: 1 / 1; }
.note-callout-a { animation: note-callout-a 4.4s ease-in-out infinite; }
.note-callout-b { animation: note-callout-b 4.4s ease-in-out infinite; }
.note-callout-static { display: none; }

@keyframes note-callout-a {
  0%, 36% { opacity: 1; }
  50%, 86% { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes note-callout-b {
  0%, 36% { opacity: 0; }
  50%, 86% { opacity: 1; }
  100% { opacity: 0; }
}

/* The always-on dot marking a note as callout-bearing. cursor:help on
   .note-callout-target needs a hover to ever be seen, which is no
   affordance at all for a first-time visitor with no reason yet to try
   hovering a picture - the dot is the thing that says "there's more here"
   before the pointer arrives. It fades out once the bubble it pointed at
   is actually open, so the two never compete for the same look. Not amber:
   The Light Means Live Rule (DESIGN.md) reserves that for state the learner
   is currently in or has earned, and an unhovered hint is neither. */
.note-hint {
  fill: var(--rule);
  animation: note-hint-breathe 2.6s ease-in-out infinite;
}
/* A running animation's value for a property always wins over a plain
   declaration for that same property, paused or not - so silencing the
   breathe on hover takes killing the animation itself, not just setting
   opacity underneath it. */
.note-callout-target:hover .note-hint { animation: none; opacity: 0; }

@keyframes note-hint-breathe {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 0.85; }
}

@media (prefers-reduced-motion: reduce) {
  .note-callout { transition-duration: 1ms; }
  .note-callout-cycle { display: none; }
  .note-callout-static { display: inline; }
  .note-hint { animation: none; opacity: 0.6; }
}

@media (max-width: 34rem) {
  /* The same drop the bare .staff takes on a phone, applied to the pair's own
     anchor instead - .diagram-pair .staff outranks that rule, so the height has
     to come down here or the pair would keep its desktop size on a phone. Both
     pictures shrink together, so they stay the same height as each other. */
  .diagram-pair-row { --pair-h: 6.5rem; }
}

/* The diagram-world radios in the settings panel. Same depth vocabulary as the
   proficiency cards on the welcome screen, and the same way round: the group
   rests recessed and the chosen one is lifted out, per The Chosen One Rises
   Rule in DESIGN.md. Amber on the chosen title carries it for anyone whose
   display flattens the shadows. */
.theme-modes { display: flex; flex-direction: column; gap: var(--sp-xs); }
.theme-mode {
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-align: left;
  padding: var(--sp-sm) var(--sp-md);
  border: none;
  border-radius: var(--r-pebble);
  background: var(--stone);
  color: inherit;
  box-shadow: var(--pressed-sm);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.theme-mode:hover { box-shadow: var(--pressed-xs); }
.theme-mode.selected { box-shadow: var(--raised-sm); }
.theme-mode.selected .theme-mode-title { color: var(--glow-text); }
.theme-mode-title { font-size: var(--step--1); }
.theme-mode-detail { color: var(--ink-faint); font-size: var(--step-label); }

/* The instrument-neutral diagram: twelve equal cells, one per half step.
   Cells are grooves cut into the stone rather than raised faces, because this is
   a ruler and not a set of controls - nothing here is pressable, and giving it
   the keyboard's raised keys would promise otherwise. A lit cell is filled with
   the same warm light the lit key uses, so "this note is in" reads identically
   across both diagrams. */
.chromatic { max-width: 100%; height: auto; }
.chromatic .cell rect {
  fill: var(--stone);
  stroke: var(--rule);
  stroke-width: 1;
  opacity: 0.55;
}
/* The five with no letter are the ones needing a sharp or a flat to write.
   Held back rather than hidden: they are still half steps and still counted. */
.chromatic .cell.between rect { opacity: 0.3; }
.chromatic .cell.lit rect { fill: var(--glow-halo); stroke: var(--glow-edge); opacity: 1; }
.chromatic .cell text { text-anchor: middle; fill: var(--ink-soft); }
.chromatic .cell.lit text { fill: var(--glow-deep); }
.chromatic .span path { stroke: var(--rule); stroke-width: 1.4; fill: none; }
.chromatic .span text { text-anchor: middle; fill: var(--ink-faint); }

/* Names printed on the keys, for the diagrams whose whole job is saying which
   key is which. Never on a playable board: naming the keys during a reading
   drill would answer the question the drill is asking.
   The SIZE is not here. It is a font-size attribute on the <text>, computed from
   the key width, the same way the clef and the signature accidentals are done.
   The type ramp is in rem and describes document text; a rem inside an svg does
   not scale with the viewBox, so a labelled keyboard would keep its lettering at
   full size while the drawing shrank on a phone. */
.key-name {
  text-anchor: middle;
  fill: var(--ink-soft);
  pointer-events: none;
}
/* On a black key the label sits on the dark face, so it takes the light side of
   the palette rather than the ink. --stone flips with the rendition and the key
   face does not, so this is spelled out rather than inherited. */
.key-name.on-black { fill: #e8ecf5; }
.key-lit-face + .key-bloom + .key-name,
.key-lit-face ~ .key-name { fill: var(--glow-deep); }

/* ── Answering by name instead of by key ─────────────────────────────────── */

.answer-mode { margin-bottom: var(--sp-sm); }

/* Twelve pitch classes in keyboard order, wrapping where they must. Not a grid
   of twelve equal columns: that reads as a calculator, and at 375px each cell
   would be under 30px. Flex lets the row break where the width runs out while
   the left-to-right order still matches the keys it replaces. */
/* One column per letter, three spellings deep - see noteNameInput() in ui.js.
   A fixed seven columns rather than a wrapping row: the columns ARE the
   meaning here, and a flex-wrap that reflowed six-and-six on a narrow screen
   would break the one thing the layout is saying. They narrow together
   instead, and the whole board scrolls if the screen cannot hold seven. */
.pitch-map {
  display: grid;
  grid-template-columns: repeat(7, minmax(2.6rem, 1fr));
  gap: var(--sp-xs);
  max-width: 34rem;
  overflow-x: auto;
}
.pitch-col {
  display: grid;
  gap: var(--sp-xs);
}

.pitch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 12px 8px;
  border: none;
  border-radius: var(--r-pebble);
  background: var(--stone);
  color: var(--ink);
  font-size: var(--step-0);
  box-shadow: var(--raised-sm);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.pitch:hover { box-shadow: var(--pressed-sm); }
/* The Press Inverts Rule, and the same amber-plus-depth pairing the drill
   keyboard uses for a chosen key, so a note entered here looks entered there. */
.pitch.selected { color: var(--glow-text); box-shadow: var(--pressed-sm); }

/* Every spelling is its own button now, so there is no quieter second label to
   style: a flat is not an afterthought to the natural above it, it is the
   thing some answers need. .pitch carries all three the same way. */

.keyboard { width: min(100%, 330px); height: auto; }
/* Keys are faces raised out of the keybed. No strokes: the groove between keys
   shows the darker floor, and each key catches light on its left edge and lip. */
.keybed { fill: var(--shade); }
.key-white { fill: var(--stone); }
/* A piano's black keys are black in any light — they can't follow the shadow
   token, or they turn pale in the daylight rendition. */
.key-black { fill: var(--key-black); }
.key-edge { stroke: var(--sheen); stroke-width: 1.2; stroke-linecap: round; opacity: 0.5; }
.key-lip { stroke: var(--sheen); stroke-width: 2; stroke-linecap: round; opacity: 0.75; }
/* Lit from beneath: the face warms toward its base and blooms under it. */
.key-lit-face { stroke: var(--glow-edge); stroke-width: 1.2; }
.lit-top { stop-color: var(--stone); }
.lit-bottom { stop-color: var(--glow); }
.key-bloom { fill: var(--glow); opacity: 0.5; }

/* Master chord entry: the notes picked for the current chord stay lit GREEN - a
   held selection, like shift-selecting files - and flip RED in place if the
   finished chord is wrong. Green is the app's "correct" colour, which is what a
   held-then-confirmed pick becomes; the keys the learner pressed are exactly the
   ones that turn. */
.key.selected .key-white,
.key.selected .key-black { fill: var(--right); }
.key.selected .key-touch { fill: var(--right); opacity: 0.55; }
.key.wrong .key-white,
.key.wrong .key-black { fill: var(--wrong); }
.key.wrong .key-touch { fill: var(--wrong); opacity: 0.55; }

/* Capped by HEIGHT, not width. The viewBox now grows sideways with the content -
   a three-note bar in six flats is nearly twice the width of a bare single note -
   so a width cap scaled every staff to the same width and made the SIMPLE ones
   render tallest, with the complex ones shrunk to nothing. Fixing the height
   instead keeps one staff size everywhere and lets the bar get as long as the
   music needs, which is how notation behaves on paper. */
/* Height scales with the box rather than fixing it. staffSvg grows its viewBox
   to hold notes far out on ledger lines, and 132 is the plain five-line box it
   grows from; multiplying by the ratio keeps the five lines the same size on
   screen and makes the ELEMENT taller instead of squashing the staff into it. */
.staff {
  height: calc(8.5rem * var(--staff-units, 132) / 132);
  width: auto;
  max-width: 100%;
}
/* Staff and ledger lines are notation the learner has to READ, so they use the
   --rule token rather than the shadow color. Borrowing --shade put them at about
   1.4:1 against the ground and made the staff effectively invisible. The sheen
   lip below each line keeps the engraved feel without costing legibility. */
.staff-cut { stroke: var(--rule); stroke-width: 1.5; }
.staff-lip { stroke: var(--sheen); stroke-width: 1.1; opacity: 0.55; }
/* A barline closes a measure. Same engraved cut+lip as the staff lines, a touch
   bolder so it reads as a boundary rather than a sixth line. */
.staff .barline .staff-cut { stroke-width: 2.4; }
.staff .barline .staff-lip { stroke-width: 1.6; }
.clef { fill: var(--ink); font-family: var(--font-display); }
/* Key signatures and written accidentals are content, like the clef, so they
   take --ink and not the accent. Amber here would say "this is live", which a
   key signature is not - it is something to read. */
.accidental { fill: var(--ink); font-family: var(--font-body); }
.notehead { fill: var(--glow); }
.note-bloom { fill: var(--glow); opacity: 0.45; }

/* The invisible, generously-sized hover target around a staff legend dot
   (staffSvg's `legend` option, sideCalloutMarkup). transparent rather than
   none: an SVG shape with no fill does not register pointer events over its
   interior, only its stroke, and this one has no stroke either. */
.note-hit { fill: transparent; }
/* A legend bubble opens sideways from its own dot rather than upward from a
   reserved band above it - see sideCalloutMarkup's comment for why the
   staff's nine positions, 7.5px apart, have no room for the keyboard's
   approach. Left-anchored and vertically centred on the dot, instead of
   .note-callout-slot's bottom-centred default. */
.note-callout-slot.side {
  align-items: center;
  justify-content: flex-start;
}

/* ── The staff you answer on ─────────────────────────────────────────────── */

/* Taller than the staff you read, because every position on it is a target and
   a target has to be big enough to hit. The gap is wider in the SVG too - see
   staffInput() in ui.js for why the two geometries are kept apart. */
.staff-input { height: 15rem; }
/* .instrument is inline-flex so it shrink-wraps whichever instrument it holds.
   The keyboard is wide enough to always wrap onto its own line; this staff is
   not, so it would sit BESIDE the Play button. Still shrink-wrapped, but block
   level and centred, so the pressed well hugs the staff and nothing shares its
   line. */
[data-staff] { display: flex; width: fit-content; margin-inline: auto; }
/* The hit area is invisible until it is wanted: a staff covered in boxes stops
   looking like notation. Pressed rather than raised, because choosing a position
   is pressing into the slab, not lifting something off it. */
.slot-hit { fill: transparent; }
.slot { cursor: pointer; }
.slot:hover .slot-hit,
.slot:focus-visible .slot-hit { fill: var(--shade); opacity: 0.5; }
.slot:focus { outline: none; }
.slot:focus-visible .slot-hit { outline: 2px solid var(--glow); outline-offset: 2px; }
/* The notehead is the answer, so it stays hidden until this position is the one
   being considered. Ledger lines come with it: a ledger line exists FOR a note,
   so drawing them on an empty staff would be inventing notation. */
.slot-head, .slot-ledger { opacity: 0; }
.slot:hover .slot-head,
.slot:focus-visible .slot-head { opacity: 0.42; }
.slot:hover .slot-ledger,
.slot:focus-visible .slot-ledger { opacity: 0.55; }
.slot.chosen .slot-head { opacity: 1; }
.slot.chosen .slot-ledger { opacity: 1; }
/* Verdicts borrow the keyboard's jade and clay rose, so right and wrong mean the
   same thing on both instruments. */
.slot.correct .slot-head { opacity: 1; fill: var(--right); }
.slot.wrong .slot-head { opacity: 1; fill: var(--wrong); }
/* Where it SHOULD have gone, shown beside the mistake rather than instead of it. */
.slot.reveal .slot-head { opacity: 0.85; fill: var(--right); }
.slot.reveal .slot-ledger { opacity: 0.8; }
/* Deliberately NOT shrunk on small screens, unlike the staff you only read.
   Every position here is a touch target, and a phone is where they are hardest
   to hit. `.staff`'s max-width still keeps a six-sharp staff from overflowing;
   it scales the whole thing down together rather than by squashing the height. */

/* ── Playing the keyboard ────────────────────────────────────────────────── */

/* The same keyboard component, now answering a question. Hover and focus warm
   the key from underneath exactly as the lit state does, because a key under
   the pointer IS live - which is what the accent is for. */
.playable .key { cursor: pointer; }
.key-touch {
  fill: var(--glow);
  opacity: 0;
  transition: opacity var(--dur-warm) var(--ease-settle);
  pointer-events: none;
}
.playable .key:hover .key-touch,
.playable .key:active .key-touch { opacity: 0.55; }

/* SVG will not draw an outline for a focused group, so each key carries its own
   ring. Same 2px amber as :focus-visible everywhere else in the app. */
.key-ring {
  fill: none;
  stroke: var(--glow);
  stroke-width: 2.5;
  opacity: 0;
  pointer-events: none;
}
.playable .key:focus-visible { outline: none; }
.playable .key:focus-visible .key-ring { opacity: 1; }

/* What has been played so far. A well, because the answer is spoken INTO the
   stone - the same reading as a learner's turn in the tutor thread. */
/* Three tracks so the readout sits on the SAME centre axis as the prompt, the
   staff, Play and the keyboard. A centred flex row would push the pill left by
   half of Undo's width, because Undo shares its line. The 1fr gutters keep the
   pill dead-centre while Undo rides in the right one - and because the track
   exists whether Undo is shown, hidden or removed, the pill never jumps. */
.entry {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--sp-md);
  margin: var(--sp-md) 0 var(--sp-sm);
}
.entry-read {
  grid-column: 2;
  margin: 0;
  padding: 10px var(--sp-md);
  min-width: 8ch;
  border-radius: var(--r-full);
  box-shadow: var(--pressed-xs);
  font-family: var(--font-display);
  font-size: var(--step-1);
  letter-spacing: 0.04em;
  text-align: center;
  color: var(--ink);
}
/* Empty before the first press. A zero-width strut holds the line box open so
   the pill does not grow taller the moment a note lands in it. */
.entry-read::before { content: "\200b"; }
.entry-read.correct { color: var(--right); box-shadow: var(--pressed-xs), 0 0 22px var(--right-halo); }
.entry-read.wrong { color: var(--wrong); box-shadow: var(--pressed-xs), 0 0 22px var(--wrong-halo); }
.entry-undo { grid-column: 3; justify-self: start; font-size: var(--step--1); }
/* Hidden rather than greyed when there is nothing to take back: a disabled
   control that stays visible just invites a click. Its grid track holds the
   space, so the readout does not shift when Undo comes or goes. */
.entry-undo:disabled { visibility: hidden; }
/* Shown only when the answer was wrong, so it reads as the correction. Its own
   line beneath the entry, never beside it - side by side the two read as a pair
   of options rather than as "you played this, it was that". */
.entry-reveal {
  margin: 0 0 var(--sp-sm);
  color: var(--right);
  font-family: var(--font-display);
  font-size: var(--step-1);
  text-align: center;
}
.entry-reveal[hidden] { display: none; }

/* The keyboard is an answer here, not an illustration, so it sits closer to the
   entry than the staff does - the whole question has to fit above the fold or
   the learner scrolls to answer every time.

   Two octaves at 616px render each white key at exactly its drawn 44px, which
   is also the touch target everything else in the app is sized to. Below that
   the well scrolls sideways instead of shrinking the keys: fourteen keys across
   a 320px phone would be 19px each, which is a mis-tap every time.

   The well takes a definite width rather than inheriting .instrument's
   shrink-to-fit: inside an inline-flex box a percentage width on the SVG has
   nothing to resolve against, and the board collapsed to its floor. */
.keys {
  display: flex;
  width: min(100%, calc(616px + 2 * var(--sp-sm)));
  margin: var(--sp-xs) auto 0;
  padding: var(--sp-sm);
  overflow-x: auto;
  overscroll-behavior-x: contain;
}
.keys .keyboard { width: 100%; min-width: 448px; }

/* ── Tutor ───────────────────────────────────────────────────────────────── */

/* Screen-reader-only text. Used for the ask-again announcement and the input
   label, neither of which should occupy space. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The toggle sits in the .play vocabulary: a small pill, quiet at rest. It is
   genuinely actionable, so amber on hover is right (The Light Means Live Rule),
   and hover presses in like every other control. */
.tutor-toggle {
  position: fixed;
  right: var(--sp-md);
  bottom: var(--sp-md);
  z-index: 20;
  padding: 12px 22px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  color: var(--ink-soft);
  font-size: var(--step--1);
  box-shadow: var(--raised-sm);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.tutor-toggle:hover,
.tutor-toggle:active { box-shadow: var(--pressed-sm); color: var(--glow-text); }

/* One material: the panel is the same stone as the page, lifted off it by
   shadow rather than set apart by colour. It overlays rather than reflowing -
   sliding the question sideways while someone is mid-answer would be worse
   than covering part of the page they chose to cover. */
.tutor {
  position: fixed;
  top: 0; right: 0;
  z-index: 21;
  display: flex;
  flex-direction: column;
  width: min(24rem, 100vw);
  height: 100dvh;
  padding: var(--sp-md);
  background: var(--stone);
  box-shadow: -8px 0 24px var(--shade), inset 1px 0 0 var(--sheen);
  transform: translateX(100%);
  visibility: hidden;
  transition: transform var(--dur-warm) var(--ease-settle), visibility var(--dur-warm);
}
.tutor.open { transform: translateX(0); visibility: visible; }

.tutor-head { flex: 0 0 auto; position: relative; margin-bottom: var(--sp-sm); }
.tutor-head h2 { font-size: var(--step-1); }

.tutor-close {
  position: absolute;
  top: -4px; right: 0;
  width: 34px; height: 34px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  box-shadow: var(--raised-sm);
  color: var(--ink-soft);
  font-size: var(--step-1);
  line-height: 1;
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.tutor-close:hover,
.tutor-close:active { box-shadow: var(--pressed-sm); color: var(--glow-text); }
.tutor-where { color: var(--ink-faint); font-size: var(--step-label); margin-top: 2px; }

.tutor-thread {
  flex: 1 1 auto;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  padding: var(--sp-sm) 0;
}

.tutor-empty { color: var(--ink-faint); font-size: var(--step--1); line-height: 1.6; }

/* No chat bubbles. What the learner says is pressed into the stone; what the
   tutor says sits on a shelf above it. */
.tutor-turn {
  padding: var(--sp-sm) var(--sp-md);
  border-radius: var(--r-well);
  background: var(--stone);
  color: var(--ink-soft);
  font-size: var(--step--1);
  line-height: 1.6;
}
.tutor-user { box-shadow: var(--pressed-xs); color: var(--ink); }
.tutor-assistant { box-shadow: var(--settled); }
.tutor-error { box-shadow: var(--pressed-xs); color: var(--glow-text); }
.tutor-waiting { color: var(--ink-faint); box-shadow: none; padding-left: 0; }

.tutor-form { flex: 0 0 auto; }
.tutor-form textarea {
  width: 100%;
  padding: var(--sp-sm) var(--sp-md);
  border: none;
  border-radius: var(--r-well);
  background: var(--stone);
  box-shadow: var(--pressed-sm);
  color: var(--ink);
  font: inherit;
  font-size: var(--step--1);
  resize: none;
}

.tutor-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-sm);
  margin-top: var(--sp-sm);
}
.tutor-budget { color: var(--ink-faint); font-size: var(--step-label); }

.tutor-send {
  padding: 10px 24px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  color: var(--glow-text);
  font-weight: 600;
  font-size: var(--step--1);
  box-shadow: var(--raised-sm);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.tutor-send:hover:not(:disabled),
.tutor-send:active:not(:disabled) { box-shadow: var(--pressed-sm); color: var(--glow-deep); }
/* Spent, exactly like an answered choice. No amber while waiting - amber means
   this thing is live right now. */
.tutor-send:disabled { cursor: default; color: var(--ink-faint); box-shadow: var(--pressed-xs); }

/* Stands where the question box will be, so it is read rather than dismissed.
   Deliberately plain: no amber, no bloom, no warning triangle. The Light Means
   Live Rule reserves amber for live and earned state, and dressing a caveat up
   as an alert is how people learn to click past caveats. */
.tutor-warning {
  color: var(--ink-soft);
  font-size: var(--step--1);
  line-height: 1.6;
  padding: var(--sp-md) 0;
}
.tutor-warning h3 {
  font-size: var(--step-0);
  color: var(--ink);
  margin-bottom: var(--sp-sm);
}
.tutor-warning p + p { margin-top: var(--sp-sm); }
.tutor-warning .primary { margin-top: var(--sp-md); padding: 12px 28px; }

.tutor-signin { color: var(--ink-soft); font-size: var(--step--1); line-height: 1.6; }
/* Points at the account strip rather than drawing a second Google button - see
   the note at the top of src/assistant.js for why that matters. */
.tutor-signin-where { margin-top: var(--sp-sm); color: var(--ink-faint); font-size: var(--step-label); }

/* The one-time hello. A bubble beside the toggle, never a modal - nothing to
   dismiss before you can carry on. */
.tutor-bubble {
  position: fixed;
  right: var(--sp-md);
  bottom: calc(var(--sp-md) + 56px);
  z-index: 20;
  max-width: 20rem;
  padding: var(--sp-md);
  border-radius: var(--r-well);
  background: var(--stone);
  box-shadow: var(--raised), var(--warm);
  color: var(--ink-soft);
  font-size: var(--step--1);
  line-height: 1.6;
  animation: settle var(--dur-warm) var(--ease-settle) both;
}
.tutor-bubble button {
  margin-top: var(--sp-sm);
  padding: 6px 16px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  box-shadow: var(--raised-sm);
  color: var(--glow-text);
  font-size: var(--step-label);
}
.tutor-bubble button:hover { box-shadow: var(--pressed-sm); }

/* ── Stone & light ───────────────────────────────────────────────────────────
   The material picker. Mirrors the tutor across the page: the tutor answers on
   the right, the material is chosen on the left.

   The swatches are the one place in the app where a surface carries a colour
   that is not the page's, and that is deliberate. A swatch is a SAMPLE - it is
   showing you a material, the way a notehead shows you a pitch - so it is
   content, not chrome. Everything else here obeys the one-material rule. */

.theme-toggle {
  position: fixed;
  left: var(--sp-md);
  bottom: var(--sp-md);
  z-index: 20;
  padding: 12px 22px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  color: var(--ink-soft);
  font-size: var(--step--1);
  box-shadow: var(--raised-sm);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.theme-toggle:hover,
.theme-toggle:active { box-shadow: var(--pressed-sm); color: var(--glow-text); }

.theme {
  position: fixed;
  top: 0; left: 0;
  z-index: 21;
  display: flex;
  flex-direction: column;
  width: min(21rem, 100vw);
  height: 100dvh;
  padding: var(--sp-md);
  background: var(--stone);
  /* The mirror of the tutor's edge: shadow falls to the right, the sheen lip
     catches on the inner edge. */
  box-shadow: 8px 0 24px var(--shade), inset -1px 0 0 var(--sheen);
  transform: translateX(-100%);
  visibility: hidden;
  transition: transform var(--dur-warm) var(--ease-settle), visibility var(--dur-warm);
}
.theme.open { transform: translateX(0); visibility: visible; }

.theme-head { flex: 0 0 auto; position: relative; margin-bottom: var(--sp-md); }
.theme-head h2 { font-size: var(--step-1); }
.theme-lede {
  margin: var(--sp-xs) 0 0;
  color: var(--ink-faint);
  font-size: var(--step-label);
  line-height: 1.55;
  max-width: 30ch;
}

.theme-close {
  position: absolute;
  top: -4px; right: 0;
  width: 34px; height: 34px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  box-shadow: var(--raised-sm);
  color: var(--ink-soft);
  font-size: var(--step-1);
  line-height: 1;
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.theme-close:hover,
.theme-close:active { box-shadow: var(--pressed-sm); color: var(--glow-text); }

.theme-scroll { flex: 1 1 auto; overflow-y: auto; }
.theme-group { margin-bottom: var(--sp-lg); }

.theme-label {
  font-family: var(--font-label);
  font-size: var(--step-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 var(--sp-sm);
}

/* --- The materials -------------------------------------------------------- */

.theme-stones {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-sm);
}
/* The same swatches on the welcome screen, which is a page rather than a narrow
   rail: two fixed columns there would be two very wide slabs.
   7rem rather than 11rem so all six fit on ONE row inside the 44rem cap. That
   reads better than four-then-two, and the row it saves is most of what kept
   the Begin button below the fold on a laptop. */
.welcome-stones {
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  max-width: 44rem;
}

/* Carved from the stone it offers, using that stone's OWN derived sheen and
   shade. What you see is the relief you would get, not a colour chip. */
.stone-swatch {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-xs);
  padding: var(--sp-sm) var(--sp-md);
  border: none;
  border-radius: var(--r-pebble);
  background: var(--sw-stone);
  color: var(--sw-ink);
  text-align: left;
  transition: box-shadow var(--dur-warm) var(--ease-settle);
  box-shadow: -4px -4px 9px var(--sw-sheen), 5px 5px 12px var(--sw-shade);
}
/* Hover presses, like every other control in the app. */
.stone-swatch:hover {
  box-shadow: inset -3px -3px 7px var(--sw-sheen), inset 3px 3px 8px var(--sw-shade);
}
/* Chosen presses AND blooms. Depth alone cannot say "chosen" here, because
   hover uses depth too - the same split that keeps .card.selected apart from
   .card:hover. */
.stone-swatch[aria-checked="true"] {
  box-shadow: inset -3px -3px 7px var(--sw-sheen), inset 3px 3px 8px var(--sw-shade),
              0 0 20px var(--sw-halo);
}

/* The current light, burning in that material. */
/* Held at its LIT size and scaled down at rest, rather than grown on selection.
   The pip is the first child of a flex column, so animating its height moved the
   name below it and made the whole grid row taller - selecting a material
   visibly resized the one beside it. transform leaves layout alone. */
.stone-swatch-pip {
  width: 14px; height: 14px;
  border-radius: var(--r-full);
  background: var(--sw-glow);
  box-shadow: 0 0 9px var(--sw-halo);
  transform: scale(0.64);
  transition: transform var(--dur-warm) var(--ease-settle);
}
/* The chosen material burns wider, the same way the chosen light does. Bloom
   and depth both go quiet on a dark material against a dark panel; a change in
   size does not, and it works without seeing hue at all. */
.stone-swatch[aria-checked="true"] .stone-swatch-pip { transform: scale(1); }
.stone-swatch-name { font-size: var(--step-label); line-height: 1.2; }

/* --- The lights ----------------------------------------------------------- */

.theme-lights { display: flex; gap: var(--sp-sm); }

.light-pip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-xs);
  flex: 1;
  padding: 0;
  border: none;
  background: none;
  color: var(--ink-faint);
  font-size: var(--step-label);
  transition: color var(--dur-warm);
}

/* A pip is a well in the CURRENT stone with the candidate light lit inside it -
   the progress-pip vocabulary exactly, because a lit pip is already how this
   world draws "light lives here". */
.light-pip-well {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border-radius: var(--r-full);
  background: var(--stone);
  box-shadow: var(--pressed-xs);
  transition: box-shadow var(--dur-warm) var(--ease-settle);
}
/* Same treatment as the material pip: lit size, scaled down at rest. */
.light-pip-well::after {
  content: "";
  width: 20px; height: 20px;
  border-radius: var(--r-full);
  background: var(--sw-glow);
  box-shadow: 0 0 10px var(--sw-halo);
  transform: scale(0.65);
  transition: transform var(--dur-warm) var(--ease-settle);
}
.light-pip:hover .light-pip-well { box-shadow: var(--pressed-sm); }
.light-pip[aria-checked="true"] { color: var(--ink); }
.light-pip[aria-checked="true"] .light-pip-well { box-shadow: var(--pressed-sm); }
/* The chosen light burns wider. Never colour alone: the name goes to full ink
   and the flame grows, so the choice reads without seeing hue at all. */
.light-pip[aria-checked="true"] .light-pip-well::after { transform: scale(1); }

/* --- Cut your own --------------------------------------------------------- */

.theme-custom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-sm);
  margin-top: var(--sp-md);
  padding: var(--sp-xs) var(--sp-sm) var(--sp-xs) var(--sp-md);
  border-radius: var(--r-full);
  box-shadow: var(--pressed-xs);
  color: var(--ink-faint);
  font-size: var(--step-label);
  cursor: pointer;
}
/* The input itself is clipped to a pebble, so the ring has to live on the
   label or it would be cut off by the overflow. */
.theme-custom:has(:focus-visible) {
  outline: 2px solid var(--glow);
  outline-offset: 4px;
}
/* A well with the sample set into it, sized to match a light pip. The groove
   matters: the sample is often close to the panel's own colour - it IS the
   current material - and a raised chip in that case reads as nothing at all. */
.theme-custom-chip {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 34px; height: 34px;
  padding: 5px;
  border-radius: var(--r-full);
  box-shadow: var(--pressed-xs);
}
.theme-custom-face {
  position: relative;
  overflow: hidden;
  width: 24px; height: 24px;
  border-radius: var(--r-full);
  box-shadow: var(--raised-sm);
}
/* Oversized and pulled back so the native swatch fills the pebble edge to edge
   instead of sitting in the browser's own bordered box. */
.theme-custom-face input[type="color"] {
  width: 200%; height: 200%;
  margin: -50%;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}
.theme-custom-face input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.theme-custom-face input[type="color"]::-webkit-color-swatch { border: none; }
.theme-custom-face input[type="color"]::-moz-color-swatch { border: none; }

/* --- What the choice actually looks like ---------------------------------- */

/* The payoff of the whole panel, so it is given the room to be that: centred,
   and the staff allowed most of the panel's width rather than sitting at
   15rem in the corner like a footnote. */
.theme-preview {
  margin-bottom: var(--sp-md);
  text-align: center;
}
/* The real staff component, at panel scale - width-led here, because the panel
   is narrow and the preview always draws the same single note. */
/* A single note has a narrow viewBox, so width-led scaling grows the HEIGHT
   fast: at 19rem the clef glyph outweighed the panel's own headings and the
   preview stopped being a preview. Centring is what gives it presence; the
   extra rem is only so it no longer reads as a footnote. */
.theme-preview .staff {
  width: 100%;
  height: auto;
  max-width: 16rem;
  margin: 0 auto var(--sp-sm);
}
.theme-preview-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-md);
  flex-wrap: wrap;
}
.theme-verdict {
  padding: 4px 12px;
  border-radius: var(--r-full);
  font-size: var(--step-label);
  box-shadow: var(--pressed-xs);
}
.theme-right { color: var(--right); }
.theme-wrong { color: var(--wrong); }

.theme-note {
  margin: 0 0 var(--sp-md);
  color: var(--ink-faint);
  font-size: var(--step-label);
  line-height: 1.55;
  min-height: 3.1em; /* holds its place, so the reset button never jumps */
}

.theme-reset {
  padding: 8px 18px;
  border: none;
  border-radius: var(--r-full);
  background: var(--stone);
  color: var(--glow-text);
  font-size: var(--step-label);
  box-shadow: var(--raised-sm);
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.theme-reset:hover { box-shadow: var(--pressed-sm); color: var(--glow-deep); }

@media (max-width: 34rem) {
  :root {
    --raised: -4px -4px 9px var(--sheen), 5px 5px 13px var(--shade);
    --raised-sm: -3px -3px 6px var(--sheen), 3px 3px 8px var(--shade);
    --settled: -3px -3px 7px var(--sheen), 4px 4px 10px var(--shade);
    --pressed: inset -3px -3px 8px var(--sheen), inset 4px 4px 10px var(--shade);
  }
  /* The shorthand resets padding-bottom, so the rail clearance has to be
     restated here or the footer lands back in the pills' band on a phone. */
  #app {
    padding: var(--sp-lg) var(--sp-md);
    padding-bottom: calc(var(--sp-lg) + var(--rail) + env(safe-area-inset-bottom, 0px));
  }
  .slide, .question { padding: var(--sp-md); }
  /* A phone has no room for a full-height staff plus a keyboard to answer on.
     Same ratio as above, so a ledger note still gets its space here. */
  .staff { height: calc(6.5rem * var(--staff-units, 132) / 132); }
  .instrument { padding: var(--sp-sm); }
  .choices { grid-template-columns: 1fr 1fr; gap: var(--sp-sm); }
  .prompt { max-width: none; }
  /* Full-bleed sheet on a phone; 100dvh above already handles the URL bar. */
  .tutor { width: 100vw; }
  .tutor-toggle { bottom: calc(var(--sp-md) + env(safe-area-inset-bottom, 0px)); }
  .tutor-bubble { bottom: calc(var(--sp-md) + 56px + env(safe-area-inset-bottom, 0px)); }
  .theme { width: 100vw; }
  /* Two pills share the bottom edge here, so this one gives up its padding
     rather than let the pair collide at 320px. */
  .theme-toggle {
    bottom: calc(var(--sp-md) + env(safe-area-inset-bottom, 0px));
    left: var(--sp-sm);
    padding: 12px 16px;
  }

  /* 11rem does not fit twice in a 375px screen, so auto-fit collapsed the
     materials to ONE column and stacked all six - the tallest block on the
     welcome screen, and most of the reason Begin sat 841px below the fold.
     8rem fits twice with room to spare. */
  .welcome-stones { grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); }

  /* A thumb should not have to aim on a phone, so the button fills the bar
     rather than sitting centred in it. The bar itself is defined once, above. */
  .welcome-cta .primary { display: block; width: 100%; text-align: center; }
}

/* ── Standalone documents ────────────────────────────────────────────────────
   About, privacy, and the server's error pages. Real HTML files rather than
   SPA routes: they must render with no JavaScript at all, because two of the
   three exist precisely for when something has gone wrong. They share the
   app's stone, tokens and carved primitives (.slide, .lede, .eyebrow, .link),
   so there is one design system and not a second one for the edges. */

.page {
  max-width: 58rem;
  margin: 0 auto;
  padding: var(--sp-xl) var(--sp-md);
  /* No floating rail on these pages, so no bottom clearance is needed - just
     the phone's home indicator. */
  padding-bottom: calc(var(--sp-xl) + env(safe-area-inset-bottom, 0px));
}

.page-head { margin-bottom: var(--sp-lg); }
.page-head .link { display: inline-block; margin-bottom: var(--sp-md); }
.page-head h1 { margin-bottom: var(--sp-sm); }

/* The trail, in place of the back-link it replaced: same job, plus it says
   where you are. The separator is a CSS ::before so a screen reader hears
   "Theory 101, About" rather than a chevron read out as punctuation. */
.crumbs { margin-bottom: var(--sp-md); }
.crumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-xs);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--step--1);
  color: var(--ink-faint);
}
.crumbs li { display: flex; align-items: center; gap: var(--sp-xs); }
.crumbs li + li::before {
  content: '\203A';
  color: var(--ink-faint);
  /* Decoration, not content - it must never reach the accessibility tree. */
  speak: never;
}
.crumbs a { color: var(--ink-soft); text-decoration: none; }
.crumbs a:hover { color: var(--glow-text); }
.crumbs [aria-current='page'] { color: var(--ink-faint); }

/* Questions and answers. A description list because that is literally what it
   is, and because the pairing survives with the stylesheet turned off.

   Scoped `.page .faq` rather than `.faq`, and that is load-bearing: `.page dt`
   further down this file matches at the same specificity and would otherwise
   win on source order, setting every question in the small uppercase label
   treatment written for one-word terms like "Google". A five-word question in
   letter-spaced caps is shouting, not asking. The overrides below are explicit
   for the same reason - the label style has to be undone, not just outranked. */
.page .faq { margin: 0; }
.page .faq dt {
  margin-top: var(--sp-md);
  font-family: var(--font-display);
  font-size: var(--step-0);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
}
.page .faq dt:first-of-type { margin-top: var(--sp-sm); }
.page .faq dd {
  margin: var(--sp-xs) 0 0;
  color: var(--ink-soft);
  max-width: 68ch;
}
/* .link and .primary were written for <button>, which has no underline to
   suppress. On these pages they sit on real anchors, so they need it said. */
.page a.link, .page a.primary { text-decoration: none; }
/* A standalone page has the full column to work with, so the lede is not held
   to the 38ch the dashboard uses to keep it beside other things. */
.page .lede { max-width: 54ch; }

/* The section heading has to outrank the body text it sits on top of, and at
   --step-0 it did not - it read as a bold first line rather than a heading. */
.page .slide h3 {
  font-family: var(--font-display);
  font-size: var(--step-1);
  color: var(--ink);
  margin: var(--sp-lg) 0 var(--sp-sm);
  letter-spacing: 0.005em;
  text-shadow: var(--incised);
}
.page .slide h3:first-child { margin-top: 0; }
.page .slide p:last-child { margin-bottom: 0; }
.page .slide strong { color: var(--ink); font-weight: 600; }
/* A filename or header name, pressed into the stone rather than boxed on top of
   it. Deliberately NOT monospace: the design system has three faces and a
   fourth would be a brand change made for two filename mentions. The label face
   plus the pressed well already reads as "a literal token". */
.page code {
  font-family: var(--font-label);
  font-size: 0.92em;
  padding: 1px 5px;
  border-radius: var(--r-pebble);
  background: var(--stone);
  box-shadow: var(--pressed-xs);
  color: var(--ink);
}
.page .slide a { color: var(--glow-text); text-decoration: underline; text-underline-offset: 2px; }

/* A definition row - "what we store / why" - without a table's rigidity. */
.page dl { margin: 0; }
.page dt {
  font-family: var(--font-label);
  font-size: var(--step-label);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-top: var(--sp-md);
}
.page dt:first-child { margin-top: 0; }
.page dd { margin: var(--sp-xs) 0 0; }

.page-foot {
  margin-top: var(--sp-xl);
  padding-top: var(--sp-md);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-md);
  align-items: center;
  color: var(--ink-faint);
  font-size: var(--step--1);
}
.page-foot a { color: var(--ink-faint); text-decoration: none; }
.page-foot a:hover { color: var(--glow-text); }
.page-foot .spacer { flex: 1 1 auto; }

/* The error pages. A signpost, not an alarm - so the status is an eyebrow, the
   way every other screen in the app introduces itself (SKILL DRILL, ABOUT), and
   the SENTENCE is the hero. An oversized numeral was the first attempt and it
   was wrong twice over: it needed a type size off the documented ramp, and it
   made the quietest moment in the app the loudest thing in it. */
.page.error { display: grid; min-height: 100vh; align-content: center; text-align: center; }
.page.error .eyebrow { margin-bottom: var(--sp-sm); }
.page.error .lede { margin: 0 auto var(--sp-lg); max-width: 44ch; }
.page.error .page-foot { justify-content: center; border-top: none; margin-top: var(--sp-xl); }

/* The SPA footer link row, so the legal pages are reachable from the app. */
.app-foot {
  margin-top: var(--sp-xl);
  padding-top: var(--sp-md);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-md);
  justify-content: center;
  color: var(--ink-faint);
  font-size: var(--step--1);
}
.app-foot a { color: var(--ink-faint); text-decoration: none; }
.app-foot a:hover { color: var(--glow-text); }

@media (max-width: 640px) {
  .page { padding-top: var(--sp-lg); }
  .page-foot { gap: var(--sp-sm); }
  .page-foot .spacer { flex-basis: 100%; }
}
