/* 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; }

/* A word inside a sentence that opens the thing it names.
   It replaced a <strong>, so it has to sit in the line without moving it: font
   and size inherited rather than set, no padding, and baseline alignment - a
   button defaults to `inline-block` and `middle`, which lifts the word off the
   baseline of the prose around it.
   The underline is the affordance. Bold alone was already there and meant
   nothing clickable, so weight cannot be the signal that changed. */
.inline-link {
  display: inline;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-underline-offset: 2px;
  transition: color var(--dur-warm) var(--ease-settle),
              text-decoration-color var(--dur-warm) var(--ease-settle);
}
.inline-link:hover {
  color: var(--glow-text);
  text-decoration-color: var(--glow-edge);
}
/* No :focus-visible rule. The global one at the top of this file already rings
   every focusable thing in the same amber at the same offset, on the documented
   radius - a second version here was a different colour and an off-scale 3px,
   which is a focus ring that looks almost but not quite like every other. */

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

/* --sp-lg, not --sp-xl. The welcome screen asks four 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); }

/* Every line on this screen runs to the same right edge.

   The measures are lifted rather than widened. .lede stopped at 38ch, .sub at
   62ch, .lede-more and .notice at 54ch, and .welcome-head .lede had a 46ch rule
   of its own - five different stopping points stacked above cards and swatches
   that run the full width, which is the ragged right this screen has been
   fixing group by group. Prose was the last of it.

   The tradeoff is real and taken deliberately: at 58rem the pitch runs a little
   past the 65-75 character band that is comfortable for long reading. This is a
   short form, not a long read, and one edge everything shares is worth more
   here than a measure only the paragraphs obey.

   Scoped to .welcome, so the dashboard and the lesson screens keep theirs. */
.welcome .lede,
.welcome .lede-more,
.welcome .sub,
.welcome .notice { max-width: none; }

/* 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;
  /* Opacity alone left both in the tab order, so a keyboard user landed on two
     invisible buttons. */
  visibility: hidden;
}

@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);
}
/* The dashboard draws no nav at all now that it is home - screenNav() returns
   an empty string rather than an empty element, so there is no rule to write
   for it and no stray margin above the eyebrow. */
/* 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 > /* The staff mnemonics: the letters, then the sentence that carries them.

   A grid rather than a bulleted list, so the letter groups line up in a column
   down the left and can be compared at a glance - E G B D F above F A C E is
   the whole point, and a ragged left edge hides it. No bullets for the same
   reason: the letters ARE the marker.

   The letters take the label face and tabular figures' spacing, because they are
   being read as a sequence to check against a staff rather than as a word.
   The sentence stays body text and quiet: it is a hook for the letters, not a
   claim of its own. */
.mnemonics {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-xs) var(--sp-md);
  margin: var(--sp-md) 0;
  padding: 0;
  list-style: none;
}
.mnemonics li { display: contents; }
.mnemonics strong {
  font-family: var(--font-label);
  letter-spacing: 0.08em;
  white-space: nowrap;
  color: var(--ink);
}
.mnemonics span { color: var(--ink-soft); }
.mnemonics em { font-style: normal; color: var(--ink); }

/* The pointer into a diagram: an arrow, and a note in the margin.

   Ink, not amber. The Light Means Live Rule reserves --glow for state the
   learner is IN or has earned, and an annotation is neither - it is part of the
   picture, and a picture that glows would outrank the question next to it.
   --ink-faint on the line clears the 3:1 non-text floor; --ink-soft on the
   words clears AA as text, which is what they are.

   No dashes, no double-drawn wobble. The hand is already in the curve of the
   path and the tilt of the label (see handPointer in ui.js); adding texture on
   top of that is the point where "someone annotated this" turns into a costume.
   Round caps and joins, because a pencil has no corners. */
.hand-line {
  fill: none;
  stroke: var(--ink-faint);
  /* Thin. At 1.5 the squiggle and its head merged into one tapering brush mark
     that read as a different hand from the label it belongs to. */
  stroke-width: 0.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The head is filled rather than stroked, so it stays a crisp small triangle
   while the line stays a pen line. Two barbs off the same stroke could only be
   as heavy as the stroke, and a head that thin does not read as an arrow. */
.hand-arrow { fill: var(--ink-faint); stroke: none; }
.hand-note {
  fill: var(--ink-soft);
  font-family: var(--font-hand);
}

/* The glossary: twelve marks, each with the word for it underneath.

   A grid rather than the column of full-width figures every other slide draws,
   because this page is a chart you scan for the one symbol you have just met,
   not prose you read top to bottom. Twelve stacked staves is a page you scroll
   past; three or four to a row is a page you FIND something on.

   auto-fit, so the count follows the width instead of being declared. The floor
   is what a staff with a clef, a key signature and two noteheads needs before
   its lines start to crowd - the widest entry in the table sets it for all of
   them, or the row would be ragged in height as well as width.

   Each figure keeps its own --pair-h, so the staves here are cut to exactly the
   size they are everywhere else in the app. A glossary that drew smaller staves
   than the lesson beside it would be teaching a second picture of the same
   thing. */
.symbol-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--sp-md);
  margin: var(--sp-md) 0;
}
/* The figure already carries a vertical margin for the column it usually sits
   in; inside the grid the gap is doing that job and the margin only loosens the
   rows against the columns. */
.symbol-grid .diagram { margin: 0; }
/* The caption IS the entry here - it is the word being taught, not a sentence
   explaining a picture - so it takes the ink and weight the rest of the app
   gives a label rather than the quieter treatment a figcaption gets. */
.symbol-grid figcaption {
  color: var(--ink);
  font-family: var(--font-label);
  font-weight: 600;
  text-align: center;
}

.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); }
/* The slot a note value sits in. It held a Unicode character and now holds a
   drawn one, so the font stack is gone and what is left is the box: a fixed
   width, so five values of very different widths still line their names up in a
   column, and a colour, which the shape inherits through currentColor. */
.note-glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.6em;
  flex: none;
  color: var(--ink);
}
/* Drawn, not typed - the Musical Symbols block these used to come from is in
   almost no text font, so on a machine without one the chart was blank.
   Sized in em so it rides the prose it sits beside, and filled with
   currentColor so it takes that prose's ink rather than carrying a token of its
   own. Ink and not amber: --glow means live or earned, and a chart of note
   lengths is neither. */
.note-value {
  height: 3.1em;
  width: auto;
  fill: currentColor;
  overflow: visible;
}
/* A hollow head is stroked rather than punched through with a second
   page-coloured ellipse, so the same glyph works on whichever stone the learner
   picked and in any prose it is dropped into. */
.note-value .hollow { fill: none; stroke: currentColor; }
/* 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));
  /* Every tile the same height, not every tile in a ROW the same height. A
     two-line title - "Sharps, Flats & Enharmonics" is the one that does it -
     made its own row taller than the rows under it, so a grid of identical
     cards arrived looking like cards of two different kinds. 1fr here sizes
     every row to the tallest one. */
  grid-auto-rows: 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 {
  /* The height a picture's CONTENT is cut to - a keybed, or a block of five
     staff lines. Declared here on the figure rather than on .diagram-pair-row
     below, because a staff standing alone has to come out the same size as one
     with a keyboard beside it, and two declarations of 8.5rem is how those two
     drift apart. .diagram-pair-row is always inside a .diagram (see ui.js), so
     it inherits this. */
  --pair-h: 8.5rem;
  margin: var(--sp-md) 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-xs);
}
.diagram .keyboard { max-width: 100%; height: auto; }

/* A staff with no keyboard beside it, cut to the same line height as one that
   has - which is the whole of this rule's reason to exist.

   It used to take height:auto along with the keyboard above. An <svg> carrying
   a viewBox and no width or height has a ratio but no intrinsic size, so
   height:auto let it lay itself out from the default replaced-element width and
   grow to whatever height the ratio asked for - around 400px of box for a
   single note, where the same staff beside a keyboard is 136px of lines. The
   sparser the bar, the taller it drew, because a narrow viewBox and a fixed
   ratio can only buy width back with height.

   Same formula as .diagram-pair .staff, and for the same reason: only 60 of the
   132 box units are lines, the rest is ledger room staffSvg reserves whether or
   not a note uses it, so scaling by the BOX would size the picture off its
   padding. No translate here though - --staff-mid re-centres a staff onto a
   keyboard's centre line, and there is no keyboard to centre on.

   :not(.diagram-pair) because the pair figure carries BOTH classes, which makes
   a plain `.diagram .staff` tie with `.diagram-pair .staff` on specificity and
   leaves the winner to source order. */
.diagram:not(.diagram-pair) .staff {
  height: calc(var(--pair-h) * var(--staff-units, 132) / var(--staff-lines, 60));
  width: auto;
  max-width: 100%;
}
.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 {
  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. */
/* The box a callout is drawn in, made inert.

   This was a real bug, and it is the reason a legend dot would not open its own
   label: each box is 34 units tall and the nine dots sit 7.5 apart, so EIGHT of
   the nine had a neighbour' box lying across their centre. The boxes are separate
   <g>s painted in order, so the later one covered the earlier one's circles and
   took the hover with it - pointing at a dot opened the label of a dot two rows
   away, and pointing beside the dot worked because there you were inside the
   right box after all.

   pointer-events was already off on the .note-callout-slot INSIDE this, which
   is not the same thing: a foreignObject is hit-tested as its own rectangle,
   and what its HTML content declares does not release it. The element that owns
   the rectangle is the element that has to give it up.

   A label is not a target. Nothing about it should be reachable by a pointer,
   including the empty space it reserves. */
.note-callout-fo { pointer-events: none; }

.note-callout-slot {
  height: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  pointer-events: none;
}
/* Translucent, and frosted rather than merely faint.
   A solid slab of stone dropped on a staff cuts the lines it is explaining
   clean in half, which reads as damage to the picture rather than as something
   floating over it. Letting the lines through keeps the bubble in the same
   world as the notation - but transparency alone would leave the letter sitting
   on top of them, so the backdrop blur softens what shows through and the text
   keeps its contrast. Both declarations are needed: the blur is what buys the
   legibility that the alpha spends.
   Two `background` lines on purpose. A browser without color-mix drops the
   second and keeps the opaque stone, which is the old bubble exactly - never a
   letter floating on bare staff lines. Same for border-color below. */
.note-callout {
  opacity: 0;
  /* Small and low, and it grows out of the dot rather than fading in place. The
     origin is the bottom edge, which is the side the dot is on, so the bubble
     unfolds upward from the mark it belongs to instead of expanding about its
     own middle and appearing to arrive from nowhere. */
  transform: translateY(5px) scale(0.88);
  transform-origin: 50% 100%;
  transition: opacity var(--dur-pop) var(--ease-settle),
              transform var(--dur-pop) var(--ease-settle);
  text-align: center;
  background: var(--stone);
  background: color-mix(in srgb, var(--stone) 86%, transparent);
  -webkit-backdrop-filter: blur(7px) saturate(1.08);
  backdrop-filter: blur(7px) saturate(1.08);
  color: var(--ink);
  font: 500 12px var(--font-body);
  line-height: 1.35;
  padding: 8px 10px;
  border-radius: var(--r-pebble);
  /* No outline, and no drop shadow. Both were here, and between them they made
     a UI chip pasted onto a drawing: a --rule ring is the same value the STAFF
     LINES are drawn in, so the bubble edge read as heavily as the notation it
     was annotating, and --raised-sm lifted it off the page as a separate
     object. What a reader wants is the opposite - the page clearing a space
     for a word, not a card arriving on top of it.
     The edge is replaced by a bleed: two rings of the page's OWN stone, thin
     and see-through, so the bubble stops being a rectangle and becomes a patch
     of ground that fades out. Stone on stone is invisible where it lands on
     bare page and only shows where it covers something, which is exactly the
     job. It is also why the old clipping note no longer applies - the
     foreignObject still crops the far edge of this, and cropping stone against
     stone cannot be seen.
     The fill is denser than the 78% it was, because the outline is no longer
     there to hold the shape and the blur alone was carrying the contrast. */
  box-shadow:
    0 0 0 5px color-mix(in srgb, var(--stone) 52%, transparent),
    0 0 14px 9px color-mix(in srgb, var(--stone) 34%, transparent);
}

/* The side callouts hold ONE letter - a staff-legend position, or a notehead
   naming itself - where the keyboard's hold whole sentences. One letter does
   not need a container at all: it can just BE the letter, written on the staff
   the way anybody annotating a score writes one.
   So there is no bubble here. It had one - a lozenge with a ring and a drop
   shadow - and beside an amber notehead it read as a badge stuck to the
   picture, two objects fighting over the same few pixels rather than a name
   for the note under it.
   What clears the staff lines instead is a halo of the page's own stone,
   thrown off the glyph itself. It has no edge anywhere, because it is the
   letter that is drawn and not a box around it, so there is nothing to align,
   nothing to collide with the notehead, and nothing for the foreignObject to
   crop. --incised leads the list so the app's carved lettering paints crisply
   over the soft part, which is what seats the letter IN the page rather than
   on it - the same treatment every title in this app already gets.
   Scoped to .side rather than applied to .note-callout, because the same rule
   on a keyboard callout would set a two-line sentence in 14px display type
   with no ground under it at all. */
.note-callout-slot.side .note-callout {
  font: 600 14px/1 var(--font-display);
  letter-spacing: 0.01em;
  color: var(--ink);
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: none;
  text-shadow:
    var(--incised),
    0 0 3px var(--stone), 0 0 3px var(--stone),
    0 0 7px var(--stone), 0 0 11px var(--stone);
  /* It opens to the RIGHT of its dot, so it grows from its left edge. Sharing
     the bubble's bottom origin would have the letter rising up past the dot it
     names on its way in, which points away from the thing being pointed at. */
  transform: translateX(-5px) scale(0.88);
  transform-origin: 0% 50%;
  /* Padding is now clearance for the halo rather than the walls of a lozenge.
     The foreignObject hosting this is 26 units tall and the glyph is 14 of
     them, so 5 either side spends the box exactly - which the border used to
     take, and no longer does. */
  padding: 5px 2px;
}
.note-callout-target { cursor: help; }
/* Through .note-callout-slot rather than straight at .note-callout, purely for
   specificity. The resting transform is set twice - once on .note-callout and
   again, differently, on .note-callout-slot.side .note-callout - and that second
   one ties this rule at three classes. It only wins today because it happens to
   be written lower in the file, which is precisely how the welcome screen's
   light strip silently reverted to the wrong layout. The extra class in the
   middle says what is meant instead of relying on where it sits. */
.note-callout-target:hover .note-callout-slot .note-callout {
  opacity: 1;
  transform: none;
}

/* 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 figure that
     now anchors both layouts - .diagram-pair .staff and .diagram:not(...) .staff
     both outrank that rule, so the height has to come down here or a diagram
     would keep its desktop size on a phone. On .diagram rather than
     .diagram-pair-row so a solo staff shrinks too; the row inherits it, so a
     pair's two pictures still shrink together and stay level with each other. */
  .diagram { --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. */
/* Sized the way .staff and .keyboard are, and for the same reason: an SVG with
   only a viewBox has no width to shrink-wrap to, so inside .instrument - which
   is inline-flex - height:auto collapsed it to nothing. It went unnoticed
   because until Half Steps & Whole Steps asked for one, the strip was built but
   never drawn anywhere. --strip-units carries its own box so callout or bracket
   headroom lengthens the picture instead of squashing the cells. */
.chromatic { max-width: 100%; height: calc(4rem * var(--strip-units, 64) / 64); }
.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(3.4rem, 1fr));
  gap: var(--sp-sm);
  max-width: 34rem;
  overflow-x: auto;
  /* Without these the scroll above never happened: a grid inside the
     inline-flex .instrument is sized to its content, so the board pushed
     through the card and the page itself scrolled sideways on a 360px phone,
     taking the B column and the Dashboard link off screen. */
  min-width: 0;
}
.instrument.named { width: min(32rem, 100%); }
.instrument.named .pitch-map { width: 100%; }
.pitch-tip {
  margin-top: var(--sp-xs);
  color: var(--ink-faint);
  font-size: var(--step--1);
}

/* One tall pad per letter: sharp on top, natural in the middle, flat below -
   the way the accidental moves the pitch. The pad is a single raised pebble
   and its three zones are pressed into it, so a column reads as ONE letter
   with three spellings rather than three unrelated buttons stacked up.
   The natural gets the most room because it is the most common answer.
   touch-action: none so a vertical flick is a swipe, not a page scroll. */
.pitch-pad {
  display: grid;
  grid-template-rows: 2.6rem 4rem 2.6rem;
  border-radius: var(--r-pebble);
  background: var(--stone);
  box-shadow: var(--raised-sm);
  overflow: hidden;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.pitch {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--ink);
  font-size: var(--step-0);
  cursor: pointer;
  transition: box-shadow var(--dur-warm) var(--ease-settle), color var(--dur-warm);
}
.pitch-natural { font-size: var(--step-2, 1.5rem); font-weight: 500; }
.pitch-sharp, .pitch-flat { color: var(--ink-soft); font-size: var(--step-1, 1.15rem); }
.pitch-sharp { border-bottom: 1px solid var(--shade); }
.pitch-flat { border-top: 1px solid var(--shade); }
.pitch:hover { box-shadow: var(--pressed-sm); color: var(--ink); }
.pitch:focus-visible { outline: 2px solid var(--glow); outline-offset: -3px; }
/* 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. */

/* One octave is 330px wide, so two must be allowed 660 or the keys halve in
   width and the whole board halves in height with them. --kb-octaves comes
   off the SVG, so the cap follows whatever the caller asked to draw. */
.keyboard { width: min(100%, calc(330px * var(--kb-octaves, 1))); 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; }
/* The clef is drawn now, not typed.
   It was a character in the Musical Symbols plane, which almost no text font
   carries: it rendered on a machine shipping Segoe UI Symbol and came out blank
   on one without it, and a staff with no clef means nothing at all.
   Stroked rather than filled, matching how the staff lines and ledger lines
   beside it are already drawn, and the stroke width is carried on the path in
   staff-space units so it scales with the staff instead of thinning as the
   diagram grows. Round caps and joins, because a nib has no corners. */
.clef {
  fill: none;
  stroke: var(--ink);
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The two dots of the F clef, which are what actually names the line. Filled,
   not stroked - a stroked circle at this size is a ring. */
.clef .clef-dot { fill: var(--ink); stroke: none; }
/* 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-music); }
.notehead { fill: var(--glow); }
/* The note's own name, written in the head.
   It was a bubble that opened beside the note on hover, and it is a letter in
   the middle of the note now: one object instead of two, always there instead
   of waiting for a pointer that a phone does not have.
   Painted in the STONE, not in ink. The head is filled with --glow, and the
   page's own ground is the one colour guaranteed to be legible on it in both
   renditions - the same 5.2:1 daylight and 8.4:1 blue-hour that --glow already
   clears against the page, read the other way round.
   The display face, because the letter is notation here rather than interface,
   and 10px against a head 20 units wide is the largest that still leaves the
   ellipse reading as a notehead rather than as a bubble with a letter in it. */
.note-letter {
  fill: var(--stone);
  font: 600 10px var(--font-display);
  text-anchor: middle;
  dominant-baseline: central;
  /* The head underneath owns any pointer that arrives; a letter that swallowed
     hovers would break the drills that make noteheads targets. */
  pointer-events: none;
}
.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; }
/* Answered. The keys stop reacting but the well keeps its pointer events, so a
   two-octave board can still be scrolled sideways on a phone. */
.spent .key, .spent .pitch { pointer-events: none; cursor: default; }
.spent .pitch:not(.selected) { color: var(--ink-faint); box-shadow: var(--pressed-xs); }
.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); }

/* ── The settings panel ──────────────────────────────────────────────────────
   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;
  /* The fallback is the width the panel always had; theme-panel.js writes
     --theme-w from the remembered value once it mounts, so the panel is never
     briefly 21rem on the way to being what you left it. */
  width: min(var(--theme-w, 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; }

/* The drag edge.

   Straddles the panel's own edge rather than sitting inside it, so the target
   is 10px of forgiveness either side of a 1px line. It is invisible until
   wanted: a permanent handle on a panel that is mostly not being resized is
   furniture, and this app spends its shadows on things you can press.

   No width transition anywhere on .theme, so a drag needs no class to defeat
   one - .resizing is here for the text selection, which otherwise sweeps the
   whole panel as the pointer crosses it. */
.theme-grip {
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: 10px;
  transform: translateX(50%);
  cursor: ew-resize;
  /* Without this a touch drag scrolls the panel instead of resizing it. */
  touch-action: none;
  background: none;
  border: none;
  padding: 0;
}
.theme-grip::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 2px; height: 2.5rem;
  border-radius: var(--r-full);
  background: var(--ink-faint);
  transform: translate(-50%, -50%);
  opacity: 0;
  transition: opacity var(--dur-warm) var(--ease-settle);
}
.theme-grip:hover::after,
.theme-grip:focus-visible::after,
.theme.resizing .theme-grip::after { opacity: 0.55; }
.theme.resizing { user-select: none; }

.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 -------------------------------------------------------- */

/* auto-fit rather than the two fixed columns this had. Two was right when the
   panel was one width; now that the edge can be dragged, a fixed count would
   mean widening only ever produced wider slabs. Columns appear instead, which
   is the whole payoff for dragging.

   5rem, down from the 9rem that was here to give every swatch's note a readable
   line. The notes are gone, so the floor is now only what a pip and the longest
   name need, and the panel packs three across at its default width where it
   packed two. Drag the edge far enough and the group reaches one line on its
   own, which is the shape the welcome screen below draws outright. */
.theme-stones {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(5rem, 1fr));
  gap: var(--sp-sm);
}

/* One line, on the same left and right edges as the proficiency and diagram
   cards above.

   Three columns stood here, which was right for six materials and became wrong
   the moment Custom made them seven. Seven is prime: every column count below
   seven leaves an orphan, and three left Custom alone on a row of its own. That
   orphan is what reads as out of line with everything else - nobody counts
   columns, they see a row that stops early.

   Seven cannot break. It also costs LESS height than the three rows it
   replaces, even with each swatch now a portrait tile, because one tall row is
   shorter than three short ones and the two gaps between them.

   Below 48rem seven names will not fit across, so the group goes back to
   packing whatever fits and the tile gives up its extra height. */
.welcome-stones {
  grid-template-columns: repeat(7, minmax(0, 1fr));
}
/* Tall enough to read as a piece of the material rather than a chip of it.
   Only on this screen: the panel is a 21rem rail, where the same height would
   turn three rows of swatches into most of a scroll. */
.welcome-stones .stone-swatch { min-height: 8.5rem; }

@media (max-width: 48rem) {
  .welcome-stones { grid-template-columns: repeat(auto-fit, minmax(5rem, 1fr)); }
  .welcome-stones .stone-swatch { min-height: 0; }
}

/* There was a .welcome-lights override here, and both it and the class it hooked
   are gone. The strip and the panel's row are one shape now that neither prints
   a per-pip sentence, so a second rule could only repeat the first - and the
   last version of it was a real bug, losing to .theme-lights on source order
   and silently reverting the strip to a grid that wrapped the fourth pip. */
/* The band that says what this screen is. The pitch above it is the product's
   first impression; this is the moment the page turns into a form, and it was
   previously unannounced - three questions simply began. */
.setup-band {
  margin: var(--sp-xl) 0 var(--sp-lg);
  padding-top: var(--sp-lg);
  border-top: 1px solid var(--shade);
}
.setup-band h2 { margin-bottom: var(--sp-xs); }

/* 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;
  /* Centred, and the pip and the name sit together in the middle of the tile
     rather than in its top-left corner. On a tall tile a corner-pinned label
     reads as a caption stuck to a rectangle; centred, the rectangle IS the
     material and the label belongs to it. It also puts the pip directly above
     the light pips in the row below, which are centred already. */
  align-items: center;
  justify-content: center;
  gap: var(--sp-xs);
  /* --sp-md of side padding was affordable at 9rem and is not at 5rem: it left
     less room for "Porcelain" than the word needs. */
  padding: var(--sp-sm) var(--sp-xs);
  border: none;
  border-radius: var(--r-pebble);
  background: var(--sw-stone);
  color: var(--sw-ink);
  text-align: center;
  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; }
/* Opacity, not a colour token. The swatch paints itself in the material it
   offers and inherits that stone's OWN derived ink, so any fixed faint colour
   here would be the panel's ink on somebody else's stone - unreadable on half
   of them. Fading the inherited ink works on all six by construction. */

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

/* A strip of centred pips, the same on both screens.

   It was a grid of labelled rows - well on the left, name over a sentence on
   the right - because each light carried a note explaining what choosing it
   did. The notes are gone, so the two-row grid was holding an empty second row
   and reserving 13rem of width for a sentence that is no longer written. One
   shape now, which is also what removed the welcome screen's override of it. */
.theme-lights {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm);
}
.light-pip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-xs);
  flex: 1 1 4rem;
  text-align: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--ink-faint);
  font-size: var(--step-label);
  transition: color var(--dur-warm);
}
.light-pip-name { line-height: 1.2; }

/* 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 --------------------------------------------------------- */

/* The custom colour is the last item in the group rather than a wide row under
   it: same swatch, same pip, same grid cell. It was a different shape and a
   different width, which said it was a different KIND of thing - when picking
   it does exactly what picking any other swatch does. */

/* The native picker, kept operable but out of sight.

   It is laid over the whole swatch rather than shrunk to a clipped pixel. Not
   cosmetic: WebKit presents the colour picker as a popover ANCHORED TO THIS
   ELEMENT'S BOX, so a 1px box clipped to nothing gave it nowhere to appear and
   a tap on iOS did nothing at all. Desktop Chrome and Firefox open a picker
   from a hidden input happily, which is why this survived so long unnoticed.

   Still invisible (opacity, not display:none or visibility:hidden - either of
   those stops .click() opening it), and still pointer-events:none, so the
   button around it keeps receiving the tap and decides what it means. */
.swatch-input {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  -webkit-appearance: none;
  appearance: none;
  background: none;
  pointer-events: none;
}
.swatch-custom { position: relative; }

/* Greyed until a colour is actually in play. The pip shows the ground rather
   than a colour, so the swatch reads as an empty slot waiting to be filled
   instead of claiming some particular shade is already chosen. */
.swatch-custom.is-empty .stone-swatch-pip,
.swatch-custom.is-empty .light-pip-well {
  background: var(--stone);
  box-shadow: var(--pressed-xs);
  opacity: 0.55;
}
.swatch-custom.is-empty { color: var(--ink-faint); }
.swatch-custom:hover.is-empty { opacity: 1; }

.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); }
  /* Same treatment for the strip: 408 units wide against 64 tall, so its width
     is what runs out first on a phone. */
  .chromatic { height: calc(3rem * var(--strip-units, 64) / 64); }
  .instrument { padding: var(--sp-sm); }
  /* Seven 2.6rem columns do not fit inside a phone's card, so the board had to
     scroll to reach B. Narrower here so all seven fit down to 360px; below that
     the board scrolls inside its own well. Still about 44px tall per cell. */
  .pitch-map { grid-template-columns: repeat(7, minmax(2.2rem, 1fr)); gap: 5px; }
  .pitch-pad { grid-template-rows: 2.75rem 4.25rem 2.75rem; }
  .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;
  }

  /* The materials had a phone-only floor of 8rem here. The 48rem fallback above
     already hands this width the same auto-fit packing at a 5rem floor, which
     draws three across on a 375px screen rather than the two 8rem allowed. */

  /* 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%; }
}

/* ── Practice control ────────────────────────────────────────────────────── */

/* Under the question, not in the settings panel: the clef you want to be asked
   in is a thing you change part way through a practice run and change back, and
   a preference that lives two screens away may as well not exist at that
   moment. Centred and quiet, because it is an aside to the question above it -
   the row reads as an offer rather than as a step. */
.practice-control {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-xs);
  margin-top: var(--sp-lg);
}
.practice-control .chips { margin-top: 0; justify-content: center; }
.practice-control-label {
  color: var(--ink-faint, var(--ink-soft));
  font-size: var(--step--1);
  letter-spacing: 0.04em;
}

/* ── Terms of art ────────────────────────────────────────────────────────── */

/* A word that carries its own definition. Underlined rather than coloured: the
   prose already uses <strong> and <em> for emphasis, and a third colour would
   compete with them for the same attention. A dotted rule is the printed
   convention for "there is more here", and it is the only one that reads as an
   offer rather than a link out. */
.term { position: relative; display: inline-block; }
.term-word {
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  cursor: help;
  text-decoration: underline dotted currentColor;
  text-underline-offset: 0.22em;
}
.term-word:hover { color: var(--ink); }

/* Hidden by visibility, not display, so the box is measured and positioned
   before it is ever shown - a popout that lays itself out at the moment it
   appears jumps on the way in. */
.term-pop {
  position: absolute;
  bottom: calc(100% + 0.55rem);
  left: 50%;
  z-index: 20;
  width: max-content;
  max-width: min(30ch, 78vw);
  transform: translateX(-50%);
  padding: var(--sp-sm) var(--sp-md);
  border-radius: var(--r-slab);
  background: var(--stone);
  box-shadow: var(--raised-sm);
  color: var(--ink-soft);
  font-size: var(--step--1);
  font-weight: 400;
  line-height: 1.5;
  text-align: left;
  text-decoration: none;
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--dur-warm) var(--ease-settle), visibility var(--dur-warm);
}
.term-word:hover + .term-pop,
.term-word:focus-visible + .term-pop { visibility: visible; opacity: 1; }

/* Near the edges of a narrow screen a centred bubble hangs off the page, and
   there is no script here to measure it. It did not only look wrong: hidden by
   visibility it still takes up layout, so a term near the right margin widened
   the whole lesson to ~436px on a 390px phone, and the phone zoomed out to fit.
   So on a phone the definition is a strip fixed above the rail instead - a
   fixed box never widens the page, and full width reads better at this size. */
@media (max-width: 34rem) {
  .term-pop {
    position: fixed;
    left: var(--sp-md);
    right: var(--sp-md);
    bottom: calc(var(--rail) + env(safe-area-inset-bottom, 0px));
    width: auto;
    max-width: none;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .term-pop { transition: none; }
}

/* ── Lesson flip ─────────────────────────────────────────────────────────── */

/* space-between with a placeholder span at either end, so the first lesson's
   "next" sits right and the last lesson's "prev" sits left - a lone button
   centred under the activity would read as the one button the screen has. */
.lesson-flip {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--sp-md);
  margin-top: var(--sp-lg);
}
/* Each half of the row, so two long titles wrap inside their own column rather
   than pushing each other off a phone. */
.lesson-flip .link {
  max-width: 45%;
  margin: 0;
  text-align: left;
}
.lesson-flip .link[data-flip="next"] { text-align: right; }
