/* Measure — dark-first token system.
   :root holds the dark (default) values; [data-theme="light"] overrides them.
   Every component should read tokens, never hardcode colors. */

:root {
  --bg: #0b0d12;
  --panel: #14171e;
  --panel-raised: #1b1f28;
  --border: #262b36;
  --text: #eef0f4;
  --text-dim: #9aa1b1;
  --accent: #5b8cff;
  --accent-text: #0b0d12;
  --success: #3ddc84;
  --danger: #ff5c6c;
  --warn: #ffb454;

  /* Piano keys look like piano keys regardless of app theme (§1 "match the instrument's
     geometry") — these deliberately don't flip in the light override below. */
  --key-white: #f4f3f1;
  --key-black: #23252b;
  --key-border: #00000055;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 16px;
  --space-4: 24px;
  --space-5: 40px;

  --radius: 10px;
  --radius-sm: 6px;

  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  color-scheme: dark;
}

:root[data-theme="light"] {
  --bg: #f5f6f9;
  --panel: #ffffff;
  --panel-raised: #eef0f4;
  --border: #dde1e8;
  --text: #14171e;
  --text-dim: #5b6270;
  --accent: #3a66d8;
  --accent-text: #ffffff;
  --success: #1f9d5c;
  --danger: #d13a4a;
  --warn: #b3760a;

  color-scheme: light;
}

* {
  box-sizing: border-box;
}

/* The `hidden` attribute must always win. The UA stylesheet's `[hidden] { display: none }` is a
   bare element rule, so ANY author rule that sets `display` on the same element overrides it — e.g.
   `.drill-summary { display: flex }` (below) left the pause/summary overlays permanently on screen,
   covering the whole drill (the "Session complete on every drill" bug). `!important` here keeps a
   `hidden` element hidden no matter what other display rule it carries; components show themselves
   by REMOVING the attribute (`el.hidden = false`), which stops matching this rule. */
[hidden] {
  display: none !important;
}

html,
body {
  height: 100%;
  margin: 0;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior: none;
  /* No double-tap-to-zoom, anywhere in the app (§11.39). iOS Safari deliberately IGNORES the
     viewport meta's `user-scalable=no`/`maximum-scale=1` (it has since iOS 10, for accessibility),
     so `touch-action` is the only mechanism that actually works there. `manipulation` removes
     exactly the non-standard double-tap gesture while still allowing panning AND pinch-zoom — the
     accessibility zoom is preserved, only the accidental double-tap (which mid-drill leaves the
     staff zoomed and off-centre) is gone. Not `none` here: that would also kill scrolling on the
     normal pages. The drill page locks down harder — see `.drill-locked` / `.drill-page` below.
     touch-action is not inherited, but the effective behaviour is the INTERSECTION of an element's
     value with its ancestors', so setting it on body covers every descendant. */
  touch-action: manipulation;
}

/* The drill screen must not scroll at all, on any device (§11.39, THOUGHTS 2026-07-23). The
   `.drill-page` container was already `height:100dvh; overflow:hidden`, but that only stops the
   DRILL from scrolling — the document behind it could still be dragged: iOS rubber-bands the whole
   page on a touch-drag, and a hairline mismatch between 100dvh and the document height (a real
   possibility while Safari's toolbars collapse/expand) is enough to make the viewport creep. Taking
   `body` out of flow with `position:fixed` leaves the document with nothing scrollable at all, which
   also stops the toolbar collapse/expand that caused the mismatch in the first place. Applied via a
   body class set by drill.html rather than a `:has()` selector so the intent is visible in the
   template. Only /drill gets this — /playground and the normal pages legitimately scroll. */
body.drill-locked {
  position: fixed;
  inset: 0;
  overflow: hidden;
  overscroll-behavior: none;
}

a {
  color: var(--accent);
}

.page {
  min-height: 100dvh;
  padding: var(--space-4);
  padding-top: calc(var(--space-4) + env(safe-area-inset-top));
  padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-width: 640px;
  margin: 0 auto;
}

.page-header {
  position: relative;
  text-align: center;
}

/* Sub-page back-to-Home control (Phase 14) — Settings/Dashboard/Custom-drill get a real header
   button instead of a tiny .hint text link buried at the bottom of the page. Home itself never
   renders one (it's the hub, nothing to go "back" to). */
.page-back-link {
  position: absolute;
  top: 0;
  left: 0;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-2);
  color: var(--text-dim);
  text-decoration: none;
  font-size: 0.9rem;
}

.page-back-link:hover,
.page-back-link:active {
  color: var(--text);
}

.brand {
  margin: 0;
  font-size: 2rem;
  letter-spacing: 0.02em;
}

/* A page-truly-centered title can collide with the absolutely-positioned back link on narrow
   phones (measured: real overlap at 375px width for "Custom Drill", the longest of the three
   sub-page titles). Reserving symmetric horizontal padding on the title keeps it centered within a
   narrower zone that clears the back link on both sides — the title is free to wrap to 2 lines on
   the narrowest phones + longest titles rather than overlap, which is the acceptable degradation
   here, not a broken layout. */
.page-header:has(.page-back-link) .brand {
  padding: 0 90px;
}

.tagline {
  margin: var(--space-1) 0 0;
  color: var(--text-dim);
}

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
}

.section-title {
  margin: 0 0 var(--space-3);
  font-size: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
}

.sync-badge {
  display: inline-block;
  margin-top: var(--space-2);
  font-size: 0.75rem;
  padding: 2px var(--space-2);
  border-radius: 999px;
  color: var(--warn);
  border: 1px solid var(--warn);
  background: color-mix(in srgb, var(--warn) 12%, var(--panel));
}

.hint {
  margin: var(--space-3) 0 0;
  color: var(--text-dim);
  font-size: 0.85rem;
}

/* Shared button tokens (Phase 14) — used by Home's nav row, Settings, and the Custom-drill page.
   Drill's own pause/summary overlay keeps its existing .drill-overlay-btn styling untouched (a
   distinct, overlay-specific context that predates this token set and isn't part of this pass). */
.btn {
  min-height: 44px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-raised);
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--space-4);
  transition: background 60ms ease-out, border-color 60ms ease-out, opacity 60ms ease-out;
}

.btn:disabled {
  opacity: 0.5;
  cursor: default;
}

.btn-primary {
  background: var(--accent);
  color: var(--accent-text);
  border-color: var(--accent);
}

.btn-danger {
  border-color: var(--danger);
  color: var(--danger);
  background: transparent;
}

.btn-sm {
  min-height: 36px;
  padding: 0 var(--space-3);
  font-size: 0.9rem;
}

/* Home nav row + quiet dev footer (Phase 14) — replaces the old buried .hint text links. */
.home-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.home-nav .btn {
  flex: 1 1 100px;
}

.home-dev-footer {
  text-align: center;
  color: var(--text-dim);
  font-size: 0.75rem;
}

.home-dev-footer a {
  color: var(--text-dim);
}

.preset-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--space-2);
}

.preset-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--panel-raised);
  border: 1px solid var(--border);
  text-decoration: none;
  color: var(--text);
  min-height: 44px;
}

.preset-title {
  font-weight: 600;
}

.preset-range {
  color: var(--text-dim);
  font-size: 0.85rem;
}

.settings-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 44px;
}

.settings-input {
  background: var(--panel-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  padding: var(--space-2);
  font-size: 1rem;
  min-height: 44px;
  min-width: 160px;
}

.settings-toggle {
  width: 24px;
  height: 24px;
}

/* Dev pages (/dev/gallery, /dev/input) — diagnostic tools, not drill UI. */

.dev-page {
  max-width: 1100px;
  margin: 0 auto;
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.dev-meta {
  color: var(--text-dim);
  font-size: 0.85rem;
}

.harness-banner {
  padding: var(--space-3);
  border-radius: var(--radius);
  font-weight: 600;
  font-size: 0.95rem;
}

.harness-pass {
  background: color-mix(in srgb, var(--success) 18%, var(--panel));
  color: var(--success);
  border: 1px solid var(--success);
}

.harness-fail {
  background: color-mix(in srgb, var(--danger) 18%, var(--panel));
  color: var(--danger);
  border: 1px solid var(--danger);
}

.harness-failures {
  margin: var(--space-2) 0 0;
  padding-left: var(--space-4);
  color: var(--danger);
  font-size: 0.85rem;
  font-family: ui-monospace, Consolas, monospace;
}

.gallery-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.gallery-section h2 {
  margin: 0;
  font-size: 1.1rem;
  border-bottom: 1px solid var(--border);
  padding-bottom: var(--space-2);
}

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--space-3);
}

.gallery-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* 24px (page) + 16px (card) padding per side is ~80px of chrome around the piano — over a fifth
   of a 375px iPhone's width, eating directly into the ≥44px key-width floor (CLAUDE.md §7.3).
   Tighten both on narrow screens; this is a real layout fix, not just a padding-shaving hack.
   Must stay physically after the .dev-page/.gallery-card base rules above — same specificity,
   so source order is what makes this win at narrow widths; moving it earlier silently loses. */
@media (max-width: 480px) {
  .dev-page {
    padding: var(--space-2);
  }
  .gallery-card {
    padding: var(--space-2);
  }
}

.gallery-caption {
  color: var(--text-dim);
  font-size: 0.8rem;
}

.gallery-scroller {
  overflow-x: auto;
}

.staff-svg {
  display: block;
  width: 100%;
  height: auto;
  min-width: 120px;
}

.rhythm-svg {
  display: block;
  width: 100%;
  height: auto;
  min-width: 160px;
}

/* Input components (Phase 2): piano keyboard, letter/interval buttons, shared feedback states.
   touch-action/user-select/tap-highlight are set here (not just in a future drill page) because
   /dev/input exercises real touch interaction per CLAUDE.md §10. */

.piano-wrap {
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
}

.piano-octave-btn {
  flex: 0 0 auto;
  min-width: 44px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-raised);
  color: var(--text);
  font-size: 1.1rem;
  cursor: pointer;
}

.piano-octave-btn:active {
  background: var(--accent);
  color: var(--accent-text);
}

.piano-svg {
  display: block;
  width: 100%;
  height: auto;
  min-width: 200px;
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  overscroll-behavior: none;
}

.piano-key {
  stroke: var(--key-border);
  stroke-width: 1;
  transition: fill 60ms ease-out;
}

.piano-key-white.state-idle {
  fill: var(--key-white);
}

.piano-key-black.state-idle {
  fill: var(--key-black);
}

.piano-key.state-pressed {
  fill: var(--accent);
}

.piano-key.state-correct {
  fill: var(--success);
}

.piano-key.state-wrong {
  fill: var(--danger);
}

.piano-key-label {
  fill: var(--key-black);
  font-size: 11px;
  pointer-events: none;
}

/* ── Keyboard Playground (CLAUDE.md §13 Phase 15; compact-controls + fill-the-space rework
   2026-07-24, §11.41). The page is a fixed-height INSTRUMENT SURFACE, not a scrolling settings page:
   a compact header, one wrapping control bar, and a keyboard that takes every remaining pixel. The
   first version stacked four full-width .settings-row controls above the board, which on a phone
   pushed the keyboard into the last ~25% of the viewport (owner report, with screenshots). Its own
   .pg-* namespace so neither the drill's height-driven .drill-answer-area rules (Phase 10) nor the
   generic .settings-row chrome leak in. */

/* No document scroll at all here (same treatment as body.drill-locked, §11.39) — the page is exactly
   viewport-height, so any scroll is iOS rubber-banding, not real content movement. touch-action is
   deliberately NOT set here: the Scroll-mode keyboard host needs pan-x, and effective touch-action is
   the intersection with its ancestors, so locking the body would silently freeze that panning. */
body.pg-locked {
  position: fixed;
  inset: 0;
  overflow: hidden;
}

.pg-page {
  height: 100dvh;
  max-width: 1100px;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  padding-top: calc(var(--space-2) + env(safe-area-inset-top));
  padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom));
  padding-left: calc(var(--space-3) + env(safe-area-inset-left));
  padding-right: calc(var(--space-3) + env(safe-area-inset-right));
  overflow: hidden;
}

/* A single compact row, not the centered .page-header block — the title is chrome here, the
   keyboard is the content. */
.pg-header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
}

.pg-back-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  margin-left: calc(var(--space-2) * -1);
  color: var(--text-dim);
  text-decoration: none;
  font-size: 1.25rem;
}

.pg-back-link:hover,
.pg-back-link:active {
  color: var(--text);
}

.pg-title {
  margin: 0;
  font-size: 1.1rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.pg-midi-chip {
  margin-left: auto;
  max-width: 40%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.75rem;
  color: var(--text-dim);
}

.pg-controls {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* 40px: comfortably tappable without spending the full 44px of vertical budget four times over,
   which is what made the old one-control-per-row layout so costly. */
.pg-select,
.pg-icon-btn,
.pg-toggle {
  min-height: 40px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-raised);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
  cursor: pointer;
}

.pg-select {
  padding: 0 var(--space-2);
}

.pg-icon-btn {
  min-width: 40px;
  padding: 0;
  line-height: 1;
}

.pg-icon-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.pg-range-shift {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.pg-range-label {
  min-width: 78px;
  text-align: center;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

/* Segmented control: the two halves read as one control rather than two loose buttons. */
.pg-toggle-group {
  display: flex;
}

.pg-toggle {
  padding: 0 var(--space-3);
}

.pg-toggle-group .pg-toggle:first-child {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.pg-toggle-group .pg-toggle:last-child {
  border-left: none;
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

.pg-toggle[aria-pressed="true"] {
  background: var(--accent);
  color: var(--accent-text);
  border-color: var(--accent);
}

/* Deliberately unstyled — no panel background, border, or padding. A wide, few-key board is
   physically short (a 15-key octave pair on a 375px phone is 22px per key, and 22px keys are ~150px
   long at a realistic aspect — anything taller is a ribbon, not a piano), so on a tall portrait
   screen there is always leftover height that no undistorted keyboard can consume. Framing that
   leftover in a bordered panel is what made it read as a broken empty box in the owner's
   screenshots; with the frame gone the same space reads as ordinary page margin around a centred
   instrument. The keys are high-contrast enough to hold the composition on their own. */
.pg-keyboard-panel {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
}

/* keyboard.js measures THIS element to decide how tall to draw the keys, so it deliberately carries
   no padding of its own (the panel above holds it instead) and takes its size from the flex layout —
   never from the SVG inside it, which would make the measure→resize→measure cycle self-feeding. */
.pg-keyboard-host {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.pg-keyboard-svg {
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  overscroll-behavior: none;
}

.pg-key-label {
  fill: var(--key-black);
  pointer-events: none;
}

/* Fit (default): full width, with the height following the viewBox that keyboard.js sized to this
   box. It sets an inline max-width on the SVG in the one case that needs it — few keys in a wide
   box, where filling the height at full width would overflow — and the auto margins centre it. */
.pg-keyboard-fit .pg-keyboard-svg {
  display: block;
  width: 100%;
  height: auto;
  /* Safety net, not the sizing mechanism (keyboard.js's measured viewBox is): if a rebuild ever
     lags a viewport change, the board letterboxes inside its box instead of spilling out of it —
     the SVG's own preserveAspectRatio keeps that undistorted. */
  max-height: 100%;
  margin: 0 auto;
  touch-action: none;
}

/* Scroll: keys hold a fixed, comfortable 44px width (the §7.3 touch floor) regardless of key count,
   and the host scrolls horizontally when they don't fit. --pg-white-count is set by keyboard.js on
   every build() so the width stays pure CSS. justify-content:flex-start (overriding the host's own
   centred default via source order) keeps the initial scroll position at the low end of the board
   rather than centring it, which would otherwise clip both ends by an equal, ambiguous amount
   before the user has scrolled either way. */
.pg-keyboard-scroll {
  justify-content: flex-start;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
}

.pg-keyboard-scroll .pg-keyboard-svg {
  display: block;
  flex: 0 0 auto;
  width: calc(var(--pg-white-count, 7) * 44px);
  height: auto;
  max-height: 100%;
  touch-action: pan-x;
}

.pg-footnote {
  flex: 0 0 auto;
  margin: 0;
  font-size: 0.75rem;
  line-height: 1.3;
  color: var(--text-dim);
  text-align: center;
}

/* Landscape phone: every vertical pixel belongs to the keyboard. The footnote is desktop-input
   guidance, which is exactly the audience NOT on a short landscape viewport. */
@media (max-height: 500px) {
  .pg-footnote {
    display: none;
  }
}

.letter-buttons,
.interval-buttons {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.letter-accidental-row,
.letter-row {
  display: flex;
  gap: var(--space-2);
}

.interval-buttons {
  flex-direction: row;
  flex-wrap: wrap;
}

.letter-btn {
  flex: 1 1 0;
  min-width: 44px;
  min-height: 44px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-raised);
  color: var(--text);
  font-size: 1rem;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: background 60ms ease-out, color 60ms ease-out;
}

.letter-btn.state-armed {
  background: var(--accent);
  color: var(--accent-text);
}

.letter-btn.state-correct {
  background: var(--success);
  color: var(--accent-text);
}

.letter-btn.state-wrong {
  background: var(--danger);
  color: var(--accent-text);
}

/* Feedback: transform/opacity only (CLAUDE.md §9/§10) — never layout-affecting properties. */
@keyframes shake {
  0%,
  100% {
    transform: translateX(0);
  }
  20% {
    transform: translateX(-6px);
  }
  40% {
    transform: translateX(5px);
  }
  60% {
    transform: translateX(-4px);
  }
  80% {
    transform: translateX(3px);
  }
}

.shake {
  animation: shake 220ms ease-in-out;
}

/* Drill page (Phase 3) — CLAUDE.md §9: fixed 100dvh, no scrolling, staff ~55% / answer ~45%. */

.drill-page {
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* Stronger than body's `manipulation` (§11.39): inside the drill itself no touch gesture should do
     anything the browser handles — no pan, no pinch, no double-tap. Every answer surface (piano,
     tap zone) already set this; hoisting it to the page root covers the header and staff area too,
     which are otherwise draggable dead space. Scoped to `.drill-page` and NOT to `body.drill-locked`
     on purpose: the pause/summary overlays are body-level SIBLINGS of this element, so they keep
     body's `manipulation` and their panels can still scroll a long summary. */
  touch-action: none;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

.drill-header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
  gap: var(--space-3);
}

.drill-icon-btn {
  min-width: 44px;
  min-height: 44px;
  border: none;
  background: transparent;
  color: var(--text);
  font-size: 1.2rem;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.drill-progress {
  font-variant-numeric: tabular-nums;
  font-size: 1.1rem;
  font-weight: 600;
}

.drill-streak {
  color: var(--text-dim);
  font-size: 0.95rem;
}

/* Phase 14 cursory pass: padding trimmed from --space-3 to --space-2 so the height-driven SVG
   below fills a few more pixels of this box — a single note reads with a bit more presence. The
   note's actual size within its own SVG viewBox is the notation renderer's territory (staff.js),
   not touched here — a CSS-only pass can reclaim box padding, not the renderer's internal
   whitespace. Revisit staff.js directly if more presence is still wanted after this. */
.drill-staff-area {
  flex: 0 0 55%;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-2);
  overflow: hidden;
}

.drill-staff-area .staff-svg,
.drill-staff-area .rhythm-svg {
  width: auto;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
}

/* Rhythm/tempo (Phase 7) — beat-pulse indicator + tap zone, CLAUDE.md §7.1, §7.3. */

.beat-pulse {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--border);
  flex: 0 0 auto;
  transition: background 40ms ease-out, transform 40ms ease-out;
}

.beat-pulse.beat-pulse-on {
  background: var(--accent);
  transform: scale(1.3);
}

.beat-pulse.beat-pulse-downbeat.beat-pulse-on {
  background: var(--success);
}

.tap-zone {
  width: 100%;
  height: 100%;
  min-height: 120px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel-raised);
  color: var(--text-dim);
  font-size: 1.1rem;
  font-weight: 600;
  cursor: pointer;
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  overscroll-behavior: none;
  transition: background 60ms ease-out, border-color 60ms ease-out;
}

.tap-zone.tap-zone-active {
  background: color-mix(in srgb, var(--accent) 25%, var(--panel-raised));
  border-color: var(--accent);
}

.drill-start-gate {
  padding: 0 var(--space-4);
}

/* MIDI device chip (Phase 9, §7.3) — compact by necessity: a real device name (e.g. "Arturia
   KeyStep 37") can be far longer than the cramped drill header has room for, so it's truncated
   with the full name in `title` rather than reflowing the header on narrow screens. */
.drill-midi-chip {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 90px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.75rem;
  color: var(--text-dim);
}

.drill-midi-chip::before {
  content: "🎹 ";
}

.drill-answer-area {
  flex: 0 0 45%;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-3);
  overflow: hidden;
  touch-action: none;
  overscroll-behavior: none;
}

/* The base .piano-svg/.piano-wrap rules above are width-driven (correct for /dev/input's demo
   cards, which sit in a normal scrolling page with no definite container height). The real drill
   answer area DOES have a definite height (a fixed flex-basis of the 100dvh drill-page column),
   so here the keyboard is instead height-driven — sized to fill that box, with object-fit:contain
   doing the aspect-ratio-preserving fit (same "letterbox, never distort, never overflow" behavior
   the SVG's own default preserveAspectRatio already gives its internal content). Phase 10: fixes
   a one-octave keyboard stretching edge-to-edge on wide/landscape screens (width:100%;height:auto
   on a flat aspect-ratio SVG made the height balloon past the answer area and clip against its
   overflow:hidden). min-width/min-height:0 override the flexbox default of shrinking no further
   than intrinsic content size, which would otherwise fight this sizing in both the piano-wrap row
   (buttons + svg) and the plain svg-only case. */
.drill-answer-area .piano-wrap {
  height: 100%;
  min-width: 0;
  max-width: 100%;
}

.drill-answer-area .piano-svg {
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  object-fit: contain;
}

/* Narrow phones + octave-shift buttons (Phase 10, octave_precision/exact modes): a 1-octave
   keyboard (7 keys) flanked by two 44px shift buttons plus their gaps and the area's own padding
   needs at least 7*44 + 2*44 + gaps + padding =~ 400px+ to keep EVERY element at the 44px floor —
   more than any phone is wide, even at zero gap/padding. This is a genuine geometric budget
   problem, not a tunable one: tightening padding/gap here (below) is real and worth doing, but on
   the narrowest phones the keys still land a few px under 44 (measured ~41px at 390px width,
   versus ~18px before this phase and ~36px with tightening alone) — see the §7.3 PLAN NOTE update
   for the honest final state, and §14 for a possible follow-up (relocating/shrinking the shift
   buttons specifically on narrow screens, deferred rather than rushed here). */
@media (max-width: 480px) {
  .drill-answer-area:has(.piano-wrap) {
    padding: var(--space-1);
  }
  .drill-answer-area .piano-wrap {
    gap: var(--space-1);
  }
}

/* Phone landscape (Phase 10): the fixed 55/45 staff/answer split leaves the keyboard only ~120px
   tall on a ~375px-tall landscape phone viewport once the header and padding are subtracted — not
   enough to clear the §7.3 44px white-key floor even after the height-driven sizing fix above,
   since object-fit:contain is still bounded by whatever height the split actually gives it. The
   keyboard's 44px floor is a harder, previously-tested constraint than the staff's legibility, so
   this rebalances toward the keyboard and trims chrome padding to reclaim pixels. max-height keeps
   this scoped to phones — iPad landscape (~768px+ tall) and desktop already clear the floor under
   the default split. */
@media (orientation: landscape) and (max-height: 500px) {
  .drill-header {
    padding: var(--space-1) var(--space-2);
  }
  .drill-staff-area {
    flex: 0 0 34%;
    padding: var(--space-2);
  }
  .drill-answer-area {
    flex: 0 0 66%;
    padding: var(--space-1) var(--space-2);
  }
}

.drill-overlay,
.drill-summary {
  position: fixed;
  inset: 0;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(var(--space-4) + env(safe-area-inset-top)) calc(var(--space-4) + env(safe-area-inset-right))
    calc(var(--space-4) + env(safe-area-inset-bottom)) calc(var(--space-4) + env(safe-area-inset-left));
}

.drill-overlay-panel,
.drill-summary-panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
  max-width: 420px;
  width: 100%;
  max-height: 100%;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  text-align: center;
}

.drill-overlay-btn {
  min-height: 44px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-raised);
  color: var(--text);
  font-size: 1rem;
  cursor: pointer;
  text-decoration: none;
  display: flex;
  align-items: center;
  justify-content: center;
}

.drill-overlay-btn-quit {
  border-color: var(--danger);
  color: var(--danger);
}

.summary-stats {
  display: flex;
  justify-content: space-around;
  gap: var(--space-2);
}

.summary-stat {
  display: flex;
  flex-direction: column;
}

.summary-stat-value {
  font-size: 1.4rem;
  font-weight: 700;
}

.summary-stat-label {
  color: var(--text-dim);
  font-size: 0.8rem;
}

.summary-section {
  text-align: left;
}

.summary-section h3 {
  margin: 0 0 var(--space-2);
  font-size: 0.9rem;
  color: var(--text-dim);
}

.summary-section ul {
  margin: 0;
  padding-left: var(--space-4);
  font-size: 0.9rem;
}

.summary-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Dashboard (Phase 4) — CLAUDE.md §7.7. */

.dashboard-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-3);
}

.dashboard-panel-header .section-title {
  margin: 0;
}

.toggle-group {
  display: flex;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.toggle-btn {
  min-height: 36px;
  padding: 0 var(--space-3);
  border: none;
  background: var(--panel-raised);
  color: var(--text-dim);
  font-size: 0.85rem;
  cursor: pointer;
}

.toggle-btn.active {
  background: var(--accent);
  color: var(--accent-text);
}

.heatmap-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  margin-bottom: var(--space-3);
}

.heatmap-staff {
  min-width: 600px;
  display: flex;
  align-items: center;
}

.heatmap-staff .staff-svg {
  width: 100%;
  height: 140px;
}

.stave-note-hit {
  cursor: pointer;
}

#heatmap-detail {
  min-height: 1.2em;
}

.streak-row {
  display: flex;
  gap: var(--space-4);
  margin-bottom: var(--space-3);
}

.calendar-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.calendar-grid {
  display: grid;
  grid-template-rows: repeat(7, 12px);
  grid-auto-flow: column;
  grid-auto-columns: 12px;
  gap: 3px;
  width: max-content;
}

.calendar-cell {
  width: 12px;
  height: 12px;
  border-radius: 2px;
  background: var(--panel-raised);
}

.calendar-cell-empty {
  background: transparent;
}

.calendar-cell[data-level="1"] {
  background: color-mix(in srgb, var(--success) 25%, var(--panel-raised));
}

.calendar-cell[data-level="2"] {
  background: color-mix(in srgb, var(--success) 50%, var(--panel-raised));
}

.calendar-cell[data-level="3"] {
  background: color-mix(in srgb, var(--success) 75%, var(--panel-raised));
}

.calendar-cell[data-level="4"] {
  background: var(--success);
}

.trend-chart {
  width: 100%;
  height: auto;
}

.trend-chart-label {
  fill: var(--text-dim);
  font-size: 8px;
}

.session-log-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.session-log-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}

.session-log-table th,
.session-log-table td {
  text-align: left;
  padding: var(--space-2);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.session-log-table th {
  color: var(--text-dim);
  font-weight: 500;
}

.settings-data-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
}

.settings-data-block:first-of-type {
  border-top: none;
  padding-top: 0;
}

.settings-data-title {
  font-weight: 600;
}

.settings-data-block .hint {
  margin: 0;
}

.settings-data-block .btn {
  align-self: flex-start;
}

#data-status {
  min-height: 1.2em;
}

/* Settings: de-emphasized Data panel (Phase 14) — moved to the bottom of the page; the transparent
   background lets it visually recede against the raised-background panels above it (Practice/
   Audio/Appearance/MIDI), matching "present but not shouting" for a rare, destructive section. */
.settings-panel-quiet {
  background: transparent;
}

/* Home: Daily Mix + level track (Phase 5) — CLAUDE.md §7.1, §7.5, §9. */

.daily-mix-panel {
  padding: var(--space-3);
}

.daily-mix-card {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  padding: var(--space-3);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent) 14%, var(--panel));
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  min-height: 44px;
  text-align: left;
}

.daily-mix-card:disabled {
  opacity: 0.6;
  cursor: default;
}

.daily-mix-title {
  font-weight: 700;
  font-size: 1.1rem;
}

.daily-mix-sub {
  color: var(--text-dim);
  font-size: 0.85rem;
}

#daily-mix-status {
  margin: var(--space-2) 0 0;
  min-height: 1.2em;
}

/* Phase 14: compact 2-column grid — all 15 levels stay visible without the long single-column
   scroll the flex-column layout produced. Every level is playable (curriculum.py's LEVELS all use
   a PLAYABLE_MODES mode), so there's no disabled-card variant to style anymore. minmax(0, 1fr), not
   a bare 1fr: grid items default to min-width:auto, so a long unwrapped level name (nowrap +
   ellipsis below) would otherwise force its column past its 1fr share and overflow the page —
   minmax(0, ...) is what actually lets the ellipsis rule take effect instead of pushing the grid
   wider than the viewport. */
.level-track {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
}

@media (max-width: 340px) {
  .level-track {
    grid-template-columns: 1fr;
  }
}

.level-card {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--panel-raised);
  border: 1px solid var(--border);
  text-decoration: none;
  color: var(--text);
  min-height: 44px;
}

.level-ring {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
}

.level-ring-track {
  fill: none;
  stroke: var(--border);
  stroke-width: 4;
}

.level-ring-fill {
  fill: none;
  stroke: var(--success);
  stroke-width: 4;
  stroke-linecap: round;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
}

.level-card-body {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.level-card-name {
  font-weight: 600;
  font-size: 0.8rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.level-card-progress {
  color: var(--text-dim);
  font-size: 0.7rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Custom drill builder + saved presets (Phase 5; moved to its own /custom-drill page in Phase 14,
   §7.8) — same classes, just no longer rendered from settings.html. */

.preset-builder {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}

.preset-builder .btn {
  align-self: flex-start;
}

#preset-builder-status {
  margin: 0;
}

.preset-list {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.preset-list-item {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--panel-raised);
  border-radius: var(--radius-sm);
}

.preset-list-name {
  font-weight: 600;
  flex: 1 1 auto;
}

.preset-list-detail {
  color: var(--text-dim);
  font-size: 0.85rem;
}

/* Phase 8 — dev-mode perf overlay (§13, ?perf=1 on /drill). Not part of the normal UI chrome. */
.perf-overlay {
  position: fixed;
  top: calc(var(--space-2) + env(safe-area-inset-top));
  right: calc(var(--space-2) + env(safe-area-inset-right));
  z-index: 999;
  background: var(--panel-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-1) var(--space-2);
  font-size: 0.7rem;
  color: var(--text-dim);
  pointer-events: none;
  font-variant-numeric: tabular-nums;
}

.perf-overlay-warn {
  color: var(--warn);
}
