/* ═══════════════════════════════════════════════════════════════════════════
   PineTone, paper / ink / sun design system (v3)
   One accent hue (sun yellow). All borders and shadows are hard ink offsets,
   with no blur anywhere. Numerals: Bricolage Grotesque 800. UI: Familjen
   Grotesk.
   Labels/data: Sometype Mono, uppercase, tracked.
   ═════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── One hue, four lightnesses ───────────────────────────────────────────
     Two earlier attempts failed the same way: they put several saturated
     colours on the instrument at once and then tried to find a lucky pair.
     Mustard against olive read as olive-and-mustard; terracotta against sage
     read as avocado-and-burnt-orange, and its dark roots read as stop signs.

     This is structural rather than another guess. The whole instrument is one
     green at four lightnesses, so nothing can clash because there is only one
     hue, and lightness stays the channel every colour-vision deficiency
     preserves.

     The resting grid is deliberately FADED: every face is light and
     low-chroma. That is what gives the loud states somewhere to stand out
     from. Chroma is spent on the things that carry meaning, which is a key
     you are pressing and the shape of a chord you asked to see, rather than
     on the field they sit in. */
  --ink:        #16241B;  /* deep forest, near black */
  --sun:        #9FCEB1;  /* the root note at rest, the darkest resting face */
  --sun-lit:    #2E8B57;  /* the root when it sounds */
  --pressed:    #1F7A47;  /* a key under your finger */
  --on-loud:    #F4FBF5;  /* labels on any loud face */
  --paper:      #F8F6F0;  /* the ground, a sheet on a workbench */
  --paper-warm: #F5F0E6;  /* side panels */
  --cream:      #F1E8D9;  /* cards, the rail plank */
  --desk:       #EAF2EC;  /* out of scale */
  --key-birch:  #FAFDF7;  /* in-scale naturals (matches KEY_WHITE) */
  --key-sharp:  #CCE2D3;  /* in-scale sharps and flats (matches KEY_SHARP) */
  --ring:       #0E3B22;  /* chord and cue overlays, loud on purpose */
  --gold-text:  #1F6B41;
  --gold-dim:   #14512F;
  --body-text:  #3B4A40;
  --muted:      #5B6B60;
  --faint:      #A6B8AC;
  --dashed:     #7C8F82;
  --hairline:   #E4DCCC;
  --staffline:  #B5C8BB;

  --font-display: 'Bricolage Grotesque', system-ui, sans-serif;
  --font-ui:      'Familjen Grotesk', system-ui, sans-serif;
  --font-mono:    'Sometype Mono', ui-monospace, monospace;

  --ease-press: cubic-bezier(.2, .7, .3, 1);
  --lift: translate(-1px, -1px);
  --ink-soft:   #38473D;
  --cream-deep: #E6DAC4;
  --sun-lift:   #8CC4A1;
  --danger:     #8C3A2A;
  --on-sun:     #16241B;  /* the resting root is light, so its labels are ink */

  /* ── Label layers ────────────────────────────────────────────────────────
     Every resting face is light now, so all three layer colours are checked
     against all four. */
  --layer-numbers:  #1B2820;
  --layer-notes:    #0C4A22;
  --layer-keyboard: #57340F;

  /* ── Wood ────────────────────────────────────────────────────────────────
     The room the instrument stands in, never the instrument itself. The one
     green still owns every key face, every chord ring and every reward; wood
     owns the surfaces those things sit on, which is where warmth can go
     without putting a second colour on the grid. It stays quiet by staying
     light: the plank, the panels and the floor are all above L* 90, and the
     two dark values are used as hairlines and grain rather than as fills. */
  --bark:       #4A3620;  /* deepest grain, the far edge of the floor */
  --wood:       #8A6039;  /* mid oak, grain lines */
  --wood-soft:  #C9A377;  /* light oak, the ring inside the Rings mark */
  --wood-pale:  #F2E7D6;  /* the plank face */
  --wood-line:  #E0CDAC;  /* grain hairline, panel bevels */

  /* ── Chord quality ──────────────────────────────────────────────────────
     These live on panel chips rather than the instrument, so they stay
     distinguishable from each other without adding hues to the grid. */
  --q-major: #1F6B41;
  --q-minor: #2E6B7C;
  --q-dim:   #5B5B7C;
  --q-aug:   #7C4A5B;
  --q-sus:   #5E7A5E;
  --q-dom:   #6B5A18;
  --q-other: #5B6B60;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}

button {
  font-family: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}
button:focus-visible, select:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--gold-text);
  outline-offset: 2px;
}

/* ─── Shared primitives ──────────────────────────────────────────────────── */

.mono-label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: .14em;
  color: var(--muted);
  text-transform: uppercase;
}
/* Secondary label text, not a guide line: --faint is 2.12:1 and fails even
   the large-text threshold. */
.mono-label.faint { color: var(--muted); }
.mono-label.gold-dim { color: var(--gold-dim); }

.numeral {
  font-family: var(--font-display);
  font-weight: 800;
  color: var(--ink);
  /* Bricolage 800 needs headroom: at line-height 1 the glyph box overflowed its
     line box and the tops/bottoms of the big numerals were clipped. */
  line-height: 1.14;
}
.numeral.sm  { font-size: 28px; letter-spacing: -.035em; margin-top: 2px; }
.numeral.md  { font-size: 32px; letter-spacing: -.04em; }
.numeral.lg  { font-size: 42px; letter-spacing: -.04em; margin-top: 2px; }
.numeral.xl  { font-size: 48px; letter-spacing: -.04em; }
.numeral.xxl { font-size: 58px; letter-spacing: -.05em; margin-top: 2px; }
.numeral.gold { color: var(--gold-text); }
.numeral .faint { color: var(--muted); }

.card {
  border: 2px solid var(--ink);
  border-radius: 16px;
  background: var(--paper);
  padding: 16px;
}
.card.cream { background: var(--cream); }
.card.sun { background: var(--sun); }
.card.shadow-lg { box-shadow: 5px 5px 0 var(--ink); }
.card-title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 16px;
  letter-spacing: -.02em;
  color: var(--ink);
}
.card-title.big { font-size: 19px; letter-spacing: -.025em; margin-top: 7px; }
.card-body { font-size: 13px; line-height: 1.5; color: var(--body-text); margin-top: 6px; }
.card-meta { font-size: 12.5px; color: var(--muted); margin-top: 7px; }

.sun-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 10px 18px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--sun);
  color: var(--ink);
  font-size: 13px;
  font-weight: 700;
  box-shadow: 3px 3px 0 var(--ink);
  transition: transform .1s var(--ease-press), box-shadow .1s var(--ease-press);
}
.sun-btn.wide { display: flex; width: 100%; margin-top: 14px; padding: 11px 0; }
.sun-btn:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--ink); }

.ghost-btn {
  padding: 9px 14px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}
.ghost-btn[aria-pressed="true"] { background: var(--sun); box-shadow: 3px 3px 0 var(--ink); }
.ghost-btn.danger { border-color: var(--dashed); color: var(--muted); }
.ghost-btn.sm { padding: 6px 12px; font-size: 12px; }

.progress-track {
  height: 10px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  overflow: hidden;
}
.progress-track.thick { height: 12px; margin-top: 14px; }
.progress-fill { height: 100%; background: var(--sun); transition: width .3s var(--ease-press); }
.progress-fill.ink { background: var(--ink); }

/* CSS hexagon (pointy-top) */

/* ─── App frame ──────────────────────────────────────────────────────────── */

#app {
  display: grid;
  grid-template-columns: 92px 1fr;
  /* minmax(0, 1fr), not 100dvh: the row is derived from the height above rather
     than restating it, and the 0 floor lets it shrink below its content so no
     child can push the frame past the viewport. */
  grid-template-rows: minmax(0, 1fr);
  height: 100dvh;
  overflow: hidden;
  background: var(--paper);
}

/* ─── Icon rail ──────────────────────────────────────────────────────────── */

#rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 18px 0;
  /* A plank rather than a panel. Two warm stops down its length with hairline
     grain over them, and a bevel on the inner edge so it reads as a board with
     a thickness. Both are gradients on the element itself, so the texture
     costs no image, no extra node and nothing per frame. */
  background:
    repeating-linear-gradient(90deg,
      transparent 0 17px, var(--wood-line) 17px 18px,
      transparent 18px 38px, var(--wood-line) 38px 39px,
      transparent 39px 61px),
    linear-gradient(180deg, var(--wood-pale) 0%, var(--cream) 54%, var(--wood-pale) 100%);
  box-shadow: inset -4px 0 0 var(--wood-line);
  border-right: 2px solid var(--ink);
  z-index: 30;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: none;
}
#rail::-webkit-scrollbar { display: none; }

.rail-btn {
  width: 68px;
  padding: 11px 0;
  border-radius: 13px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  border: 2px solid transparent;
  transition: background .14s, box-shadow .14s;
}
.rail-btn .rail-label { font-size: 11px; font-weight: 500; color: var(--muted); }
.rail-btn.active { background: var(--sun); border-color: var(--ink); box-shadow: 3px 3px 0 var(--ink); }
.rail-btn.active .rail-label { font-weight: 700; color: var(--ink); }

.rail-icon { display: block; }
.logo-cone { display: block; width: 34px; height: 34px; border-radius: 9px; }

/* ── Rail icons ──────────────────────────────────────────────────────────────
   Three places rather than three functions: a grove to wander, a trail to
   follow, and the rings you add by coming back. Each is drawn in CSS from the
   theme tokens, so none of them is an extra file to load. */
.rail-icon { display: block; width: 22px; height: 22px; margin: 0 auto 5px; }

/* Grove: a conifer. */
.icon-grove {
  background: var(--muted);
  clip-path: polygon(50% 0%, 74% 42%, 62% 42%, 84% 78%, 56% 78%, 56% 100%, 44% 100%, 44% 78%, 16% 78%, 38% 42%, 26% 42%);
}
.rail-btn.active .icon-grove { background: var(--ink); }

/* Trail: a winding path. */
.icon-trail {
  background: none;
  border: 0;
  position: relative;
}
.icon-trail::before {
  content: '';
  position: absolute; inset: 2px 4px;
  border-left: 3px solid var(--muted);
  border-radius: 0 0 0 14px;
  transform: skewX(-8deg);
}
.icon-trail::after {
  content: '';
  position: absolute; inset: 2px 4px;
  border-right: 3px solid var(--muted);
  border-radius: 0 14px 0 0;
  transform: skewX(-8deg);
}
.rail-btn.active .icon-trail::before, .rail-btn.active .icon-trail::after { border-color: var(--ink); }

/* Rings: a cut trunk, one ring per season, and the one mark in the app that
   is actually looking at wood. Its inner ring is the grain colour in both
   states; the outer ring carries the active contrast, as on every other. */
.icon-rings {
  border: 2px solid var(--muted);
  border-radius: 50%;
  box-shadow: inset 0 0 0 2px var(--paper), inset 0 0 0 4px var(--wood-soft), inset 0 0 0 6px var(--paper);
}
.rail-btn.active .icon-rings {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 2px var(--paper), inset 0 0 0 4px var(--wood), inset 0 0 0 6px var(--paper);
}
.icon-square { width: 17px; height: 17px; border-radius: 4px; border: 3px solid var(--muted); }
.rail-btn.active .icon-square { border-color: var(--ink); }

.rail-foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
#helpButton {
  width: 30px; height: 30px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 14px;
  color: var(--ink);
}
.rail-avatar {
  width: 38px; height: 38px;
  border-radius: 999px;
  background: var(--sun);
  border: 2px solid var(--ink);
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 14px;
  color: var(--ink);
}
.rail-level { font-family: var(--font-mono); font-size: 9px; letter-spacing: .1em; color: var(--muted); }

/* ─── Main / header ──────────────────────────────────────────────────────── */

#main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }

#topbar {
  height: 76px;
  flex: none;
  display: flex;
  align-items: center;
  padding: 0 24px;
  gap: 14px;
  border-bottom: 2px solid var(--ink);
  /* The header is the second half of the frame: the same board as the rail,
     turned along the top, so the two meet in the corner and the stage sits
     inside them. Ink on this is 12.7:1, so the title loses nothing. */
  background: linear-gradient(180deg, var(--wood-pale) 0%, var(--cream) 100%);
}
#screenTitle {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 24px;
  letter-spacing: -.035em;
  color: var(--ink);
}
#screenSub { font-size: 13.5px; color: var(--muted); padding-top: 4px; }

/* ─── The song picker in the title ────────────────────────────────────────
   On the Trail the title is the song, and it opens the library. The library
   used to be a 296px panel down the right of the screen, permanently, for a
   choice made once a session; the staff has that width now. */
body.screen-song #screenTitle { display: none; }
.song-pick-wrap { position: relative; display: flex; align-items: center; }
.song-pick {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  max-width: 42vw;
  padding: 3px 11px 3px 3px;
  border: 2px solid transparent;
  border-radius: 11px;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 24px;
  letter-spacing: -.035em;
  color: var(--ink);
}
.song-pick > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.song-pick .chev {
  flex: none;
  width: 0; height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid var(--ink);
  transition: transform .14s var(--ease-press);
}
.song-pick[aria-expanded="true"] { background: var(--cream); border-color: var(--ink); }
.song-pick[aria-expanded="true"] .chev { transform: rotate(180deg); }
@media (hover: hover) and (pointer: fine) {
  .song-pick:hover { background: var(--cream); border-color: var(--ink); }
}

.song-menu {
  position: absolute;
  top: calc(100% + 10px);
  left: 0;
  z-index: 60;
  width: 330px;
  max-height: min(62dvh, 560px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 13px;
  background: var(--paper);
  border: 2px solid var(--ink);
  border-radius: 15px;
  box-shadow: 5px 5px 0 var(--ink);
}
.song-menu[hidden] { display: none; }
.topbar-spacer { flex: 1; }

.key-stepper {
  display: flex;
  align-items: center;
  border: 2px solid var(--ink);
  border-radius: 999px;
  overflow: hidden;
  background: var(--paper);
}
.key-stepper button {
  width: 32px; height: 34px;
  display: grid;
  place-items: center;
  font-size: 15px;
  color: var(--ink);
}
.key-stepper #keyPrev { border-right: 2px solid var(--ink); }
.key-stepper #keyNext { border-left: 2px solid var(--ink); }
#keyName {
  min-width: 74px;
  text-align: center;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 15px;
  color: var(--ink);
}

#headerCombo {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 13px 6px 9px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--sun);
  box-shadow: 3px 3px 0 var(--ink);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

/* ─── Content: panels + stage ────────────────────────────────────────────── */

#content { flex: 1; display: flex; min-height: 0; }

#panel-left {
  flex: none;
  width: 252px;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  background: var(--paper-warm);
  border-right: 2px solid var(--ink);
  overflow-y: auto;
}

.panel-section { display: flex; flex-direction: column; gap: 9px; }
/* The fold bar exists for the phone layout only. */
.panel-toggle {
  display: none;
  position: sticky; top: 0; z-index: 2;
  align-items: center; justify-content: space-between;
  margin: 0 -14px; padding: 12px 16px;
  background: var(--paper-warm);
  border: none; border-bottom: 2px solid var(--ink);
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink);
}
.panel-toggle .chev {
  width: 10px; height: 10px;
  border-right: 2.4px solid var(--ink); border-bottom: 2.4px solid var(--ink);
  transform: rotate(225deg); transition: transform .18s var(--ease-press);
}
body.phone-panel-shut .panel-toggle { border-bottom: none; }
.legend-block { margin-top: auto; }

/* Screen-scoped visibility: elements keep their natural display; the :not()
   guards remove them everywhere else and outrank any later display rule. */
body:not(.screen-explore) .only-explore { display: none !important; }
body:not(.screen-song) .only-song { display: none !important; }
body:not(.screen-progress) .only-progress { display: none !important; }
.panel-section.only-explore, .panel-section.only-song { display: flex; }
#progress-screen { display: grid; }

body.screen-progress #panel-left { display: none; }
/* Focus is a mode, not a screen: it clears the panel wherever you are. */
body.focus-mode #panel-left { display: none; }
body.screen-progress #stage-wrap { display: none; }
/* No instrument on this screen, so the controls that sit on top of one have
   nothing to control. They were floating over the cards. */
body.screen-progress .label-pills, body.screen-progress #forest { display: none; }
body.screen-progress #stage { overflow-y: auto; background: var(--paper-warm); }

/* Scale pills */
.pill {
  padding: 7px 12px;
  border-radius: 999px;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--ink);
  border: 2px solid var(--ink);
  background: var(--paper);
  transition: background .14s, box-shadow .14s;
}
.pill.active { background: var(--sun); font-weight: 700; box-shadow: 3px 3px 0 var(--ink); }

/* Chord grid */
.chord-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.chord-btn {
  padding: 10px 0;
  text-align: center;
  border-radius: 10px;
  font-size: 13px;
  font-weight: 700;
  color: var(--ink);
  border: 2px solid var(--ink);
  background: var(--paper);
  box-shadow: 2px 2px 0 var(--ink);
  transition: transform .1s var(--ease-press), box-shadow .1s var(--ease-press);
}
.chord-btn:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--ink); }

/* Voice select */
#instrument {
  width: 100%;
  padding: 9px 12px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--paper);
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%231A1814' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
}

/* Legend */
.legend-row-i { display: flex; align-items: center; gap: 10px; font-size: 12.5px; color: var(--body-text); }
.swatch { width: 18px; height: 18px; flex: none; border-radius: 999px; }
.sw-root { background: var(--sun);   border: 2px solid var(--ink); }
.sw-in   { background: var(--key-birch); border: 2px solid var(--ink); }
.sw-sharp{ background: var(--key-sharp); border: 2px solid var(--ink); }
.sw-out  { background: var(--desk);  border: 2px dashed var(--dashed); }

/* Song library */
#songList { display: flex; flex-direction: column; gap: 8px; }
.song-row {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 11px;
  border-radius: 12px;
  border: 2px solid var(--ink);
  background: var(--paper);
  text-align: left;
  width: 100%;
  transition: background .14s, box-shadow .14s;
}
.song-row.active { background: var(--sun); box-shadow: 3px 3px 0 var(--ink); }
.song-chip {
  width: 28px; height: 32px;
  flex: none;
  display: grid;
  place-items: center;
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
  background: var(--ink);
  font-size: 10.5px;
  font-weight: 700;
  color: var(--sun);
}
.song-title { font-size: 13.5px; font-weight: 500; color: var(--ink); letter-spacing: -.01em; }
.song-row.active .song-title { font-weight: 700; }
.song-meta { font-size: 11.5px; color: var(--muted); margin-top: 2px; }

.play-btn {
  width: 40px; height: 40px;
  flex: none;
  border-radius: 999px;
  background: var(--sun);
  border: 2px solid var(--ink);
  display: grid;
  place-items: center;
  box-shadow: 3px 3px 0 var(--ink);
}
.play-btn:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--ink); }
.play-btn .tri {
  width: 0; height: 0;
  border-left: 11px solid var(--ink);
  border-top: 7px solid transparent;
  border-bottom: 7px solid transparent;
  margin-left: 3px;
}
.play-btn.playing .tri {
  border: none;
  width: 10px; height: 12px;
  border-left: 3.5px solid var(--ink);
  border-right: 3.5px solid var(--ink);
  margin-left: 0;
}

/* ─── Stage ──────────────────────────────────────────────────────────────── */

#stage {
  flex: 1;
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  background: var(--paper);
}

#stage-wrap { flex: 1; position: relative; min-height: 0; overflow: hidden; }
#keyboard { position: absolute; inset: 0; display: block; touch-action: none; }

.stage-tools {
  position: absolute;
  left: 14px;
  bottom: 14px;
  display: flex;
  gap: 8px;
  z-index: 5;
}
.stage-tools button {
  width: 36px; height: 36px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  display: grid;
  place-items: center;
  box-shadow: 3px 3px 0 var(--ink);
}
.stage-tools button:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--ink); }
.stage-tools svg { width: 17px; height: 17px; }



/* Loading toast */
#loading-toast {
  position: absolute;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--cream);
  box-shadow: 3px 3px 0 var(--ink);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  z-index: 20;
  transition: opacity .35s;
}
#loading-toast.toast-hide { opacity: 0; }
.loading-spinner {
  width: 14px; height: 14px;
  border: 2.5px solid var(--ink);
  border-top-color: var(--sun);
  border-radius: 999px;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Move handles.
   Dragging on the keys now plays them rather than sliding the instrument, so
   there has to be a deliberate way to move it. On touch these were already the
   only way; on a pointer they are shown too, plus the whole stage outside the
   grid still pans. */
#move-handles { display: block; }
.move-handle {
  position: absolute;
  /* The grips belong to the frame, not to the grid, so they take the frame's
     colour and lift to --cream-deep on hover, which is the same pair the rail
     buttons use. */
  background: var(--cream);
  border: 2px solid var(--ink);
  z-index: 6;
  touch-action: none;
  cursor: grab;
  opacity: 0.55;
  transition: opacity .15s var(--ease-press), background .15s var(--ease-press);
}
.move-handle:active { cursor: grabbing; }
/* Three grip lines, so it reads as something to take hold of. */
.move-handle::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(var(--grip-dir, 90deg),
    var(--ink) 0 2px, transparent 2px 6px);
  background-position: center;
  background-repeat: no-repeat;
  opacity: 0.5;
}
.move-handle.top, .move-handle.bottom {
  left: 50%; transform: translateX(-50%);
  width: 92px; height: 18px; border-radius: 999px;
  --grip-dir: 90deg;
}
.move-handle.top::after, .move-handle.bottom::after { background-size: 26px 8px; }
.move-handle.top { top: 8px; }
.move-handle.bottom { bottom: 8px; }
.move-handle.left, .move-handle.right {
  top: 50%; transform: translateY(-50%);
  height: 92px; width: 18px; border-radius: 999px;
  --grip-dir: 0deg;
}
.move-handle.left::after, .move-handle.right::after { background-size: 8px 26px; }
.move-handle.left { left: 8px; }
.move-handle.right { right: 8px; }
@media (hover: hover) and (pointer: fine) {
  .move-handle:hover { opacity: 1; background: var(--cream-deep); }
}
/* On a phone the top and bottom handles would sit under the lane and the
   layer bar, so only the side ones show in portrait. */
@media (pointer: coarse) and (orientation: portrait) {
  .move-handle.left, .move-handle.right { display: none; }
}

/* ─── Right panel pieces ─────────────────────────────────────────────────── */

.tile { border-radius: 14px; padding: 14px; }


/* ─── You (progress) screen ──────────────────────────────────────────────── */

#progress-screen {
  flex: 1;
  padding: 24px 28px;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  grid-auto-rows: min-content;
  align-content: start;
  gap: 16px;
  min-height: 0;
  /* The screen scrolls as one page. It was overflow: hidden with the lower
     cards allowed to shrink, so a full list of milestones and discoveries
     ran out of the bottom of its card and was cut off by the screen edge. */
  overflow-y: auto;
  background: var(--paper-warm);
}
/* Notes about where progress is kept take a row of their own above the strip,
   and only when one is showing, so the cards below keep the stretching row. */
.prog-notes { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 12px; }
.prog-notes:not(:has(> .card:not([hidden]))) { display: none; }
.keep-card, .account-card { display: flex; align-items: center; gap: 16px; padding: 14px 18px; }
.keep-card[hidden], .account-card[hidden] { display: none; }
.keep-copy { flex: 1; }
.account-card .google-btn[hidden], .account-card .mini-btn[hidden], .account-card .avatar[hidden] { display: none; }
.level-card { padding: 20px; }
.level-xp { display: flex; align-items: baseline; gap: 9px; margin-top: 8px; }
.xp-of { font-size: 13px; color: var(--muted); font-weight: 500; }
.streak-card { padding: 20px; }
.streak-card .xp-of { color: var(--muted); }
/* A day is a ring, which is what the screen is named for: a trunk adds one a
   year and you add one a day. Hexagons were left over from v3's mark. */
.streak-rings { display: flex; gap: 7px; margin-top: 14px; }
.streak-rings .day-ring {
  width: 22px; height: 22px;
  flex: none;
  border-radius: 50%;
  border: 2px solid var(--faint);
  background: transparent;
}
.streak-rings .day-ring.on {
  border-color: var(--ink);
  background: var(--sun);
  box-shadow: inset 0 0 0 3px var(--paper);
}

.keys-card, .badges-card {
  padding: 22px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
/* Not centred and not stretched: the wheel is 240px tall and the copy beside
   it is three lines, so centring a stretched row left a wide empty band above
   the scale list. */
.keys-main { display: flex; gap: 26px; align-items: center; }
.key-wheel { position: relative; width: 240px; height: 240px; flex: none; }
/* The keys on the grid are circles and the screen is called Rings, so these
   are rings too. They were hexagons because v3's mark was one. */
.wheel-ring {
  position: absolute;
  width: 40px; height: 40px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 2px solid var(--faint);
  background: var(--paper);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}
.wheel-ring.on {
  border-color: var(--ink);
  background: var(--sun);
  color: var(--paper);
  box-shadow: 2px 2px 0 var(--ink);
}
.wheel-center {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  text-align: center;
}
.wheel-center .mono-label { margin-top: 4px; }
.keys-copy { flex: 1; min-width: 0; }
.keys-copy .card-title.big { font-size: 21px; margin-top: 0; }
.keys-copy .sun-btn { margin-top: 15px; }

.stat-rows { display: flex; flex-direction: column; gap: 11px; }
.stat-rows.bordered-top { flex: none; padding-top: 16px; border-top: 2px solid var(--hairline); }
.stat-row { display: flex; align-items: center; gap: 12px; }
.stat-row .stat-name { width: 86px; flex: none; font-size: 12.5px; color: var(--body-text); }
.stat-row .stat-track {
  flex: 1;
  height: 9px;
  border: 1.5px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  overflow: hidden;
}
.stat-row .stat-fill { height: 100%; background: var(--sun); }

/* Seven scale rows and five session rows are usually all at zero on a first
   visit, and at 10px each they read as a wall of empty pills rather than a
   list of things to fill in. Thinner and tighter reads as a list. */
/* A fixed name column, so 'Pentatonic major' cannot wrap and shunt its own
   row out of line with the others. */
.stat-rows .stat-row { gap: 12px; }
.stat-rows .stat-name { width: 116px; flex: none; font-size: 12.5px; }
.stat-rows .stat-track { height: 6px; border-width: 1.5px; }
.stat-rows .stat-count { font-variant-numeric: tabular-nums; }
/* An untouched row is quieter than a started one, so progress is visible at a
   glance instead of having to read every number. */
.stat-row.empty .stat-name, .stat-row.empty .stat-count { color: var(--muted); }
.stat-row.empty .stat-track { border-color: var(--faint); }
.stat-row .stat-count { width: 52px; text-align: right; font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.stat-row .stat-count.ink { color: var(--ink); font-size: 11.5px; }

/* No clipping: the You screen's stage scrolls, so a list longer than the card
   extends it rather than being cut off mid-row. */
.badges-main { display: flex; flex-direction: column; gap: 12px; }
.badge-row { display: flex; align-items: center; gap: 12px; }
/* The shape a discovery is marked with: a cone, matching the app's own mark.
   Shared by the badge rows and the reward toasts so the two cannot drift. */
/* The discovery mark, drawn as a real cone (see coneMark in ui.js). The
   border-radius version read as a coffee bean at this size. */
.cone-mark { flex: none; width: 30px; height: 36px; display: block; }
.badge-title { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.badge-row.locked .badge-title { color: var(--muted); }
.badge-meta { font-size: 11.5px; color: var(--muted); margin-top: 1px; }
.badge-xp { font-family: var(--font-mono); font-size: 11px; font-weight: 600; color: var(--gold-text); margin-left: auto; }

/* ─── The Rings screen: today's ring, the trunk, what is next ─────────────── */
.rings-hero { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr) minmax(0, .95fr); gap: 16px; }
.rings-hero > .card, .hero-side > .card, .next-card, .activity-card, .keys-card, .badges-card { min-width: 0; }
.card-meta, .badge-meta, .badge-title { overflow-wrap: anywhere; }
.today-card { display: flex; align-items: center; gap: 22px; padding: 22px; }
.today-ring { position: relative; width: 150px; height: 150px; flex: none; }
.ring-svg { width: 100%; height: 100%; display: block; }
.ring-track { fill: none; stroke: var(--desk); stroke-width: 18; }
.ring-edge { fill: none; stroke: var(--ink); stroke-width: 2; }
.ring-fill { fill: none; stroke: var(--sun-lit); stroke-width: 16; stroke-linecap: round; }
.ring-svg.closed .ring-track { stroke: var(--sun); }
.ring-svg.closed .ring-fill { stroke: var(--pressed); }
.ring-mid { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
.ring-mid .mono-label { margin-top: 1px; }
.today-copy { flex: 1; min-width: 0; }
.today-copy .card-title.big { font-size: 21px; margin-top: 4px; }
.goal-pick { display: inline-flex; margin-top: 14px; border: 2px solid var(--ink); border-radius: 999px; overflow: hidden; background: var(--paper); }
.goal-btn { display: flex; align-items: baseline; gap: 6px; padding: 7px 12px; border: none; border-right: 2px solid var(--ink); background: none; font-size: 12.5px; font-weight: 600; color: var(--ink); }
.goal-btn:last-child { border-right: none; }
.goal-btn span { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); }
.goal-btn[aria-pressed="true"] { background: var(--sun); }
.goal-btn[aria-pressed="true"] span { color: var(--ink); }
@media (hover: hover) and (pointer: fine) {
  .goal-btn:hover { background: var(--cream); }
  .goal-btn[aria-pressed="true"]:hover { background: var(--sun); }
}
/* The trunk is wood, the one place the Rings screen uses it: it is the room's
   material, and a cross-section is what the screen is named for. */
.trunk-card { display: flex; align-items: center; gap: 16px; padding: 18px 20px; }
.trunk { width: 160px; height: 160px; flex: none; }
.trunk-svg { width: 100%; height: 100%; display: block; }
.trunk-svg .bark { fill: var(--bark); stroke: var(--ink); stroke-width: 2; }
.trunk-svg .tree-ring { fill: var(--wood-pale); stroke: var(--wood-line); stroke-width: .8; }
.trunk-svg .tree-ring.full { fill: var(--wood-soft); stroke: var(--wood); stroke-width: 1; }
.trunk-svg .tree-ring.today { fill: var(--desk); stroke: var(--sun-lit); }
.trunk-svg .tree-ring.full.today { fill: var(--sun); stroke: var(--pressed); }
.trunk-svg .pith { fill: var(--wood); }
.trunk-svg .trunk-ghost { fill: none; stroke: var(--dashed); stroke-width: 2; stroke-dasharray: 5 6; }
.trunk-copy { min-width: 0; }
.trunk-copy .numeral { margin-top: 6px; }
.hero-side { display: flex; flex-direction: column; gap: 16px; }
.streak-rings .day-ring.rest { border: 2px dashed var(--dashed); }

.next-card { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 14px; padding: 20px 22px; }
.next-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 22px; }
.next-item { display: flex; align-items: center; gap: 12px; min-width: 0; }
.next-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.next-text .stat-track { margin-top: 6px; height: 7px; border: 1.5px solid var(--ink); border-radius: 999px; overflow: hidden; background: var(--paper); }
.next-text .stat-fill { height: 100%; background: var(--sun); }
.next-side { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.next-side .stat-count { font-family: var(--font-mono); font-size: 11px; color: var(--ink); font-variant-numeric: tabular-nums; }

.track-list { display: flex; flex-direction: column; gap: 12px; }
.track-row { display: flex; align-items: center; gap: 14px; }
.track-row.locked .badge-title { color: var(--muted); }
.tier-pips { display: flex; gap: 4px; flex: none; width: 76px; }
.tier-pip { width: 15px; height: 15px; border-radius: 50%; border: 2px solid var(--faint); background: transparent; }
.tier-pip.on { border-color: var(--ink); background: var(--sun); box-shadow: inset 0 0 0 2px var(--paper); }

.activity-card { grid-column: 1 / -1; display: flex; gap: 28px; align-items: flex-start; padding: 20px 22px; }
.activity-main { flex: none; display: flex; flex-direction: column; gap: 12px; max-width: 100%; }
.heatmap { display: flex; gap: 3px; overflow-x: auto; padding-bottom: 2px; }
.heat-col { display: flex; flex-direction: column; gap: 3px; }
.heat, .heat-key i { display: block; width: 13px; height: 13px; border-radius: 4px; }
.heat-key { display: flex; align-items: center; gap: 4px; font-family: var(--font-mono); font-size: 9.5px; color: var(--muted); }
.heat-key span { margin: 0 3px; }
.h0 { background: var(--paper-warm); box-shadow: inset 0 0 0 1px var(--hairline); }
.h1 { background: var(--key-sharp); }
.h2 { background: var(--sun); }
.h3 { background: var(--sun-lit); }
.heat.today { box-shadow: 0 0 0 2px var(--ink); }
.heat.future { visibility: hidden; }
.records { flex: 1; min-width: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 18px; }
.record .numeral { margin-top: 4px; }
.record .card-meta { margin-top: 2px; }

@media (max-width: 1279px) {
  .rings-hero { grid-template-columns: 1fr 1fr; }
  .hero-side { grid-column: 1 / -1; flex-direction: row; }
  .hero-side > .card { flex: 1; }
  .next-card, .activity-card, .rings-hero { margin-top: 16px; }
  .rings-hero { margin-top: 0; }
  .activity-card { flex-direction: column; }
  .records { width: 100%; }
}
@media (max-width: 760px) {
  .rings-hero { grid-template-columns: 1fr; }
  .hero-side { flex-direction: column; }
  .today-card { gap: 16px; padding: 18px; }
  .today-ring { width: 112px; height: 112px; }
  .today-copy .card-title.big { font-size: 18px; }
  .trunk-card { flex-direction: row; }
  .trunk { width: 112px; height: 112px; }
  .heatmap, .heat-col { gap: 2px; }
  .heat { width: 10px; height: 10px; border-radius: 3px; }
  .next-list { grid-template-columns: 1fr; }
  .goal-pick { display: flex; }
  .goal-btn { flex: 1; flex-direction: column; align-items: center; gap: 1px; padding: 5px 4px; font-size: 12px; }
  .goal-btn span { font-size: 10px; }
}

/* ─── Help overlay ───────────────────────────────────────────────────────── */

#help-overlay { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; padding: 24px; }
.help-scrim { position: absolute; inset: 0; background: rgba(26, 24, 20, .45); }
.help-card {
  position: relative;
  width: min(520px, 100%);
  max-height: calc(100dvh - 48px);
  overflow-y: auto;
  background: var(--paper);
  border: 2px solid var(--ink);
  border-radius: 20px;
  box-shadow: 8px 8px 0 var(--ink);
  padding: 22px;
}
.help-head { display: flex; align-items: center; justify-content: space-between; }
.help-head h2 { margin: 0; font-family: var(--font-display); font-weight: 800; font-size: 21px; letter-spacing: -.03em; }
.drawer-close {
  width: 32px; height: 32px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  display: grid;
  place-items: center;
  color: var(--ink);
}
.drawer-close svg { width: 15px; height: 15px; }
.help-list { margin: 14px 0 0; padding: 0 0 0 18px; display: flex; flex-direction: column; gap: 10px; font-size: 13.5px; line-height: 1.5; color: var(--body-text); }
.help-list b { color: var(--ink); }
.help-list kbd {
  font-family: var(--font-mono);
  font-size: 11px;
  border: 1.5px solid var(--ink);
  border-radius: 6px;
  padding: 1px 5px;
  background: var(--cream);
  color: var(--ink);
}
.help-actions { display: flex; gap: 10px; margin-top: 18px; flex-wrap: wrap; }

/* ─── Phone companion (≤760px) ───────────────────────────────────────────── */


@media (max-width: 760px) {
  #app { grid-template-columns: 1fr; grid-template-rows: 1fr 64px; }

  /* Rail becomes the bottom tab bar. The plank turns with it: grain runs
     along the length of a board, so on a phone that is across, not down. */
  #rail {
    order: 2;
    flex-direction: row;
    justify-content: space-around;
    padding: 6px 10px;
    border-right: none;
    border-top: 2px solid var(--ink);
    background:
      repeating-linear-gradient(0deg,
        transparent 0 17px, var(--wood-line) 17px 18px,
        transparent 18px 38px, var(--wood-line) 38px 39px,
        transparent 39px 61px),
      linear-gradient(90deg, var(--wood-pale) 0%, var(--cream) 54%, var(--wood-pale) 100%);
    box-shadow: inset 0 4px 0 var(--wood-line);
  }
  .rail-logo, .rail-foot .rail-avatar, .rail-foot .rail-level { display: none; }
  .rail-foot { margin: 0; flex-direction: row; }
  .rail-btn { width: 62px; padding: 6px 0; gap: 4px; }

  #topbar { height: 60px; padding: 0 12px; gap: 8px; }
  #screenTitle { font-size: 18px; }

  

  /* Companion order: combo → falling lane → key field at the bottom (thumbs). */
  body.screen-song #stage-wrap { order: 3; flex: none; height: 38dvh; border-top: 2px solid var(--ink); background: var(--paper-warm); }

}

/* ─── Reduced motion ─────────────────────────────────────────────────────── */



/* ═══════════════════════════════════════════════════════════════════════════
   Tablet (761-1279px). The desktop rail + two panels eat 640px of chrome,
   which left an iPad-portrait stage 128px wide. Narrow everything, and drop
   the right panel entirely below 1024 so the instrument keeps the room.
   Ranges are explicit so these never collide with the phone rules.
   ═════════════════════════════════════════════════════════════════════════ */

@media (min-width: 761px) and (max-width: 1279px) {
  #app { grid-template-columns: 76px 1fr; }
  #rail { gap: 6px; padding: 12px 0; }
  .rail-btn { width: 62px; padding: 8px 0; gap: 5px; }
  #topbar { height: 64px; padding: 0 14px; gap: 10px; }
  #screenTitle { font-size: 20px; }
  #panel-left { width: 216px; padding: 16px; gap: 14px; }
  #progress-screen { grid-template-columns: 1fr; padding: 18px; }
  .level-card { min-width: 260px; }
}

/* The header chip is the only combo surface below the desktop tier. */
@media (max-width: 1023px) { body.screen-song #headerCombo { display: flex; } }
/* One rule per element, over the whole range it is hidden, rather than one per
   abutting tier. */
@media (max-width: 1279px) { #screenSub { display: none; } }

/* The header combo chip stands in for the right panel's COMBO card. It owns
   the pill recipe above outright now that the streak chip is gone. */
#headerCombo {
  display: none;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 16px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   Hover affordances. Pointer devices only, so touch never gets a stuck state.
   The language is consistent: lift up-left by 1px, deepen the ink shadow.
   ═════════════════════════════════════════════════════════════════════════ */

@media (hover: hover) and (pointer: fine) {
  .pill, .chord-btn, .song-row, .sun-btn, .ghost-btn,
  .stage-tools button, #helpButton,
  .key-stepper button, .play-btn, .rail-btn, .rail-logo {
    transition: transform .13s var(--ease-press), box-shadow .13s var(--ease-press),
                background .13s var(--ease-press), color .13s var(--ease-press);
  }

  .rail-btn:hover:not(.active) { background: var(--cream-deep); }
  .rail-btn:hover:not(.active) .rail-label { color: var(--ink); }
  .rail-btn:hover:not(.active) .icon-grove { background: var(--ink-soft); }
  .rail-btn:hover:not(.active) .icon-trail::before,
  .rail-btn:hover:not(.active) .icon-trail::after,
  .rail-btn:hover:not(.active) .icon-rings,
  .rail-btn:hover:not(.active) .icon-square { border-color: var(--ink-soft); }
  .rail-btn.active:hover { transform: var(--lift); box-shadow: 4px 4px 0 var(--ink); }
  .rail-logo:hover { transform: scale(1.08) rotate(-4deg); }

  .pill:hover:not(.active) { transform: var(--lift); box-shadow: 2px 2px 0 var(--ink); }
  .pill.active:hover { transform: var(--lift); box-shadow: 4px 4px 0 var(--ink); }
  .chord-btn:hover { transform: var(--lift); box-shadow: 4px 4px 0 var(--ink); background: var(--cream); }
  .song-row:hover:not(.active) { transform: var(--lift); box-shadow: 3px 3px 0 var(--ink); }
  .song-row.active:hover { transform: var(--lift); box-shadow: 4px 4px 0 var(--ink); }
  .sun-btn:hover { transform: var(--lift); box-shadow: 5px 5px 0 var(--ink); }
  .ghost-btn:hover { background: var(--cream); transform: var(--lift); }
  .ghost-btn.danger:hover { border-color: var(--danger); color: var(--danger); background: var(--paper); }
  .stage-tools button:hover { transform: var(--lift); box-shadow: 4px 4px 0 var(--ink); background: var(--sun); }
  #helpButton:hover { background: var(--sun); transform: scale(1.08); }
  .key-stepper button:hover { background: var(--sun); }
  .play-btn:hover { transform: var(--lift); box-shadow: 4px 4px 0 var(--ink); }
  .wheel-ring { transition: transform .16s var(--ease-press); }
  .wheel-ring:hover { transform: scale(1.12); }
  .badge-row { transition: background .13s; border-radius: 10px; }
  .badge-row:hover { background: var(--cream); }
  #instrument:hover { background-color: var(--cream); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Reward motion. Every animation here is triggered by a class that JS adds and
   removes, so nothing loops forever and nothing runs without a real event.
   ═════════════════════════════════════════════════════════════════════════ */

/* Combo counter pops on each increment. */
@keyframes comboPop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.18) rotate(-1.5deg); }
  100% { transform: scale(1); }
}
.combo-pop { animation: comboPop .26s var(--ease-press); }

/* Combo tiers escalate the card so a run feels like it is building. */
.combo-surface { transition: box-shadow .2s var(--ease-press), transform .2s var(--ease-press); }
.combo-surface.tier-1 { box-shadow: 6px 6px 0 var(--ink); }
.combo-surface.tier-2 { box-shadow: 7px 7px 0 var(--ink); transform: var(--lift); }
/* tier-3 is state, not a one-shot: it runs for as long as the run survives and
   stops the moment a miss resets the combo. Reduced motion kills it wholesale. */
.combo-surface.tier-3 { box-shadow: 8px 8px 0 var(--ink); transform: translate(-2px, -2px); animation: comboGlow 1.6s ease-in-out infinite; }
@keyframes comboGlow {
  0%, 100% { background: var(--sun); }
  50%      { background: var(--sun-lift); }
}

/* Floating "+n xp" ticks that rise off the XP tile. */
#xp-ticks { position: relative; }
.xp-tick {
  position: absolute;
  right: 4px; top: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 15px;
  color: var(--gold-text);
  pointer-events: none;
  animation: xpRise .9s ease-out forwards;
}
@keyframes xpRise {
  0%   { transform: translateY(0) scale(.85); opacity: 0; }
  22%  { transform: translateY(-8px) scale(1.05); opacity: 1; }
  100% { transform: translateY(-34px) scale(1); opacity: 0; }
}

/* Streak chip pulses the day it ticks over. */
}

/* Reward toasts: badge unlocks and level-ups slide in from the right. */
#toast-stack {
  position: fixed;
  right: 20px; bottom: 20px;
  display: flex;
  flex-direction: column-reverse;
  gap: 10px;
  z-index: 200;
  pointer-events: none;
}
/* Built as `card shadow-lg reward-toast` (+ `sun` when it is a level-up), with
   .mono-label / .card-title / .badge-xp / .hex inside, so only the deltas live
   here. See showToast() in ui.js. */
.reward-toast {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 18px 13px 14px;
  border-radius: 14px;
  max-width: 320px;
  animation: toastIn .32s var(--ease-press) both, toastOut .35s var(--ease-press) 3.1s both;
}
.reward-toast .cone-mark { animation: coneDrop .6s var(--ease-press); }
.reward-toast .toast-kicker { font-size: 9px; }
.reward-toast.level .toast-kicker { color: var(--gold-dim); }
.reward-toast .toast-title { font-size: 15px; margin-top: 2px; }
@keyframes toastIn  { from { transform: translateX(140%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(140%); opacity: 0; } }
@keyframes coneDrop { 0% { transform: translateY(-9px) rotate(-12deg) scale(.7); opacity: 0; } 60% { transform: translateY(2px) rotate(4deg) scale(1.06); } 100% { transform: translateY(0) rotate(0) scale(1); } }

/* Screen changes settle in rather than snapping. */
@keyframes screenIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.screen-enter .screen-region { animation: screenIn .26s var(--ease-press) both; }

/* You screen: the wheel fills in and the bars grow when you arrive. */
@keyframes hexPop { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.key-wheel .wheel-ring { animation: hexPop .34s var(--ease-press) both; }
/* scaleX, not width: width animates layout on every bar every frame. The
   earlier `width: 0 !important` here was a no-op anyway, since the spec ignores
   !important inside keyframes. */
@keyframes barGrow { from { transform: scaleX(0); } }
.stat-fill, .progress-fill { transform-origin: left center; animation: barGrow .55s var(--ease-press); }

/* One reduced-motion policy for the whole app. A blanket rather than a list of
   selectors, because a list silently fails open: any animation added later that
   nobody remembers to add here would still run, for exactly the people who
   asked it not to. Colour and shadow feedback survives; only movement goes. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  :root { --lift: none; }        /* kills every hover displacement at once */
  /* The three hovers that scale instead of lifting. */
  .rail-logo:hover, #helpButton:hover, .wheel-ring:hover { transform: none; }
}

/* ─── Header tools: Focus toggle + Settings ──────────────────────────────── */

#focusToggle, #settingsToggle {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 8px 13px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  box-shadow: 3px 3px 0 var(--ink);
}
#settingsToggle { padding: 8px; }
#settingsToggle svg { width: 17px; height: 17px; display: block; }
#focusToggle .icon-square { width: 13px; height: 13px; border-width: 2.5px; border-color: var(--ink); }
#focusToggle[aria-pressed="true"] { background: var(--sun); }
#focusToggle:active, #settingsToggle:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--ink); }
@media (hover: hover) and (pointer: fine) {
  #focusToggle:hover, #settingsToggle:hover { transform: var(--lift); box-shadow: 4px 4px 0 var(--ink); background: var(--cream); }
  #focusToggle[aria-pressed="true"]:hover { background: var(--sun); }
}

/* ─── Settings modal (the v2 advanced set) ───────────────────────────────── */

/* Settings docks on the right rather than covering the app, so the instrument
   stays visible and playable while you turn the dials and watch them land. */
#panel-settings {
  display: none;
  flex: none;
  width: 320px;
  flex-direction: column;
  padding: 16px 20px 24px;
  background: var(--paper-warm);
  border-left: 2px solid var(--ink);
  overflow-y: auto;
  overscroll-behavior: contain;
}
body.settings-open #panel-settings { display: flex; }
#panel-settings .help-head { position: sticky; top: -16px; z-index: 1; padding: 8px 0 10px; margin-top: -8px; background: var(--paper-warm); }
#panel-settings .help-head h2 { font-size: 19px; }
#settingsToggle[aria-pressed="true"] { background: var(--sun); }
.set-group { display: flex; flex-direction: column; gap: 10px; padding: 16px 0; border-bottom: 2px solid var(--hairline); }
.set-group:last-of-type { border-bottom: none; }
.set-row { display: flex; align-items: center; gap: 12px; }
/* The playing-note colour: a swatch you click, and a way back to the default. */
.color-pick { display: flex; align-items: center; gap: 8px; }
.color-pick input[type="color"] {
  width: 44px; height: 32px; padding: 2px;
  border: 2px solid var(--ink); border-radius: 10px;
  background: var(--paper); cursor: pointer;
}
.set-row label { flex: 1; font-size: 13.5px; color: var(--body-text); }
.set-row select, .set-row input[type="number"] {
  width: 190px;
  padding: 8px 11px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--paper);
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}
.set-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.set-pair .set-row select, .set-pair .set-row input { width: 84px; }
.set-check { display: flex; align-items: center; gap: 9px; font-size: 13.5px; color: var(--body-text); cursor: pointer; }
.set-check input { width: 17px; height: 17px; accent-color: var(--ink); }

.slider-row { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; align-items: center; }
.slider-row label { font-size: 13.5px; color: var(--body-text); }
.slider-val { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink); }
.slider-row input[type="range"] { grid-column: 1 / -1; width: 100%; accent-color: var(--sun); }

.set-advanced { border-top: 2px solid var(--hairline); padding-top: 12px; }
/* A group after the Microtonal disclosure needs its own rule above it: the
   disclosure draws only the line on its top edge. */
.set-advanced + .set-group { margin-top: 12px; border-top: 2px solid var(--hairline); }
.set-advanced summary {
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
}
.set-advanced label { display: block; margin-top: 12px; font-size: 12.5px; color: var(--body-text); }
.set-advanced textarea {
  width: 100%;
  margin-top: 5px;
  padding: 9px 11px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--paper);
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--ink);
  resize: vertical;
}
.set-actions { display: flex; gap: 10px; margin-top: 18px; flex-wrap: wrap; }
.set-copy { font-size: 13px; line-height: 1.5; color: var(--body-text); }
.set-buttons { display: flex; gap: 10px; flex-wrap: wrap; }
.restore-confirm { display: flex; flex-direction: column; gap: 10px; padding: 12px; border: 2px dashed var(--dashed); border-radius: 12px; }
.restore-confirm[hidden] { display: none; }
/* Accounts. The Google mark sits on the same paper and ink as every other
   button, which Google's own guidance allows for a neutral button. */
.account-block { display: flex; flex-direction: column; gap: 12px; }
.account-block[hidden], #accountOut[hidden], #accountIn[hidden] { display: none; }
#accountOut, #accountIn { display: flex; flex-direction: column; gap: 10px; }
.google-btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 9px 16px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  font-family: var(--font-ui);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  box-shadow: 3px 3px 0 var(--ink);
  white-space: nowrap;
}
.google-btn:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--ink); }
.g-mark { width: 18px; height: 18px; flex: none; }
.text-input {
  flex: 1;
  min-width: 0;
  padding: 8px 11px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--paper);
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}
.name-row { display: flex; flex-direction: column; gap: 6px; }
.name-row label { font-size: 13.5px; color: var(--body-text); }
/* A ring around the initial, the same mark the Rings screen is named for.
   The photo, when there is one, comes through /api so no other origin is
   ever fetched by the page. */
.avatar {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 2px solid var(--ink);
  border-radius: 50%;
  box-shadow: 0 0 0 3px var(--paper), 0 0 0 5px var(--hairline);
  background: var(--paper-warm);
  overflow: hidden;
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 700;
  color: var(--ink);
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.profile-row { display: flex; align-items: center; gap: 14px; }
.profile-text { min-width: 0; }
.profile-name { font-size: 16px; font-weight: 700; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.profile-meta { font-size: 12px; color: var(--muted); margin-top: 2px; }
.sync-status { font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em; color: var(--muted); min-height: 14px; }
.legal-links { display: flex; gap: 8px; font-size: 12px; color: var(--muted); }
.legal-links a { color: var(--body-text); }

/* ─── The account menu in the header ─────────────────────────────────────
   Where every site that works without an account keeps one: the far end of
   the header. Signed out it is a button like Focus; signed in it is the
   avatar, with the same offset shadow. */
.acct-wrap { position: relative; display: flex; align-items: center; }
.acct-wrap[hidden] { display: none; }
.acct-button {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 8px 14px 8px 11px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  box-shadow: 3px 3px 0 var(--ink);
}
.acct-person { width: 16px; height: 16px; flex: none; display: block; }
.acct-button:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--ink); }
.acct-button[aria-expanded="true"] { background: var(--sun); }
.acct-button.signed-in { padding: 0; border: none; background: none; box-shadow: none; border-radius: 50%; }
.acct-button.signed-in .acct-person, .acct-button.signed-in .acct-label { display: none; }
.acct-button.signed-in .avatar { box-shadow: 3px 3px 0 var(--ink); }
.acct-button.signed-in:active { transform: none; }
.acct-button.signed-in:active .avatar { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--ink); }
.acct-button.signed-in[aria-expanded="true"] .avatar { box-shadow: 0 0 0 3px var(--sun), 3px 3px 0 3px var(--ink); }
@media (hover: hover) and (pointer: fine) {
  .acct-button:not(.signed-in):hover { transform: var(--lift); box-shadow: 4px 4px 0 var(--ink); background: var(--cream); }
  .acct-button:not(.signed-in)[aria-expanded="true"]:hover { background: var(--sun); }
  .acct-button.signed-in:hover .avatar { transform: var(--lift); box-shadow: 4px 4px 0 var(--ink); }
}
.avatar[hidden] { display: none; }
.avatar.sm { width: 36px; height: 36px; font-size: 14px; box-shadow: none; }
.avatar.md { width: 54px; height: 54px; font-size: 21px; box-shadow: none; }

.acct-menu {
  position: absolute;
  top: calc(100% + 12px);
  right: 0;
  z-index: 60;
  width: 304px;
  max-height: calc(100dvh - 96px);
  overflow-y: auto;
  padding: 16px;
  background: var(--paper);
  border: 2px solid var(--ink);
  border-radius: 15px;
  box-shadow: 5px 5px 0 var(--ink);
}
.acct-menu[hidden], #menuOut[hidden], #menuIn[hidden], .avatar-pick[hidden], .google-btn[hidden], .menu-copy[hidden] { display: none; }
#menuOut, #menuIn { display: flex; flex-direction: column; gap: 12px; }
#menuOut .google-btn { align-self: flex-start; }
.menu-title { font-family: var(--font-display); font-weight: 800; font-size: 18px; letter-spacing: -.02em; color: var(--ink); }
.menu-copy { font-size: 13.5px; line-height: 1.45; color: var(--body-text); }
.menu-head { display: flex; align-items: center; gap: 13px; }
.avatar-edit { position: relative; flex: none; padding: 0; border: none; border-radius: 50%; background: none; line-height: 0; }
.avatar-pen {
  position: absolute;
  right: -4px;
  bottom: -4px;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: var(--sun);
  color: var(--ink);
}
.avatar-pen svg { width: 11px; height: 11px; }
.avatar-edit[aria-expanded="true"] .avatar-pen { background: var(--ink); color: var(--on-loud); }
.avatar-pick { display: flex; flex-direction: column; gap: 10px; padding: 12px; border: 2px solid var(--hairline); border-radius: 12px; background: var(--paper-warm); }
.avatar-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; justify-items: center; }
.avatar-choice { padding: 3px; border: 2px solid transparent; border-radius: 50%; background: none; line-height: 0; }
.avatar-choice[aria-pressed="true"] { border-color: var(--ink); background: var(--sun); }
@media (hover: hover) and (pointer: fine) {
  .avatar-choice:hover { border-color: var(--dashed); }
  .avatar-choice[aria-pressed="true"]:hover { border-color: var(--ink); }
  .menu-item:hover { background: var(--cream); }
}
.menu-stats { padding: 8px 11px; border-radius: 10px; background: var(--desk); font-family: var(--font-mono); font-size: 11px; letter-spacing: .03em; color: var(--gold-dim); }
.menu-list { display: flex; flex-direction: column; padding-top: 6px; border-top: 2px solid var(--hairline); }
.menu-item { padding: 9px 10px; border: none; border-radius: 10px; background: none; text-align: left; font-size: 14px; font-weight: 600; color: var(--ink); }
#menuSync:empty { display: none; }

/* The avatars: a key from the grid with its number, set in tree rings. Each
   note has its own grain, from the room's wood and the instrument's green,
   and the root is the one loud face, as it is on the grid. */
.avatar[data-note] {
  --a: var(--wood-pale); --b: var(--wood-line); --face: var(--key-birch); --lab: var(--ink);
  --x: 42%; --y: 40%; --w: 4px;
  background: repeating-radial-gradient(circle at var(--x) var(--y), var(--a) 0 var(--w), var(--b) var(--w) calc(var(--w) + 1.5px));
}
.av-face {
  width: 58%;
  height: 58%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: var(--face);
  color: var(--lab);
  box-shadow: 2px 2px 0 var(--ink);
  font-family: var(--font-display);
  font-size: .72em;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -.02em;
}
.av-face small { align-self: flex-start; margin-top: 22%; font-size: .62em; }
.avatar.sm .av-face { border-width: 1.5px; box-shadow: 1px 1px 0 var(--ink); }
.avatar[data-note="0"]  { --a: var(--sun); --b: var(--sun-lift); --face: var(--sun-lit); --lab: var(--on-loud); --x: 40%; --y: 38%; --w: 5px; }
.avatar[data-note="1"]  { --a: var(--wood-pale); --b: var(--wood-line); --face: var(--key-sharp); --x: 60%; --y: 35%; --w: 3px; }
.avatar[data-note="2"]  { --a: var(--wood-pale); --b: var(--wood-soft); --x: 35%; --y: 62%; }
.avatar[data-note="3"]  { --a: var(--desk); --b: var(--staffline); --face: var(--key-sharp); --x: 64%; --y: 60%; --w: 3.5px; }
.avatar[data-note="4"]  { --a: var(--cream); --b: var(--wood-soft); --x: 50%; --y: 30%; --w: 6px; }
.avatar[data-note="5"]  { --a: var(--key-sharp); --b: var(--sun); --x: 30%; --y: 45%; }
.avatar[data-note="6"]  { --a: var(--wood-pale); --b: var(--wood); --face: var(--key-sharp); --x: 55%; --y: 55%; --w: 5px; }
.avatar[data-note="7"]  { --a: var(--sun); --b: var(--pressed); --x: 45%; --y: 65%; }
.avatar[data-note="8"]  { --a: var(--cream-deep); --b: var(--wood-soft); --face: var(--key-sharp); --x: 70%; --y: 42%; --w: 3px; }
.avatar[data-note="9"]  { --a: var(--desk); --b: var(--sun); --x: 38%; --y: 30%; --w: 5px; }
.avatar[data-note="10"] { --a: var(--wood-soft); --b: var(--wood); --face: var(--key-sharp); --x: 58%; --y: 68%; }
.avatar[data-note="11"] { --a: var(--paper-warm); --b: var(--wood-line); --face: var(--sun); --x: 48%; --y: 48%; --w: 2.5px; }

@media (max-width: 760px) {
  .acct-label { display: none; }
  .acct-button:not(.signed-in) { padding: 7px; box-shadow: none; }
  .acct-button.signed-in .avatar { box-shadow: none; }
  .avatar.sm { width: 34px; height: 34px; }
  /* A phone has no room beside the button, so the menu spans the screen. */
  .acct-wrap { position: static; }
  .acct-menu { position: fixed; top: 66px; left: 10px; right: 10px; width: auto; max-height: calc(100dvh - 150px); }
}

/* ─── Privacy and terms pages ───────────────────────────────────────────── */
/* The app's frame fixes the page to the window; a document scrolls. */
html:has(> body.legal-page), body.legal-page { height: auto; overflow: auto; }
body.legal-page { background: var(--paper-warm); }
.legal { max-width: 680px; margin: 0 auto; padding: 56px 24px 96px; color: var(--body-text); font-size: 15.5px; line-height: 1.65; }
.legal h1 { font-family: var(--font-display); font-size: 44px; letter-spacing: -.03em; color: var(--ink); margin: 28px 0 10px; }
.legal h2 { font-family: var(--font-display); font-size: 20px; letter-spacing: -.02em; color: var(--ink); margin: 36px 0 8px; }
.legal a { color: var(--ink); }
.legal ul { padding-left: 20px; }
.legal li { margin: 6px 0; }
.legal code { font-family: var(--font-mono); font-size: 13px; }
.legal-lede { font-size: 18px; color: var(--ink); }
.legal-date { font-family: var(--font-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.legal-back { font-family: var(--font-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; }

@media (max-width: 760px) {
  #focusToggle span:not(.icon-square) { display: none; }
  #focusToggle { padding: 8px; }
  .set-row { flex-direction: column; align-items: stretch; gap: 5px; }
  .set-row select, .set-row input[type="number"] { width: 100%; }
}

/* ─── Key picker ─────────────────────────────────────────────────────────── */

#keyPick {
  min-width: 84px;
  padding: 0 26px 0 12px;
  height: 34px;
  border: none;
  background: transparent;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 15px;
  color: var(--ink);
  text-align: center;
  text-align-last: center;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='6'%3E%3Cpath d='M1 1l3.5 3.5L8 1' stroke='%231A1814' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 9px center;
}
#keyPick:focus-visible { outline: 3px solid var(--gold-text); outline-offset: -3px; }
@media (hover: hover) and (pointer: fine) { #keyPick:hover { background-color: var(--cream); } }

/* ─── Keys-mastered wheel: never let it push out of its card ─────────────── */

.keys-main { flex-wrap: wrap; }
.key-wheel { max-width: 100%; }
@media (max-width: 1279px) {
  /* The 226px wheel plus copy does not fit a one-column You screen. */
  .keys-main { flex-direction: column; align-items: flex-start; }
  .keys-copy { width: 100%; }
}

/* ─── You screen below the desktop tier: stack and scroll ────────────────── */
/* Two columns plus a 226px wheel do not fit; clipping it hid real content. */
@media (max-width: 1279px) {
  body.screen-progress #progress-screen { display: block; overflow-y: auto; padding: 18px; }
  .keys-card, .badges-card { margin-top: 16px; }
}
@media (max-width: 760px) {
  body.screen-progress #progress-screen { padding: 14px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Phone companion, per the drawn spec: notation strip on top, the key field
   filling the middle, the three label pills as a bar above the tab rail.
   The pills used to live only in the left panel, which is hidden here, so on a
   phone there was no way to switch labels at all.
   ═════════════════════════════════════════════════════════════════════════ */

@media (max-width: 760px) {
  #content { flex-direction: column; min-height: 0; }

  /* The Grove's panel is a drop-down above the keys rather than a column
     beside them: a phone has no room beside them, and hiding it left the
     chords, the scales and the voice out of reach. Open to begin with, it
     takes at most two fifths of the screen and scrolls inside that; the bar
     at its top folds it away and the keys take the room back. */
  #panel-left {
    order: 0;
    width: auto;
    max-height: 42dvh;
    padding: 0 14px 14px;
    gap: 14px;
    border-right: none;
    border-bottom: 2px solid var(--ink);
  }
  .panel-toggle { display: flex; }
  body.phone-panel-shut #panel-left { max-height: none; overflow: visible; padding-bottom: 0; }
  body.phone-panel-shut #panel-left > :not(.panel-toggle) { display: none !important; }
  body.phone-panel-shut .panel-toggle .chev { transform: translateY(-3px) rotate(45deg); }
  #panel-left .chord-grid { grid-template-columns: repeat(4, 1fr); }

  /* The notation is the strip above the keys, and it carries its own
     controls, so a phone needs nothing beside it. The instrument keeps a
     floor of its own, so no future addition to the strip can squeeze it
     flat: an earlier version left 16px of canvas. */
  body.screen-song #lane { order: 1; }
  body.screen-song #stage-wrap { min-height: 40dvh; }
  /* No physical keyboard on a phone, so the layer that names its keys has
     nothing to say. */
  .label-pills .pill.layer-keyboard { display: none; }

  #stage { order: 2; }
  body.screen-song #stage-wrap { flex: 1; height: auto; border-top: 2px solid var(--ink); background: var(--paper-warm); }
  body.screen-progress #panel-left { display: none !important; }
}

@media (max-width: 760px) {
  #screenTitle, .song-pick {
    font-size: 16px;
    max-width: 34vw;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .song-pick { padding: 2px 7px 2px 2px; gap: 5px; }
  /* The combo chip and the song picker cannot both have the room they want on
     a phone, and the chip is a scoreboard for a screen you are reading. */
  body.screen-song #headerCombo { display: none; }
  #topbar { gap: 6px; padding: 0 10px; }
  #focusToggle, #settingsToggle { padding: 7px; box-shadow: none; }
  #keyPick { min-width: 62px; font-size: 14px; }
  .key-stepper button { width: 26px; }
}

/* On a phone there is no room to dock beside the instrument, so settings takes
   the whole content area instead of squeezing the keys into a sliver. */
@media (max-width: 760px) {
  body.settings-open #panel-settings {
    order: 0;
    width: auto;
    flex: 1;
    border-left: none;
  }
  body.settings-open #stage,
  body.settings-open:not(.screen-progress) #panel-left { display: none !important; }
}

/* At 320px a label beside its control does not fit; the H/V step pair was
   running off the edge. Stack them inside the docked panel. */
#panel-settings .set-row { flex-direction: column; align-items: stretch; gap: 5px; }
#panel-settings .set-row label { flex: none; }
#panel-settings .set-row select,
#panel-settings .set-row input[type="number"] { width: 100%; }
#panel-settings .set-pair { grid-template-columns: 1fr 1fr; gap: 10px; }
#panel-settings .set-pair .set-row select,
#panel-settings .set-pair .set-row input { width: 100%; }

/* Below the desktop tier there is not room for the rail, the left panel, the
   instrument AND a 320px settings column: iPad portrait was left with a 156px
   stage. The left panel yields while settings is open, the same way Focus
   clears it, so the instrument keeps usable width. */
@media (min-width: 761px) and (max-width: 1023px) {
  body.settings-open:not(.screen-progress) #panel-left { display: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Collapsible panel sections, chord diagrams, the scale list, the floating
   label pills and the hints strip. All from the drawn direction.
   ═════════════════════════════════════════════════════════════════════════ */

.section-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.collapse-btn {
  width: 20px; height: 20px;
  flex: none;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  position: relative;
}
/* A minus that becomes a plus, so the control states which way it goes. */
.collapse-btn::before,
.collapse-btn::after {
  content: '';
  position: absolute;
  inset: 50% 3px auto 3px;
  height: 2px;
  margin-top: -1px;
  background: var(--ink);
  transition: transform .16s var(--ease-press);
}
.collapse-btn::after { transform: rotate(-90deg); opacity: 0; }
.collapsed .collapse-btn::after { opacity: 1; transform: rotate(0deg); }
.collapsed .collapse-btn::before { transform: rotate(90deg); }
.collapsible.collapsed .section-body { display: none; }
@media (hover: hover) and (pointer: fine) { .collapse-btn:hover { background: var(--sun); } }

/* With both sections shut the panel only needs room for its headings, and the
   difference goes to the instrument. */
body.panel-compact #panel-left { width: 168px; }

/* Scales read as a list once there are seven of them. */
.scale-list { display: flex; flex-direction: column; gap: 5px; }
.scale-row {
  padding: 8px 12px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--paper);
  font-size: 13px;
  font-weight: 500;
  color: var(--ink);
  text-align: left;
  transition: background .14s, box-shadow .14s, transform .14s var(--ease-press);
}
.scale-row.active { background: var(--sun); font-weight: 700; box-shadow: 3px 3px 0 var(--ink); }
@media (hover: hover) and (pointer: fine) {
  .scale-row:hover:not(.active) { transform: var(--lift); box-shadow: 2px 2px 0 var(--ink); }
  .scale-row.active:hover { transform: var(--lift); box-shadow: 4px 4px 0 var(--ink); }
}

/* Chord buttons carry the shape the chord makes on the grid. A roman numeral
   alone says nothing; the shape is the thing that transfers to every key. */
.chord-grid { grid-template-columns: repeat(3, 1fr); gap: 7px; }
.chord-btn { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 7px 0 5px; }
.chord-dots { width: 40px; height: 34px; display: block; }
.chord-name { font-size: 12.5px; font-weight: 700; line-height: 1; }
/* Where in the key the chord last sounded: I, then IV, then V. */
.chord-deg { font-family: var(--font-mono); font-size: 10.5px; line-height: 1; opacity: .85; min-height: 10px; }

/* Label pills float over the stage, bottom right, opposite the view tools. */
.label-pills {
  position: absolute;
  right: 14px; bottom: 14px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  z-index: 6;
}
.label-pills .pill { background: var(--paper); box-shadow: 3px 3px 0 var(--ink); text-align: center; }
.label-pills .pill.active { background: var(--sun); }

/* Each layer toggle carries its layer's own colour, so the control and the
   labels it turns on are visibly the same thing. An active chip is painted in
   the layer colour with mist text rather than the shared gold, because three
   gold chips in a row said nothing about which layer was which. */
.label-pills .pill { border: 2px solid var(--ink); }
.label-pills .pill.active { box-shadow: 3px 3px 0 var(--ink); color: var(--paper); }
.label-pills .pill.layer-numbers.active  { background: var(--layer-numbers); }
.label-pills .pill.layer-notes.active    { background: var(--layer-notes); }
.label-pills .pill.layer-keyboard.active { background: var(--layer-keyboard); }
/* Off is the quiet state: mist face, ink text, and a stub of the layer colour
   so you can still tell what turning it on would give you. */
.label-pills .pill:not(.active) { background: var(--paper); color: var(--ink); }
.label-pills .pill.layer-numbers:not(.active)  { border-left: 6px solid var(--layer-numbers); }
.label-pills .pill.layer-notes:not(.active)    { border-left: 6px solid var(--layer-notes); }
.label-pills .pill.layer-keyboard:not(.active) { border-left: 6px solid var(--layer-keyboard); }

/* The last layer on cannot be switched off; the refusal is a nudge, not a jump. */
.label-pills .pill.refused { animation: nudge 220ms var(--ease-press); }
@keyframes nudge {
  0%, 100% { transform: translateX(0); }
  30%      { transform: translateX(-3px); }
  70%      { transform: translateX(3px); }
}

/* First-run hints, dismissible and then gone for good. */
#hints {
  position: absolute;
  left: 50%; bottom: 14px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 16px;
  max-width: calc(100% - 220px);
  padding: 9px 12px 9px 16px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--cream);
  box-shadow: 3px 3px 0 var(--ink);
  font-size: 12.5px;
  color: var(--body-text);
  z-index: 6;
}
#hints .hint { white-space: nowrap; }
#hints .hint b { color: var(--ink); font-weight: 700; }
#hintsClose {
  width: 20px; height: 20px;
  flex: none;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  font-size: 10px;
  line-height: 1;
  color: var(--ink);
}
@media (hover: hover) and (pointer: fine) { #hintsClose:hover { background: var(--sun); } }
body.hints-done #hints, body.screen-progress #hints { display: none; }

@media (max-width: 1100px) { #hints .hint:nth-child(3) { display: none; } }
@media (max-width: 860px) { #hints .hint:nth-child(2) { display: none; } }
@media (max-width: 760px) {
  /* On a phone the pills become the bar the drawn companion shows, and the
     hints would crowd a screen that is mostly keys. */
  #hints { display: none; }
  .label-pills {
    /* A real row in the stage's flex column rather than an overlay, so a tap
       near the bottom of the grid hits a key instead of being ambiguous. Two
       things have to be undone for that: the base rule's absolute positioning,
       and its right/bottom offsets, which as a relative element would shift it
       out of where the flow put it. */
    position: relative;
    inset: auto;
    z-index: 2;
    order: 4;
    flex: none;
    flex-direction: row;
    justify-content: center;
    gap: 8px;
    padding: 9px 12px;
    border-top: 2px solid var(--ink);
    background: var(--paper-warm);
  }
  .label-pills .pill { flex: 1; max-width: 130px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   The forest
   Everything here sits behind the instrument and must never compete with it,
   so it is built from three cheap layers: flat drawn conifers, two slow
   gradient washes for dappled light, and eight drifting motes. No filters, no
   blend modes, no per-frame script; the canvas needs that budget.
   ═══════════════════════════════════════════════════════════════════════ */
#forest {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  border-radius: inherit;
}
/* The instrument and every control sit above it. #forest is the only child of
   #stage that is allowed below; naming the wrapper explicitly is what makes
   that true, since an earlier version pointed at `#stage > canvas` and the
   canvas is a grandchild, so nothing was ordered at all and the atmosphere
   painted over the keys. */
#stage > #stage-wrap { position: relative; z-index: 1; }
#stage > #lane { position: relative; z-index: 1; }
#stage > #progress-screen { position: relative; z-index: 1; }

/* The ground the conifers stand on, and the one place warmth is allowed
   behind the instrument. A single wash rising from the bottom edge: no
   texture, no seams, nothing that could be mistaken for a key. It is held
   under a sixth of full strength for the same reason the canopy is. */
.forest-floor {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 38%;
  background: linear-gradient(180deg, transparent 0%, var(--wood-soft) 58%, var(--wood) 86%, var(--bark) 100%);
  opacity: 0.15;
}

/* Receding conifers along the horizon, breathing very slightly so the scene
   is never quite still. 5% opacity: present as depth, invisible as detail. */
.forest-far {
  position: absolute;
  left: -4%; right: -4%; bottom: 0;
  width: 108%; height: 46%;
  color: var(--ink);
  opacity: 0.055;
  animation: canopySway 34s ease-in-out infinite;
  transform-origin: 50% 100%;
}
.forest-far .tree-a { opacity: 1; }
.forest-far .tree-b { opacity: 0.7; }
@keyframes canopySway {
  0%, 100% { transform: scaleY(1) translateX(0); }
  50%      { transform: scaleY(1.012) translateX(-6px); }
}

/* Dappled light. Two offset washes drifting on different periods, which is
   what keeps the pattern from reading as a loop. */
.dapple {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(circle, var(--sun) 0%, transparent 68%);
  opacity: 0.10;
  mix-blend-mode: normal;
}
.dapple-1 {
  width: 46vw; height: 46vw; top: -12%; left: 4%;
  animation: drift1 41s ease-in-out infinite;
}
.dapple-2 {
  width: 34vw; height: 34vw; bottom: -14%; right: 2%;
  animation: drift2 53s ease-in-out infinite;
  opacity: 0.075;
}
@keyframes drift1 {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate(5%, 4%) scale(1.09); }
}
@keyframes drift2 {
  0%, 100% { transform: translate(0, 0) scale(1.05); }
  50%      { transform: translate(-6%, -3%) scale(1); }
}

/* Motes in the light shaft. Each carries its own column, delay and duration
   from inline custom properties, so eight elements share one keyframe. */
.motes { position: absolute; inset: 0; }
.motes i {
  position: absolute;
  left: var(--x);
  bottom: -14px;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--gold-text);
  opacity: 0;
  animation: mote var(--t) linear var(--d) infinite;
}
@keyframes mote {
  0%       { transform: translateY(0) translateX(0); opacity: 0; }
  12%      { opacity: 0.5; }
  70%      { opacity: 0.34; }
  100%     { transform: translateY(-84vh) translateX(26px); opacity: 0; }
}

/* Needles falling, the counterpart to the motes rising. Rotated slivers rather
   than dots, so they read as pine rather than as snow. */
.needles { position: absolute; inset: 0; }
.needles i {
  position: absolute;
  left: var(--x);
  top: -18px;
  width: 2px; height: 11px;
  border-radius: 1px;
  background: var(--q-minor);
  opacity: 0;
  animation: needle var(--t) linear var(--d) infinite;
}
@keyframes needle {
  0%   { transform: translateY(0) translateX(0) rotate(12deg); opacity: 0; }
  10%  { opacity: 0.30; }
  80%  { opacity: 0.22; }
  100% { transform: translateY(88vh) translateX(var(--sway)) rotate(190deg); opacity: 0; }
}

/* Ferns in the near corners, the only elements drawn at real contrast, and
   still only just. The right one is mirrored rather than drawn twice. */
.fern {
  position: absolute;
  bottom: -6px;
  width: 128px; height: 128px;
  color: var(--ink);
  opacity: 0.085;
}
.fern-left  { left: -10px; animation: frond 26s ease-in-out infinite; transform-origin: 0% 100%; }
.fern-right { right: -10px; transform: scaleX(-1); animation: frond 31s ease-in-out infinite reverse; transform-origin: 100% 100%; }
@keyframes frond {
  0%, 100% { rotate: 0deg; }
  50%      { rotate: -2.2deg; }
}

/* The forest is atmosphere, not information, so reduced motion removes the
   movement entirely rather than merely shortening it. The scene stays. */
@media (prefers-reduced-motion: reduce) {
  .forest-far, .dapple, .fern { animation: none !important; }
  .motes, .needles { display: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   Chord quality colours
   One colour per idea, used everywhere that idea appears: on the vocabulary
   buttons, on the chords the chosen scale contains, and on the keys of the
   grid while that chord sounds. The whole face of the button is the colour,
   with the name in pale on it: a bar along the top was a category label
   that had to be looked for. Quality is never signalled by colour alone,
   since the name and the dot pattern both say it too, which is what keeps
   the panel usable without hue discrimination.
   ═══════════════════════════════════════════════════════════════════════ */
.chord-btn { position: relative; overflow: hidden; }
.chord-btn.q-major, .chord-btn.q-minor, .chord-btn.q-dim, .chord-btn.q-aug,
.chord-btn.q-sus, .chord-btn.q-dom, .chord-btn.q-other { color: var(--on-loud); }
.chord-btn.q-major { background: var(--q-major); }
.chord-btn.q-minor { background: var(--q-minor); }
.chord-btn.q-dim   { background: var(--q-dim); }
.chord-btn.q-aug   { background: var(--q-aug); }
.chord-btn.q-sus   { background: var(--q-sus); }
.chord-btn.q-dom   { background: var(--q-dom); }
.chord-btn.q-other { background: var(--q-other); }
/* The dots stay legible on a coloured face: paper dots, the root in sun. */
.chord-btn[class*=" q-"] .chord-dots circle { stroke: var(--on-loud); }
@media (hover: hover) and (pointer: fine) {
  .chord-btn[class*=" q-"]:hover { background-color: var(--ink); }
}

/* Progressions: a strip of the chords it will play, each dot in that
   chord's quality, over the degrees it is made of. */
.prog-grid { grid-template-columns: repeat(2, 1fr); }
.prog-btn { gap: 5px; padding: 8px 0 6px; }
.prog-strip { display: flex; gap: 4px; }
.prog-dot {
  display: block;
  width: 11px; height: 11px;
  border-radius: 999px;
  border: 1.5px solid var(--ink);
  background: var(--q-other);
}
.prog-dot.q-major { background: var(--q-major); }
.prog-dot.q-minor { background: var(--q-minor); }
.prog-dot.q-dim   { background: var(--q-dim); }
.prog-dot.q-aug   { background: var(--q-aug); }
.prog-dot.q-sus   { background: var(--q-sus); }
.prog-dot.q-dom   { background: var(--q-dom); }

/* ─── Inversions ──────────────────────────────────────────────────────────── */
.inversion-block .mono-label { margin-bottom: 7px; }
.inv-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; }
.inv-btn {
  padding: 6px 0;
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--ink);
  background: var(--paper);
  border: 2px solid var(--ink);
  border-radius: 8px;
  cursor: pointer;
}
.inv-btn.active { background: var(--sun); font-weight: 700; box-shadow: 2px 2px 0 var(--ink); }
.inv-note { font-size: 11.5px; color: var(--muted); margin-top: 6px; line-height: 1.35; }
@media (hover: hover) and (pointer: fine) {
  .inv-btn:hover:not(.active) { background: var(--cream-deep); transform: var(--lift); }
}

.discovery-count { color: var(--gold-text); }

/* ══════════════════════════════════════════════════════════════════════════
   The trail: number notation, scrolling
   Jianpu across the full width of the stage. The old five-line staff lived in
   a 250px side panel, which is not enough length for notation to be read
   ahead of; this gets the whole stage width and gives the reader about a bar
   and a half of warning before each note lands.

   The strip is laid out once at absolute positions and moved by a single
   transform per frame, so the per-frame cost does not grow with the number of
   notes on screen.
   ═══════════════════════════════════════════════════════════════════════ */
/* The staff sits beside the instrument rather than above it, sharing its
   vertical pitch axis: an octave line is drawn at the screen y of the root
   keys on the grid, so a 1 in the notation is level with the 1s you play.
   That alignment is the whole reason it moved here.

   It takes the larger half of the screen, and it took the side panel's width
   as well: reading ahead is the whole job of this screen, and a picker in the
   title costs nothing to keep a library in. */
#lane {
  --lane-dy: 0px;
  flex: 1 1 58%;
  min-width: 340px;
  position: relative;
  display: none;
  flex-direction: column;
  border-left: 2px solid var(--ink);
  background: var(--paper-warm);
  overflow: hidden;
}
body.screen-song #stage { flex-direction: row; }
body.screen-song #lane { display: flex; }
body.screen-song #stage-wrap { flex: 1 1 42%; min-width: 300px; }

/* The Trail is driven from the strip above its own notation, so nothing has
   to sit beside it. */
.lane-head {
  flex: none;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 12px;
  border-bottom: 2px solid var(--hairline);
  background: var(--paper);
  overflow-x: auto;
  scrollbar-width: none;
}
.lane-head::-webkit-scrollbar { display: none; }
.lane-head-gap { flex: 1; min-width: 4px; }
.lane-head .mode-row, .lane-head .parts-row { margin: 0; flex: none; }
.lane-head .ghost-btn, .lane-head .mode-btn { white-space: nowrap; }
.lane-barcount {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
/* How far through the song you are, as a hairline under the controls rather
   than a track taking a row of its own. */
.lane-progress {
  flex: none;
  height: 4px;
  border: 0;
  border-bottom: 1.5px solid var(--hairline);
  border-radius: 0;
  background: var(--paper);
}

/* The key declaration, exactly as the reference writes it. */
.lane-key {
  flex: none;
  display: inline-flex;
  align-items: center;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
  background: var(--paper);
  border: 2px solid var(--ink);
  border-radius: 7px;
  padding: 3px 8px;
  white-space: nowrap;
}

.lane-body { position: relative; flex: 1; min-height: 0; }
.lane-lines { position: absolute; inset: 0; }

/* One line per octave of the tonic. The tonic's own line is heavier, so the
   octave being read from is findable at a glance. */
.lane-doline {
  position: absolute;
  left: 0; right: 0;
  height: 0;
  border-top: 1.5px solid var(--staffline);
}
.lane-doline.tonic { border-top: 2px solid var(--dashed); }

/* Where the note sounds. Its left edge is set from LANE_STRIKE_X in ui.js,
   the constant the notes are placed by: a second copy here had drifted 20px
   off the notes it was meant to meet. */
.lane-strike {
  position: absolute;
  top: 0; bottom: 0;
  width: 0;
  margin-left: -1.25px;
  border-left: 2.5px dashed var(--gold-text);
  z-index: 2;
}

.lane-scroll {
  position: absolute;
  inset: 0;
  will-change: transform;
}
/* Advancing to the next note is a scroll, not a cut. Playing the note on the
   line used to teleport the next one onto it, which reads as a flicker rather
   than as the music moving past you. Playback clears this, since a frame loop
   is already moving smoothly and a transition would fight it. Reduced motion
   is covered by the blanket policy above. */
.lane-scroll.gliding { transition: transform .2s var(--ease-press); }
/* The staff follows the instrument when the instrument is panned: one number
   for the whole axis, so it is a transform rather than a relayout. */
.lane-lines { transform: translateY(var(--lane-dy, 0px)); }

.lane-bar {
  position: absolute;
  top: 14px; bottom: 14px;
  width: 0;
  border-left: 1.5px solid var(--hairline);
}

.lane-note {
  position: absolute;
  transform: translate(-50%, -50%);
  min-width: 26px;
  padding: 3px 6px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 15px;
  font-weight: 600;
  line-height: 1;
  color: var(--ink);
  background: var(--paper);
  border: 2px solid var(--ink);
  border-radius: 7px;
  white-space: nowrap;
}
/* A ring means a half note or longer, and a trailing dot means dotted. Both
   are duration rather than octave, which is the reference's convention: the
   octave is already carried by how high the note sits. */
.lane-note.long { border-radius: 999px; border-width: 2.5px; }
.lane-note.dotted::after {
  content: '';
  position: absolute;
  right: -7px; top: 50%;
  width: 4px; height: 4px;
  margin-top: -2px;
  border-radius: 50%;
  background: var(--ink);
}
/* How long the note is held, drawn as a tail so a half note looks twice a
   quarter instead of only being circled. */
.lane-tail {
  position: absolute;
  left: 100%; top: 50%;
  height: 4px;
  margin-top: -2px;
  background: var(--faint);
  border-radius: 0 2px 2px 0;
  z-index: -1;
}

/* The moment reading and hearing line up: the note that just sounded swells
   off the line and fades. */
@keyframes laneHit {
  0%   { transform: translate(-50%, -50%) scale(1); background: var(--sun); }
  45%  { transform: translate(-50%, -50%) scale(1.55); background: var(--sun); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(2.1); background: var(--sun); opacity: 0; }
}
.lane-note.hit { animation: laneHit .62s var(--ease-press) forwards; z-index: 4; }
/* Behind the line and already played: still legible if you look back, but not
   competing with what is still coming. */
.lane-note.done { opacity: 0.3; border-color: var(--faint); }
/* You play: a note that went past the line unplayed. Dashed, so it reads as
   missed and not merely as already played, without colour alone saying so. */
.lane-note.missed { opacity: 0.45; border-style: dashed; color: var(--muted); }

/* On a phone the lane is the notation, so it keeps its height but loses the
   bar lines and tails, which are unreadable at that width. */
/* On a phone there is no room beside the instrument, so the staff goes back
   above it, full width. */
@media (max-width: 760px) {
  body.screen-song #stage { flex-direction: column; }
  #lane { width: auto; min-width: 0; flex: none; height: 34dvh; border-left: 0; border-bottom: 2px solid var(--ink); }
  /* Three modes, Hard and Sheet do not fit one row at phone width, and a
     row that scrolls sideways hides the last of them. Two rows instead. */
  .lane-head { gap: 7px; padding: 6px 10px; flex-wrap: wrap; overflow-x: visible; }
  .lane-head-gap { display: none; }
  .lane-barcount { display: none; }
  .lane-bar, .lane-tail { display: none; }
  .lane-note { font-size: 13px; min-width: 22px; }
}


/* The chord shape settles in when you hover it, one dot at a time, so the
   diagram reads as a shape being placed on the grid rather than a static
   icon. Staggered from the root outward, which is the order you would play
   it in. Reduced motion is handled by the blanket policy above. */
@media (hover: hover) and (pointer: fine) {
  .chord-btn:hover .chord-dots circle { animation: dotSettle .34s var(--ease-press) both; }
  .chord-btn:hover .chord-dots circle:nth-child(2) { animation-delay: .05s; }
  .chord-btn:hover .chord-dots circle:nth-child(3) { animation-delay: .10s; }
  .chord-btn:hover .chord-dots circle:nth-child(4) { animation-delay: .15s; }
}
@keyframes dotSettle {
  0%   { transform: scale(.35); opacity: .25; }
  60%  { transform: scale(1.18); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.chord-dots circle { transform-box: fill-box; transform-origin: center; }

/* ─── Trail: the mode switch ──────────────────────────────────────────────────
   Follow waits for you to play the note; Listen plays it for you. Following is
   the default, so the transport controls are hidden in that mode: there is
   nothing to start, and a play button that does nothing is worse than none. */
.mode-row { display: grid; grid-template-columns: repeat(3, auto); gap: 6px; margin-top: 12px; }
.mode-btn {
  padding: 7px 10px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--ink);
  background: var(--paper);
  border: 2px solid var(--ink);
  border-radius: 8px;
  cursor: pointer;
}
.mode-btn.active { background: var(--sun); font-weight: 700; box-shadow: 2px 2px 0 var(--ink); }
@media (hover: hover) and (pointer: fine) {
  .mode-btn:hover:not(.active) { background: var(--cream-deep); transform: var(--lift); }
}

/* The instrument takes the whole left of the Trail now that the controls and
   the notation both live on the right. */
body.screen-song #panel-left { display: none; }

/* The running count for Discovery mode, beside the setting that turns it on. */
.set-note { font-size: 11.5px; color: var(--muted); margin: 4px 0 0 26px; min-height: 14px; }

/* ─── Trail: imported scores ──────────────────────────────────────────────────
   A score brings chords, rests, bar numbers and dynamics onto the lane, parts
   to choose between, and the transcriber's printed sheet. */

/* The key and the metre, side by side in the declaration box. */
.lane-meter {
  margin-left: 8px;
  padding-left: 8px;
  border-left: 1.5px solid var(--staffline);
  color: var(--muted);
}

.lane-barnum {
  position: absolute;
  top: -12px; left: 3px;
  font-family: var(--font-mono);
  font-size: 9px;
  color: var(--muted);
  white-space: nowrap;
}
.lane-bar.end { border-left-width: 4px; border-left-color: var(--dashed); }

/* A rest sits on the middle of the staff, as the transcriber draws it, quiet
   enough not to read as a note to play. */
.lane-rest {
  position: absolute;
  transform: translate(-50%, -50%);
  color: var(--muted);
  line-height: 0;
}

.lane-dyn {
  position: absolute;
  bottom: 8px;
  transform: translateX(-50%);
  font-family: Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-weight: 700;
  font-size: 14px;
  color: var(--body-text);
}

/* Library heading, with the import control beside it. */
.songs-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.mini-btn { padding: 5px 11px; font-size: 12px; }
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* An imported score is marked by its chip and can be removed from the list. */
.song-item { position: relative; }
.song-row.imported { padding-right: 40px; }
.song-row.imported .song-chip { background: var(--gold-text); color: var(--on-loud); font-size: 13px; }
.song-remove {
  position: absolute;
  top: 50%; right: 9px;
  width: 26px; height: 26px;
  margin-top: -13px;
  display: grid;
  place-items: center;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
}
.song-remove svg { width: 12px; height: 12px; }
@media (hover: hover) and (pointer: fine) {
  .song-remove:hover { background: var(--cream-deep); }
}

/* Parts: one pill per staff or instrument, any number lit. */
.parts-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.parts-row[hidden] { display: none; }
.part-pill {
  padding: 5px 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--muted);
  background: var(--paper);
  border: 2px dashed var(--dashed);
  border-radius: 999px;
}
.part-pill.active { color: var(--ink); background: var(--sun); border: 2px solid var(--ink); font-weight: 700; }

/* ─── The sheet dialog ──────────────────────────────────────────────────────
   The pages are paper, so they stay white: they are a preview of what prints,
   not app surface. */
#sheet-overlay { position: fixed; inset: 0; z-index: 110; display: flex; align-items: center; justify-content: center; padding: 24px; }
.sheet-card {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(1040px, 100%);
  max-height: calc(100dvh - 48px);
  background: var(--paper);
  border: 2px solid var(--ink);
  border-radius: 20px;
  box-shadow: 8px 8px 0 var(--ink);
  overflow: hidden;
}
.sheet-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  padding: 18px 22px 14px;
  border-bottom: 2px solid var(--ink);
  background: var(--paper-warm);
}
.sheet-head h2 { margin: 4px 0 0; font-family: var(--font-display); font-weight: 800; font-size: 22px; letter-spacing: -.03em; color: var(--ink); }
.sheet-meta { margin-top: 3px; font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.sheet-actions { display: flex; align-items: center; gap: 10px; }
.sheet-print { background: var(--sun); box-shadow: 3px 3px 0 var(--ink); }
.sheet-print:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--ink); }
.sheet-pages {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 22px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  /* The tray the pages lie on is the desk, so it is warm: --desk is an
     instrument colour (an out-of-scale key) and only ever looked right here
     by coincidence of its name. */
  background: var(--cream-deep);
}
.sheet-pages .sheet-page {
  display: block;
  flex: none;
  width: 100%;
  max-width: 960px;
  height: auto;
  border: 1.5px solid var(--staffline);
  border-radius: 4px;
  box-shadow: 0 2px 10px rgba(22, 36, 27, .12);
}

/* ─── Dropping a score ──────────────────────────────────────────────────── */
#drop-hint {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(22, 36, 27, .35);
  pointer-events: none;
}
body.drop-armed #drop-hint { display: flex; }
.drop-card {
  padding: 26px 34px;
  text-align: center;
  background: var(--paper);
  border: 2.5px dashed var(--ink);
  border-radius: 20px;
  box-shadow: 8px 8px 0 var(--ink);
}
.drop-title { margin-top: 8px; font-family: var(--font-display); font-weight: 800; font-size: 24px; letter-spacing: -.03em; color: var(--ink); }
.drop-sub { margin-top: 4px; font-size: 13px; color: var(--muted); }

@media (max-width: 760px) {
  /* The library is a strip you swipe along on a phone. The phone rule above
     sets it scrolling sideways but it inherited the desktop column, so it
     stacked and all but the first song was cut off under the staff. */
  body.screen-song #songList { flex-direction: row; }
  body.screen-song #songList .song-item { flex: none; }
  .lane-barnum, .lane-dyn { display: none; }
  #sheet-overlay { padding: 10px; }
  .sheet-head { flex-direction: column; align-items: stretch; }
  .sheet-pages { padding: 10px; }
}

/* ─── Printing the sheet ────────────────────────────────────────────────────
   While the sheet is open, printing prints the pages and nothing else, one
   page of paper per page of notation, edge to edge. */
@page { size: A4 landscape; margin: 0; }
@media print {
  body.sheet-open > *:not(#sheet-overlay) { display: none !important; }
  /* The app shell is one fixed viewport that never scrolls, which on paper
     would be one page with everything after it cut off. */
  html:has(body.sheet-open), body.sheet-open { height: auto !important; min-height: 0 !important; overflow: visible !important; }
  body.sheet-open { background: #fff !important; }
  body.sheet-open #sheet-overlay { position: static; display: block !important; padding: 0; }
  body.sheet-open #sheet-overlay .help-scrim,
  body.sheet-open .sheet-head { display: none !important; }
  body.sheet-open .sheet-card { display: block; width: auto; max-height: none; border: 0; border-radius: 0; box-shadow: none; overflow: visible; }
  body.sheet-open .sheet-pages { display: block; padding: 0; overflow: visible; background: #fff; }
  body.sheet-open .sheet-pages .sheet-page {
    width: 297mm; height: 210mm; max-width: none;
    border: 0; border-radius: 0; box-shadow: none;
    break-after: page; page-break-after: always;
  }
  body.sheet-open .sheet-pages .sheet-page:last-child { break-after: auto; page-break-after: auto; }
}

/* A chord on the lane: its tones stacked in one token, as Jianpu writes a
   chord, each marked as you find it. A ringed tone is a long one, as on a
   single note. */
.lane-note.chord {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 2px 5px;
}
.lane-tone {
  position: relative;
  display: block;
  min-width: 20px;
  line-height: 17px;
  font-size: 14px;
  text-align: center;
  border-radius: 999px;
}
.lane-tone.long { box-shadow: inset 0 0 0 1.5px var(--ink); }
.lane-tone.dotted::after {
  content: '';
  position: absolute;
  right: -5px; top: 50%;
  width: 3px; height: 3px;
  margin-top: -1.5px;
  border-radius: 50%;
  background: var(--ink);
}
.lane-tone.got { background: var(--sun); }
