/* --- Self-Hosted Typography (Inter + JetBrains Mono) --- */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/Inter-Regular.woff2') format('woff2');
}

@font-face {
  font-family: 'Inter';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/Inter-Italic.woff2') format('woff2');
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/Inter-SemiBold.woff2') format('woff2');
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/Inter-Bold.woff2') format('woff2');
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/JetBrainsMono-Regular.woff2') format('woff2');
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/JetBrainsMono-Bold.woff2') format('woff2');
}


/* --- Glass Top Navigation Header --- */
/* #818: an element with backdrop-filter != none establishes a CSS Backdrop
   Root, and its descendant glass surfaces (#searchResults, #mobileNavDrawer,
   #navAccountMenu) can then only sample pixels inside the header itself —
   which is EMPTY below the bar row, so the popovers blurred nothing and
   painted a raw translucent wash over the page. The header HOST is
   therefore transparent and filterless; the glass fill, hairline and blur
   render on ::before (position: absolute; inset: 0; z-index: -1;
   pointer-events: none), which samples the live page behind the header
   without trapping any descendant. */
.glass-header {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  transition: all 0.25s ease;
}

.glass-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  border-radius: inherit;
  /* #810: the same shared --glass-* tokens as the player bar and every
     glass surface, in BOTH themes (the light variant comes from the
     html:not(.dark) token block). */
  background: var(--glass-bg);
  border-bottom: 1px solid var(--glass-border);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  transition: all 0.25s ease;
}

@supports not ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))) {
  .glass-header::before {
    background: #1c1917;
  }
  html:not(.dark) .glass-header::before {
    background: #fff9e8;
  }
}


/* Compact header on scroll */
.glass-header.header-compact {
  padding-top: 0.35rem !important;
  padding-bottom: 0.35rem !important;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45);
}

.glass-header.header-compact .header-logo-text {
  display: none;
}

@media (min-width: 640px) {
  .glass-header.header-compact .header-logo-text {
    display: block;
  }
  .glass-header.header-compact .header-subtitle {
    display: none;
  }
}

/* Hymn Stem Mixer — Dark Audio Workstation Theme */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
/* --- #812: canonical font tokens ------------------------------------------
   ONE declaration of each stack; every font-family in the app consumes
   these (the CSS rules below, both tailwind configs — their fontFamily
   points at the same vars — and the .glass-select recipe). The lyric
   serif is the documented exception for sung-text surfaces (.lyrics-line,
   .instrumental-row, the print view). */
:root {
  --font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  --font-lyric: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
}

body {
  /* #818 L0 canvas: the dark tint lives on body.bg-daime-darkBg below (the
     Tailwind utility owns the class and would win at plain-element
     specificity); this rule only carries the fallback for markup without
     the utility. */
  background-color: #0f1412;
  color: #e8e6e3;
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.4;
  min-height: 100vh;
  /* PB-04 M4: dynamic viewport height on mobile browsers (100vh fallback above). */
  min-height: 100dvh;
}

/* #818 L0 canvas: subtle radial ambient depth, so the translucent glass
   surfaces refract light even when the page is stationary. Pinned at
   body.bg-daime-darkBg specificity because the .bg-daime-darkBg utility
   (dark #121814) would otherwise out-race a plain `body` rule; the light
   canvas (html:not(.dark) body, #f6f1e3 + warm parchment glow) is pinned
   in the index.html inline theme block the same way. */
body.bg-daime-darkBg {
  background-color: #0f1412;
  background-image:
    radial-gradient(ellipse 80% 50% at 18% -8%, rgba(29, 78, 52, 0.18), transparent 70%),
    radial-gradient(ellipse 60% 45% at 85% 12%, rgba(212, 175, 55, 0.06), transparent 65%);
  background-attachment: fixed;
}

code, pre, .font-mono, .mono, kbd, samp {
  font-family: var(--font-mono);
}
/* --- Two-pane layout: hymn library sidebar + player pane --- */
.app-layout {
  display: flex;
  min-height: 100vh;
  min-height: 100dvh; /* PB-04 M4 */
}

.sidebar {
  width: 290px;
  min-width: 290px;
  background: #181b22;
  border-right: 1px solid #282d37;
  display: flex;
  flex-direction: column;
  height: 100vh;
  position: sticky;
  top: 0;
}

.sidebar-header {
  padding: 16px 16px 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  border-bottom: 1px solid #282d37;
}

.brand-block {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.brand-title {
  font-size: 15px;
  font-weight: 700;
  color: #f1f0ee;
  letter-spacing: 0.3px;
  line-height: 1.25;
}

.lang-switcher {
  display: flex;
  gap: 6px;
  align-items: center;
}

.lang-btn {
  background: #1e222a;
  border: 1px solid #3a3f4b;
  color: #a0a6b5;
  padding: 3px 8px;
  font-size: 11px;
  font-weight: 600;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.15s ease;
}

.lang-btn:hover {
  color: #e8e6e3;
  border-color: #555b6a;
}

.lang-btn.active {
  background: #16a085;
  border-color: #76d7c4;
  color: #ffffff;
}

.sidebar-subheading {
  font-size: 13px;
  font-weight: 600;
  color: #c0c6d4;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  margin-top: 2px;
}


.sidebar-controls {
  padding: 0 14px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.sidebar-controls input[type="search"],
.sidebar-controls select {
  background: #1e222a;
  border: 1px solid #3a3f4b;
  border-radius: 6px;
  color: #e8e6e3;
  padding: 8px 10px;
  font-size: 13px;
  outline: none;
  width: 100%;
}

.sidebar-controls input[type="search"]:focus,
.sidebar-controls select:focus {
  border-color: #76d7c4;
}

.hymn-list {
  list-style: none;
  overflow-y: auto;
  flex: 1;
  padding: 4px 8px;
}

.hymn-item {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 9px 10px;
  border-radius: 6px;
  cursor: pointer;
  border: 1px solid transparent;
}

.hymn-item:hover {
  background: #22262f;
}

.hymn-item.active {
  background: #1e2f2c;
  border-color: #76d7c4;
}

.hymn-num {
  font-size: 12px;
  color: #8892a0;
  min-width: 30px;
  font-variant-numeric: tabular-nums;
}

.hymn-title {
  font-size: 13px;
  color: #d5d3cf;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hymn-item.active .hymn-title {
  color: #76d7c4;
}

.sidebar-footer {
  padding: 10px 18px;
  border-top: 1px solid #282d37;
  font-size: 12px;
  color: #8892a0;
}

.player-pane {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

@media (max-width: 760px) {
  .app-layout { flex-direction: column; }
  .sidebar { width: 100%; min-width: 0; height: auto; position: static; }
  .hymn-list { max-height: 200px; }
}

.app-header {
  background: #181b22;
  border-bottom: 1px solid #282d37;
  padding: 12px 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}





.now-playing {
  font-size: 14px;
  color: #76d7c4;
  font-weight: 500;
}





.sel-grow {
  flex: 1 1 240px;
}

label {
  font-size: 12px;
  font-weight: 600;
  color: #8f96a3;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

select, input[type="search"] {
  background: #1e222a;
  color: #e8e6e3;
  border: 1px solid #3a3f4b;
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 13px;
  outline: none;
  transition: border-color 0.15s;
}

select:focus, input[type="search"]:focus {
  border-color: #5dade2;
}

.transport-bar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: #1a1d24;
  border-bottom: 1px solid #2d323f;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.transport-controls {
  display: flex;
  gap: 8px;
}

.btn {
  background: #2b303c;
  color: #e8e6e3;
  border: 1px solid #3a3f4b;
  border-radius: 6px;
  padding: 8px 16px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: background 0.15s, border-color 0.15s;
}

.btn:hover {
  background: #363c4a;
}

.btn-primary {
  background: #16a085;
  border-color: #1abc9c;
  color: #ffffff;
}

.btn-primary:hover {
  background: #1abc9c;
}

.seek-group {
  flex: 1 1 280px;
  display: flex;
  align-items: center;
  gap: 12px;
}

.seek-group input[type="range"] {
  flex: 1;
}

.master-group {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Range Inputs */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  height: 6px;
  background: #2d323f;
  border-radius: 3px;
  outline: none;
  cursor: pointer;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #5dade2;
  cursor: pointer;
  transition: transform 0.1s, background 0.15s;
}

input[type="range"]::-webkit-slider-thumb:hover {
  background: #7fb3d5;
  transform: scale(1.15);
}

/* Main Area */
.mixer-main {
  flex: 1;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 1600px;
  width: 100%;
  margin: 0 auto;
}

.waveform-container {
  width: 100%;
  min-height: 260px;
  background: #1e222a;
  border: 1px solid #3a3f4b;
  border-radius: 8px;
  overflow-x: auto;
  position: relative;
}

/* Channel Strips */
.channels-strip {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}

.chan {
  min-width: 130px;
  flex: 1 1 140px;
  background: #1e222a;
  padding: 12px;
  border-radius: 8px;
  border: 1px solid #3a3f4b;
  display: flex;
  flex-direction: column;
  gap: 10px;
  transition: opacity 0.2s, border-color 0.2s;
}

.chan.err {
  opacity: 0.4;
  border-color: #c0392b;
}

.chan-title {
  text-align: center;
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 1px;
  color: #f1f0ee;
  padding-bottom: 4px;
  border-bottom: 1px solid #2d323f;
}

.chan-buttons {
  display: flex;
  gap: 6px;
}

.chan-buttons button {
  flex: 1;
  padding: 6px 4px;
  border: 1px solid #3a3f4b;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 700;
  background: #2b303c;
  color: #cbd3e1;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.chan-buttons button:hover {
  background: #363c4a;
}

.chan-buttons button.mute.on {
  background: #c0392b;
  border-color: #d9534f;
  color: #ffffff;
}

.chan-buttons button.solo.on {
  background: #27ae60;
  border-color: #2ecc71;
  color: #ffffff;
}

.chan-vol {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: stretch;
}

.chan-vol-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.chan-vol input[type="range"] {
  width: 100%;
}
/* Feat #31: Skeleton loading & error states for multitrack stems */
@keyframes stemShimmer {
  0% { opacity: 0.4; }
  50% { opacity: 0.8; }
  100% { opacity: 0.4; }
}

.stem-skeleton-row, .stem-skeleton {
  animation: stemShimmer 1.8s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .stem-skeleton-row, .stem-skeleton {
    animation: none !important;
  }
}

.stem-error-row, .stem-error-container {
  min-height: 160px;
}


/* Positioning context for the buffering overlay (absolute, inset:0) — the
   mount containers don't carry Tailwind `relative` in index.html; owning
   this here keeps index.html untouched (not in this slice's owned files). */
#mixerWaveformContainer {
  position: relative;
}
/* ADR 0038 D1 buffering state UX (issue #550, part of #316): the mixer's
   visible overlay for the governor's `priming`/`starved`/`seeking` states
   — spinner + localized state text + a buffered-ranges bar driven by REAL
   stem-stream coverage (never a time estimate). The error surface
   (`.stem-error-container`, above) replaces it on `error`. */
.mixer-buffer-overlay {
  position: absolute;
  inset: 0;
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 1rem;
  background: rgba(12, 10, 9, 0.82);
  border-radius: 0.5rem;
  text-align: center;
}
.mixer-buffer-overlay[hidden] {
  display: none;
}
.mixer-buffer-state-text {
  font-size: 0.8rem;
  color: #e7e5e4;
  font-weight: 500;
}
.mixer-buffer-overlay--starved .mixer-buffer-state-text {
  color: #fcd34d;
}
.mixer-buffer-overlay--seeking .mixer-buffer-state-text {
  color: #7dd3fc;
}
.mixer-buffer-bar {
  position: relative;
  width: min(100%, 20rem);
  height: 0.375rem;
  border-radius: 9999px;
  background: rgba(68, 64, 60, 0.6);
  overflow: hidden;
}
.mixer-buffer-bar[hidden] {
  display: none;
}
.mixer-buffer-seg {
  position: absolute;
  top: 0;
  bottom: 0;
  background: #d4af37;
  border-radius: 9999px;
}
/* --- Page chrome (epic-93 wave 2, slice B) --- */

/* Tailwind display utilities (.flex etc.) override the UA [hidden] rule;
   keep the hidden attribute authoritative (#90 modal, #89 menu). */
[hidden] {
  display: none !important;
}

/* #86: keep the last hymn clear of the fixed bottom player
   (#globalBottomBar is position:fixed bottom-0). */
@media screen {
  #libraryView,
  #hinarioView {
    padding-bottom: 8rem;
  }
  body:has(#globalBottomBar:not(.hidden)) {
    padding-bottom: 8rem;
  }
}

@media screen and (max-width: 639px) {
  #libraryView,
  #hinarioView {
    padding-bottom: 9.5rem;
  }
  body:has(#globalBottomBar:not(.hidden)) {
    padding-bottom: 9.5rem;
  }
}

/* Feat #17 Phase 1: Printable View Stylesheet */
@media print {
  @page {
    size: A4;
    margin: 15mm;
  }
  html, body {
    background-color: #ffffff !important;
    color: #111827 !important;
    background: #ffffff !important;
  }
  header, nav, footer, #statusBanner, #studyControls, #transportBar, #mixerSection, .print\:hidden, #printControlsBar, #searchResults, #langSwitcher, #categoryFilters, #chordStrip, #notationControl, #btnChordStrip {
    display: none !important;
  }
  #appView, #printView, #printContentContainer {
    margin: 0 !important;
    padding: 0 !important;
    max-width: 100% !important;
    width: 100% !important;
    display: block !important;
  }
  .print-hymn-card {
    break-after: page !important;
    page-break-after: always !important;
    break-inside: avoid !important;
    page-break-inside: avoid !important;
    border: none !important;
    box-shadow: none !important;
    padding: 0 !important;
    margin-bottom: 2rem !important;
    background: transparent !important;
    color: #111827 !important;
  }
  .print-stanza {
    break-inside: avoid !important;
    page-break-inside: avoid !important;
  }
  .chord-line {
    color: #92400e !important;
    font-weight: 700 !important;
  }
  .lyrics-line {
    color: #111827 !important;
  }
  .translation-line {
    color: #4b5563 !important;
  }
}
/* Issue #201: chord labels anchored to their word (chord_changes) */
.karaoke-word {
  position: relative;
  display: inline-block;
}

/* Issue #701: Syllable-accurate karaoke highlight (epic #703 / ADR-0003 D1) */
.karaoke-syllable {
  position: relative;
  display: inline-block;
  transform-origin: center bottom;
  /* Issue #712: transition only transform/opacity (compositor-friendly);
     color/text-shadow still change, just without per-frame interpolation.
     LDR-05: the duration is data-driven — lyrics-controller sets
     --syllable-transition-duration from each occurrence's own length.
     Fallback 0.06s keeps crisp 60ms behavior when the variable is unset. */
  transition: transform var(--syllable-transition-duration, 0.06s) cubic-bezier(0.2, 0.8, 0.2, 1),
              opacity var(--syllable-transition-duration, 0.06s) ease-out;
}

/* Multi-syllable words: dotted underline separator between syllables */
.karaoke-word > .karaoke-syllable:not(:last-of-type) {
  border-bottom: 1.5px dotted rgba(212, 175, 55, 0.45);
}

/* Active syllable highlight (~1.1em scale, gold text, subtle glow) */
.karaoke-syllable.karaoke-syllable-active,
.karaoke-syllable.is-active {
  color: var(--daime-gold, #F6E05E);
  transform: scale(1.1);
  text-shadow: 0 0 8px rgba(246, 224, 94, 0.55);
}

/* Long note extension: dynamic slight growth in intensity & size */
.karaoke-syllable.is-active.is-extended {
  color: #FFF3B0;
  transform: scale(1.15);
  text-shadow: 0 0 12px rgba(246, 224, 94, 0.8);
}

/* Syllables carrying dim render dim and are never re-timed */
.karaoke-syllable.is-dim,
.karaoke-syllable[data-dim="true"] {
  opacity: 0.45;
}

/* Suppress coarse gold line background & gold bar in syllable mode (LDR-05 / S8 groundtruth fix) */
.karaoke-line.karaoke-active,
.has-syllables .karaoke-line.karaoke-active,
.karaoke-line.has-syllables.karaoke-active,
#lyricsContainer.has-syllables .karaoke-line.karaoke-active {
  background: transparent !important;
  border-left: none !important;
  border-left-color: transparent !important;
  box-shadow: none !important;
  padding-left: 0.25rem !important;
}
.has-syllables .karaoke-word-active,
#lyricsContainer.has-syllables .karaoke-word-active {
  font-weight: inherit !important;
}
.has-anchored-chords .lyrics-line {
  padding-top: 1.35em;
}
.chord-anchor {
  position: absolute;
  top: -1.25em;
  left: 0;
  white-space: nowrap;
  pointer-events: auto;
}
.chord-anchor:focus-visible,
.chord-token:focus-visible {
  outline: 2px solid #d4af37;
  outline-offset: 2px;
  border-radius: 2px;
}

/* --- Breakpoint contract (issue #432 B-3) --------------------------------
   ONE documented set of viewport bands for the whole app. Every media
   query that switches layout MUST use these values; they are also the
   mirror of the JS constant MOBILE_MAX in web/js/chord-popover.js.

     --bp-sm:     640px  compact phone  (Tailwind `sm:`)
     --bp-mobile: 700px  the project MOBILE band (ZETA-4 touch targets,
                             the chord bottom sheet, the catalog 2-col flip)
     --bp-md:     800px  tablet         (supersedes Tailwind `md:` = 768px)
     --bp-lg:    1024px  desktop        (Tailwind `lg:`)

   TWO AXES, one source:
     * LAYOUT axis (--bp-mobile, 700px) — the mobile/desktop switch.
         mobile band  = @media (max-width: 700px)   (width <= 700)
         desktop band = @media (min-width: 701px)   (width >= 701)
       These are exact complements for integer CSS pixels: never both,
       never neither. The mobile band is INCLUSIVE of 700px because the
       JS mirror (MOBILE_MAX = 700, web/js/chord-popover.js) tests
       `winWidth <= MOBILE_MAX`; a CSS edge of 699px would put 700px in
       the desktop band in CSS while JS puts it in the mobile band —
       two different layouts at one width, the defect #432 B-1 names.
     * COMPACT axis (--bp-sm, 640px) — refinements INSIDE the mobile
       band (tighter type, clamped labels). It NEVER switches a layout
       by itself. Express it as `max-width: 639px`; do not pair it
       with a `min-width: 640px` layout rule, which would fire the
       mobile layout at the same width the compact rules stop.
     * Tailwind's stock `md:` (768px) is NOT a band of this app: use
       --bp-md (800px) or an explicit `min-[800px]:` arbitrary variant.
   The custom properties are the machine-readable copy; the media queries
   below cannot consume them (no var() in media conditions), so the
   literals are kept adjacent to this comment as the single source.
   -------------------------------------------------------------------- */
:root {
  --bp-sm: 640px;
  --bp-mobile: 700px;
  --bp-md: 800px;
  --bp-lg: 1024px;
}

/* --- Chord Popover (P1) --- */
/* RC-C: popover and toast must sit above .study-expander-overlay (z-70) — the
   popover was invisible when opened from inside the expander — but below
   #globalBottomBar (z-80). The toast's Tailwind z-[60] utility ties on
   specificity and the CDN-injected sheet lands after this file, so the toast
   override needs !important (UR8 convention for beating Tailwind utilities).
   #810: the popover's PAINT is the shared glass recipe (.glass-surface on
   the element, chord-popover.js) — the old standalone blur(12px) here was
   inert decoration over an opaque bg-stone-900 and is gone. */
#chordPopover {
  z-index: 75;
}
.daime-toast {
  z-index: 75 !important;
}
/* #432 B-1: the sheet's CSS must agree with the JS branch band.
   The JS takes the bottom-sheet branch for winWidth <= --bp-mobile (700)
   and signals it by adding the .chord-popover-bottom-sheet class, so the
   sheet's geometry + stacking are keyed on THAT CLASS, not on a second
   media band. A duplicated band is what caused the defect: the old rule
   lived in `@media (max-width: 639px)` while the JS branch ran to 700px,
   so the 640-699px strip fell back to the plain #chordPopover rule
   (z-index 75) while the bar sits at z-index 80 — the always-visible
   #globalBottomBar painted OVER the sheet and swallowed its voicing row.
   Keying on the class removes the second band: JS and CSS now share one
   decision, so they cannot drift again. */
#chordPopover.chord-popover-bottom-sheet {
  /* BUG-CHORD-01 RC-B: the sheet must clear the always-visible fixed
     #globalBottomBar (z-80): z-90 matches the bar-flyout convention. The
     bottom calc is the no-JS fallback only — chord-popover.js pins an inline
     bottom of the measured bar px (already including the bar's own
     safe-area padding), so this declaration must NOT be !important. */
  position: fixed !important;
  bottom: calc(var(--bar-h, 5rem) + env(safe-area-inset-bottom, 0px));
  left: 0 !important;
  right: 0 !important;
  top: auto !important;
  width: 100% !important;
  max-width: 100% !important;
  border-radius: 1.25rem 1.25rem 0 0 !important;
  border-bottom: 0 !important;
  z-index: 90 !important;
}

/* Issues #285/#286: chord popover viewport clamping + dark-theme diagrams */
#chordPopover {
  max-width: min(22rem, calc(100vw - 2rem));
}
#chordPopover uke-chord {
  /* uke-chord reads --uke-fill/--uke-text/--uke-fingertext;
     light theme keeps readable dark strokes on the light popover */
  --uke-fill: #1F2937;
  --uke-text: #1F2937;
  --uke-fingertext: #E5E7EB;
}
html.dark #chordPopover uke-chord,
html.dark .chord-strip-diagram uke-chord {
  --uke-fill: #E5E7EB;
  --uke-text: #E5E7EB;
  --uke-fingertext: #121814;
}

/* Issue #288: compact key verification badge (dot) */
.key-dot {
  display: inline-block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 9999px;
  margin-left: 0.375rem;
  vertical-align: middle;
}
.key-dot--verified {
  background: #22c55e;
  box-shadow: 0 0 2px rgba(34, 197, 94, 0.6);
}
.key-dot--unverified {
  background: #9ca3af;
  box-shadow: 0 0 2px rgba(156, 163, 175, 0.4);
}

/* RC-G: removed the inert light-DOM `uke-chord svg` descendant rules (#288) —
   the svg lives inside the element's shadow DOM so those selectors never
   matched; chord-diagrams.js sizes the shadow svg directly (host-level rules
   stay). Also: `.popover-diagram` never existed in the DOM; the live class is
   .chord-popover-diagram (chord-popover.js template). */
#chordPopover .chord-popover-voicing,
.chord-popover-diagram {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
}
/* Bottom-sheet: constrain diagram card and allow internal scroll.
   Keyed on the sheet CLASS (not a second media band) so the sheet's
   geometry can never drift from the JS branch that applies the class
   (issue #432 B-1). */
#chordPopover.chord-popover-bottom-sheet {
  overflow-y: auto;
  max-height: 70vh;
}
/* BUG-CHORD-01: clamp anchored chord labels on 320-375px so adjacent
   labels stop colliding. */
@media (max-width: 639px) {
  .chord-anchor {
    font-size: 0.72em;
    max-width: 96px;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* #432 B-2: the chord diagram must never paint outside its card.
   fitChordDiagram() (web/js/chord-diagrams.js) sizes the shadow svg to
   width/height 100%, but a percentage width against an INDEFINITE host
   box falls back to the CSS default object size (300x150) — the diagram
   then paints ~300px wide regardless of the card. The slot therefore gets
   a DEFINITE width (capped so the diagram keeps its proportions), and the
   <uke-chord> host is clamped to it. overflow:hidden is the safety net for
   a vendor svg that has not been re-fitted yet: it is clipped to the card
   instead of bleeding over its neighbours. */
.chord-popover-slot {
  width: 100%;
  max-width: 12rem;
}
#chordPopover .chord-popover-slot uke-chord {
  display: block;
  max-width: 100%;
  overflow: hidden;
}

/* Issue #202: per-note score highlight (verovio SVG, beat-synced) */
.score-svg g.note,
.score-svg g.chord {
  transition: fill 120ms ease, stroke 120ms ease;
}

.score-svg g.note.note-active,
.score-svg g.chord.note-active {
  fill: #e74c3c;
  stroke: #e74c3c;
}

/* Translucent surface + backdrop blur; border lives here so the glass tint
   and the hairline always stay in sync. #810: the bar paints the SAME
   --glass-* tokens as every other glass surface, so the material can no
   longer drift between the bar and the rest of the app.
   #818: like .glass-header, the bar HOST is transparent + filterless —
   the glass renders on ::before, so the .bar-popover flyouts
   (#barToolsMenu, #barWaveMenu, #globalVersionMenu, …) are never trapped
   in the bar's Backdrop Root and keep sampling the live page. */
.glass-bar {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

.glass-bar::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  border-radius: inherit;
  background: var(--glass-bg);
  border-top: 1px solid var(--glass-border);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  /* The scroll-glass handoff below animates the fill; the Tailwind
     transition on the host no longer has a background to transition. */
  transition: background-color 0.2s ease, border-color 0.2s ease;
}

/* #443/#810 scroll-glass — the contract player.js has signalled since #443
   (it toggles .is-scrolled on the bar and touches NOTHING else): while the
   page is scrolled the bar never hides and never moves, it only becomes
   MORE TRANSLUCENT — a measurably lighter alpha of the same material, in
   BOTH themes (--glass-bg-scrolled). #818: the fill lives on ::before, so
   the scrolled material is painted there too. */
.glass-bar.is-scrolled::before {
  background: var(--glass-bg-scrolled);
}

/* Solid-color fallback for engines without backdrop-filter (#208 note). */
@supports not ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))) {
  .glass-bar::before {
    background: #1c1917;
  }
  html:not(.dark) .glass-bar::before {
    background: #fff9e8;
  }
}

/* Transport cluster is the auto-sized center column of a 3-col grid:
   centered no matter how wide the left/right sections are (#208 addendum). */
.player-bar-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  column-gap: 0.5rem;
}

/* #432 slice B: the desktop 3-column grid belongs to the DESKTOP band
   (min-width: 701px). The old min-width: 640px edge put the desktop
   layout in the 640-700px window, where the centred transport cluster and
   the bar's right-hand control column share one row and collide (measured:
   #globalNextBtn / #globalLoopBtn overlapped the error toast by up to
   31px). The mobile band is --bp-mobile (700px) — see the breakpoint
   contract block in web/style.css; the JS mirror is MOBILE_MAX = 700
   (web/js/chord-popover.js), whose branch is INCLUSIVE (<=). */
@media (min-width: 701px) {
  .player-bar-grid {
    column-gap: 1rem;
  }
}

/* #432 slice B: the right-hand column is a flex row of popover buttons. It
   is a GRID TRACK (minmax(0, 1fr)), so without min-width: 0 its content
   (the error toast) can push the track wider than the column and overlap
   the centred transport cluster. */
.player-bar-grid > div:last-child {
  min-width: 0;
}

/* #432 slice B: the audio-error toast is a transient MESSAGE, not a bar
   control. It is a SIBLING of the grid row (web/js/player.js), anchored to
   the bar itself, so a long error message can never widen a grid track or
   land on the centred transport cluster (measured: in the right column's
   flex flow it overlapped #globalNextBtn / #globalLoopBtn by up to 31px at
   640/641/699/700px). It is clamped to the viewport so the message stays
   fully readable at every width. */
#globalErrorToast {
  position: absolute;
  right: 0.75rem;
  /* Fully clear of the bar: the transport buttons are 48px tall and their
     boxes overhang the bar's top edge by ~2px, so the toast must sit a
     full control-height above it, not just above the bar's box. */
  bottom: calc(100% + 0.75rem);
  max-width: min(22rem, calc(100vw - 1.5rem));
  z-index: 2;
}

#globalErrorMsg {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Spotify-style -10s/+10s skip buttons flanking play/pause (#208). */
.glass-skip-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 9999px;
  border: 1px solid rgba(212, 175, 55, 0.35);
  background: transparent;
  color: #d6d3d1;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  /* 0-slack: keeps the ring unclipped when rem font-scale inflates the box. */
  padding: 0.25rem;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}

.glass-skip-btn:hover {
  color: #ffffff;
  border-color: rgba(212, 175, 55, 0.8);
  background: rgba(212, 175, 55, 0.12);
}

html:not(.dark) .glass-skip-btn {
  color: #374151;
  border-color: rgba(138, 120, 74, 0.5);
}

html:not(.dark) .glass-skip-btn:hover {
  color: #1f2937;
  background: rgba(179, 143, 0, 0.12);
}


/* The neutral glass control (#810 tokenization). The FILL is the shared
   glass material — the same --glass-bg/--glass-border tokens every glass
   surface paints, in BOTH themes — so the header button, the player-bar
   button and every .glass-btn can no longer drift apart. Only the
   interaction states keep button-scoped tokens (--btn-glass-*-hover,
   --btn-glass-color*); the old --btn-glass-bg/--btn-glass-border fill
   duplicates were deleted in the #446 token block at the tail. */
.glass-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--btn-radius);
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  color: var(--btn-glass-color);
  cursor: pointer;
  transition: all 0.15s ease;
}

.glass-btn:hover {
  color: var(--btn-glass-color-hover);
  border-color: var(--btn-glass-border-hover);
  background: var(--btn-glass-bg-hover);
}

.glass-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Glass metadata chips (issue #343; #810 tokenization) — the chip paints
   the same --glass-* material as the surfaces it sits on. */
.glass-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.15rem 0.5rem;
  border-radius: 9999px;
  font-size: 0.6875rem; /* 11px */
  font-weight: 600;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  color: #e7e5e4;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: all 0.15s ease;
}

html:not(.dark) .glass-chip {
  color: #292524;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

/* #432 slice B: the player bar's MOBILE band is --bp-mobile (700px) — see
   the breakpoint contract block in web/style.css. The JS mirror is
   MOBILE_MAX = 700 (web/js/chord-popover.js) and its branch is INCLUSIVE
   (<=), so 700px belongs to the mobile band here too. The old
   max-width: 639px edge left the 640-700px window on the desktop
   3-column grid, where the centred transport cluster and the right-hand
   control column share one row and the controls overlap (measured:
   #globalNextBtn and #globalLoopBtn collided with the error toast by up to
   31px at 640/641/699/700px). */
@media (max-width: 700px) {
  .player-bar-container {
    padding-top: 0.35rem !important;
    padding-bottom: 0.5rem !important;
    padding-left: 0.75rem !important;
    padding-right: 0.75rem !important;
  }

  /* 2-row layout on mobile: Row 1 = Left Info + Right Controls, Row 2 = Centered Transport */
  .player-bar-grid {
    display: grid !important;
    grid-template-columns: minmax(0, 1fr) auto !important;
    row-gap: 0.35rem !important;
    column-gap: 0.5rem !important;
    align-items: center !important;
  }

  .player-bar-grid > div:first-child {
    grid-column: 1 / 2 !important;
    grid-row: 1 / 2 !important;
    min-width: 0 !important;
    max-width: 100% !important;
    overflow: hidden !important;
  }

  .player-bar-grid > div:last-child {
    grid-column: 2 / 3 !important;
    grid-row: 1 / 2 !important;
    justify-self: end !important;
    gap: 0.5rem !important;
    min-width: 0 !important;
  }

  #globalTransportCluster {
    grid-column: 1 / -1 !important;
    grid-row: 2 / 3 !important;
    justify-self: center !important;
    width: 100% !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    gap: 0.6rem !important;
  }

  #globalTransportCluster > button {
    padding: 0.3rem !important;
  }

  #globalPlayBtn {
    width: 2.75rem !important;
    height: 2.75rem !important;
    font-size: 1.15rem !important;
  }

  /* PB-01/PB-04(a) D2: skip buttons stay visible on phones (the desktop-only
     hide rule is gone); the previously competing sizing blocks are collapsed
     into this one scoped block. Width/height/svg need no !important (nothing
     competes); padding keeps it to beat #globalTransportCluster > button
     (0.3rem !important) declared above in this same media query. */
  #globalTransportCluster .glass-skip-btn {
    width: 2.25rem;
    height: 2.25rem;
    padding: 0.25rem !important;
  }

  #globalTransportCluster .glass-skip-btn svg {
    width: 1.25rem;
    height: 1.25rem;
  }

  #globalPrevBtn svg, #globalNextBtn svg {
    width: 1.35rem !important;
    height: 1.35rem !important;
  }

  #globalCoverThumb {
    width: 2.25rem !important;
    height: 2.25rem !important;
  }

  #globalHymnName {
    font-size: 0.82rem !important;
    line-height: 1.15rem !important;
  }

  #globalTimeLabel, #globalHinarioName {
    font-size: 0.7rem !important;
  }
}

/* Spotify-style marquee: pause-and-roll for overflowing hymn titles (#253).
   setupMarqueeObserver (player.js) measures overflow and sets
   --marquee-distance (px) / --marquee-duration; the animation only runs while
   the container carries .is-overflowing. */
.player-title-marquee-container {
  overflow: hidden;
  position: relative;
  white-space: nowrap;
  max-width: 100%;
  mask-image: linear-gradient(to right, black 85%, transparent 100%);
  -webkit-mask-image: linear-gradient(to right, black 85%, transparent 100%);
}

.marquee-content {
  display: inline-block;
  will-change: transform;
}

.player-title-marquee-container:not(.is-overflowing) .marquee-content {
  animation: none;
}

.player-title-marquee-container.is-overflowing {
  /* The fade mask lives on the container; drop it while the roll is active. */
  mask-image: none;
  -webkit-mask-image: none;
}

.player-title-marquee-container.is-overflowing .marquee-content {
  animation: marquee-pause-roll var(--marquee-duration, 10s) linear infinite;
}

@keyframes marquee-pause-roll {
  0%, 10% {
    transform: translateX(0);
  }
  90%, 100% {
    /* translateX percentage resolves against the span's own width, so the
       roll distance must come from --marquee-distance (scrollWidth -
       clientWidth) alone (#253 review). */
    transform: translateX(calc(-1 * var(--marquee-distance, 0px)));
  }
}

@media (prefers-reduced-motion: reduce) {
  .player-title-marquee-container.is-overflowing .marquee-content {
    animation: none;
    transform: none;
  }

  /* Keep the trailing fade mask when motion is reduced (must out-rank the
     .is-overflowing removal above). */
  .player-title-marquee-container.is-overflowing {
    mask-image: linear-gradient(to right, black 85%, transparent 100%);
    -webkit-mask-image: linear-gradient(to right, black 85%, transparent 100%);
  }
}

/* #252: track counter under the cover art (NN/TT or bare NN). */
.player-counter-text {
  font-family: var(--font-mono);
  font-size: 9px;
  line-height: 1;
  color: #78716c;
  text-align: center;
  min-width: 40px;
  white-space: nowrap;
}

/* #432 slice B: the counter steps up with the DESKTOP band (min-width:
   701px), matching the player-bar grid's band. */
@media (min-width: 701px) {
  .player-counter-text {
    font-size: 10px;
  }
}

/* Interactive scrubber track and knob holder (Spotify-style) */
.scrubber-track-container {
  height: 0.35rem;
  background: rgba(41, 37, 36, 0.7);
  cursor: pointer;
  position: relative;
  transition: height 0.15s ease;
  touch-action: none;
}

.scrubber-track-container:hover,
.scrubber-track-container.is-dragging {
  height: 0.55rem;
}

.scrubber-thumb-knob {
  position: absolute;
  top: 50%;
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 9999px;
  background: #ffffff;
  border: 2px solid #D4AF37;
  box-shadow: 0 1px 4px rgba(0,0,0,0.5);
  transform: translate(-50%, -50%) scale(0);
  transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: none;
  z-index: 10;
}

.scrubber-track-container:hover .scrubber-thumb-knob,
.scrubber-track-container.is-dragging .scrubber-thumb-knob {
  transform: translate(-50%, -50%) scale(1);
}

.scrubber-track-container.is-dragging .scrubber-thumb-knob {
  transform: translate(-50%, -50%) scale(1.25);
  box-shadow: 0 0 10px rgba(212, 175, 55, 0.8);
}
/* Issue #811: the legacy player-bar version selectors (global + mobile,
   both gone from the DOM since the #401 single-bar cutover) left orphaned
   CSS behind — deleted. The LIVE select family (grouped-card version
   selects included) is styled by the ONE .glass-select recipe in the
   glass section; .version-badge stays a mono chip. */

/* Issue #235: grouped hinario cards — version badge + in-card version select */
.version-badge {
  font-family: var(--font-mono);
  letter-spacing: 0.3px;
  line-height: 1.4;
  white-space: nowrap;
  /* #328: never let a long badge push past its card — clip in place. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* #328: containment for the details display mode (the LAST view in the
   switcher): the grid row and every card clip their contents, so cards with
   badges/version selects can never bleed over adjacent cards. */
.mode-details {
  min-width: 0;
  overflow: hidden;
}
.mode-details > * {
  min-width: 0;
  max-width: 100%;
}

/* #811: the card version select carries .glass-select for ALL of its
   paint/focus; this block keeps ONLY its geometry specifics. */
.hinario-version-select {
  width: 100%;
  max-width: 240px;
}

/* Card link intercepts clicks; the select must win over it */
a .hinario-version-select {
  position: relative;
  z-index: 2;
}


/* #432 slice B: the compact edge is max-width: 639px. The old
   max-width: 640px collided with the @media (min-width: 640px) bands at
   EXACTLY 640px, so 640px got the mobile rule AND the desktop rule at
   once (measured: the in-card select computed max-width 100% at 640px but
   240px at 641px — a 1px seam with a different rendered width either
   side). */
@media (max-width: 639px) {
  .hinario-version-select {
    max-width: 100%;
    width: 100%;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  }
}

/* --- Chord Strip (P2, #243) --- */
/* One row of distinct-chord cards above #lyricsContainer. Horizontal scroll
   on small screens; sticky above the lyrics card from 1024px up. */
#chordStrip {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  gap: 0.5rem;
  /* BUG-CHORD-01 RC-F: as a flex child the strip must be allowed to shrink
     below its content, else it inflates the horizontal scroll region. */
  min-width: 0;
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 0.5rem 0.625rem;
  background: var(--glass-bg); /* #810: shared glass material */
  border: 1px solid var(--glass-border);
  border-radius: 0.75rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(212, 175, 55, 0.4) transparent;
  /* Scroll affordance: right-edge fade hints that more cards exist. */
  -webkit-mask-image: linear-gradient(to right, black 92%, transparent 100%);
  mask-image: linear-gradient(to right, black 92%, transparent 100%);
}
#chordStrip.hidden {
  display: none;
}
@media (min-width: 1024px) {
  #chordStrip:not(.hidden) {
    position: sticky;
    /* #432 slice B: the strip is sticky BELOW the sticky #siteHeader, which
       is 62px tall in this band (z-index 50, measured). The old top: 0.5rem
       pinned the strip's top 54px UNDER the header, so the upper part of
       the strip was permanently covered once the page scrolled
       (elementFromPoint at the strip's top edge returned the header's
       search field). 4.25rem = 68px clears the 62px header plus its 1px
       border. The strip is sticky only at >=1024px, so this literal is
       exact for the only band where it applies; revisit if the header
       gains a taller variant. */
    top: 4.25rem;
    z-index: 30;
  }
}

.chord-strip-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  flex: 0 0 auto;
  min-width: 3.25rem;
  padding: 0.3rem 0.5rem 0.25rem;
  background: rgba(41, 37, 36, 0.85);
  border: 1px solid rgba(120, 113, 108, 0.5);
  border-radius: 0.5rem;
  color: #d6d3d1;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
}
.chord-strip-item:hover {
  border-color: rgba(212, 175, 55, 0.55);
  background: rgba(58, 52, 44, 0.9);
}
.chord-strip-item.is-active {
  border-color: rgba(212, 175, 55, 0.95);
  background: rgba(68, 59, 40, 0.95);
  box-shadow: 0 0 0 1px rgba(212, 175, 55, 0.55), 0 0 10px rgba(212, 175, 55, 0.25);
}
.chord-strip-item.is-active .chord-strip-name {
  color: #fbbf24;
}
.chord-strip-name {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.8rem;
  line-height: 1.1;
  color: #d4af37;
  white-space: nowrap;
  /* BUG-CHORD-01 RC-F: clamp long names so nowrap text stops bleeding
     across neighbouring strip cards. */
  max-width: 5.5rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chord-strip-diagram {
  margin-top: 0.15rem;
  height: 3.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.chord-strip-diagram uke-chord {
  display: block;
  width: 2.75rem;
  height: 3.4rem;
}
.chord-strip-nodiagram {
  font-size: 0.7rem;
  color: #78716c;
  line-height: 3.4rem;
}

/* Unified loading indicators (issue #316): spin circle for audio buffering
   / stem decoding and an indeterminate top bar for route transitions. */
@keyframes daime-spin {
  to { transform: rotate(360deg); }
}
.spinner {
  display: inline-block;
  width: 1.25rem;
  height: 1.25rem;
  flex: 0 0 auto;
  border: 2px solid rgba(212, 175, 55, 0.25);
  border-top-color: #d4af37;
  border-radius: 9999px;
  animation: daime-spin 0.8s linear infinite;
}
.stem-loading-spinner {
  width: 1rem;
  height: 1rem;
  border-width: 2px;
}
.route-loader {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 80;
  height: 3px;
  overflow: hidden;
  background: rgba(212, 175, 55, 0.15);
  pointer-events: none;
}
.route-loader.hidden {
  display: none;
}
.route-loader-bar {
  height: 100%;
  width: 40%;
  background: linear-gradient(90deg, rgba(212, 175, 55, 0.4), #f2c94c);
  border-radius: 9999px;
  animation: daime-route-slide 1.1s ease-in-out infinite;
}
@keyframes daime-route-slide {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(350%); }
}
@media (prefers-reduced-motion: reduce) {
  .spinner,
  .route-loader-bar {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* Study panel expander modal (issue #313; #810 glass): centered pop-up
   with a frosted backdrop hosting lyrics / sheet music / piano roll /
   chord strip. The SCRIM is the shared --modal-scrim material (one dim,
   one blur, every modal backdrop); the modal surface paints the --glass-*
   recipe (added to the #445 recipe list at the tail). The overlay sits at
   z-70 — DELIBERATELY below the chord popover (z-75), the bar (z-80) and
   its flyouts (z-90), so the study surface never covers the transport. */
.study-expander-overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  padding-bottom: max(5.5rem, calc(5rem + env(safe-area-inset-bottom, 0px)));
  background: var(--modal-scrim);
  -webkit-backdrop-filter: blur(var(--modal-scrim-blur)) saturate(140%);
  backdrop-filter: blur(var(--modal-scrim-blur)) saturate(140%);
}
.study-expander-overlay.hidden {
  display: none;
}
.study-expander-modal {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 60rem;
  max-height: min(92vh, 60rem);
  /* #810: the paint (translucent bg + blur + border) is the shared glass
     recipe — this selector is in the #445 recipe list at the tail. */
  border-radius: 1rem;
  box-shadow: 0 25px 60px rgba(0, 0, 0, 0.6);
  overflow: hidden;
}
.study-expander-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.6rem 0.9rem;
  background: rgba(18, 24, 20, 0.9);
  border-bottom: 1px solid rgba(212, 175, 55, 0.22);
}
.study-expander-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: 0.25rem;
}
.study-expander-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 0.5rem;
  color: #d6d3d1;
  background: var(--glass-bg); /* #810: shared glass material */
  border: 1px solid var(--glass-border);
  cursor: pointer;
  transition: color 150ms ease, border-color 150ms ease;
}
.study-expander-close:hover {
  color: #ffffff;
  border-color: rgba(212, 175, 55, 0.6);
}
.study-expander-close:focus-visible {
  outline: 2px solid rgba(212, 175, 55, 0.85);
  outline-offset: 1px;
}
.study-expand-trigger {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  z-index: 30;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border-radius: 0.5rem;
  color: #d6d3d1;
  background: var(--glass-bg); /* #810: shared glass material */
  border: 1px solid var(--glass-border);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  cursor: pointer;
  transition: color 150ms ease, border-color 150ms ease, transform 150ms ease;
}
.study-expand-trigger:hover {
  color: #fbbf24;
  border-color: rgba(212, 175, 55, 0.7);
  transform: scale(1.06);
}
.study-expand-trigger:focus-visible {
  outline: 2px solid rgba(212, 175, 55, 0.85);
  outline-offset: 1px;
}
.study-expand-trigger-inline {
  position: static;
  margin-left: auto;
  flex: 0 0 auto;
}
@media (max-width: 639px) {
  .study-expander-overlay {
    padding: 0.5rem;
    padding-bottom: max(5.5rem, calc(5rem + env(safe-area-inset-bottom, 0px)));
  }
  .study-expander-modal {
    max-height: calc(96vh - 5.5rem);
  }
}
@media (prefers-reduced-motion: reduce) {
  .study-expander-overlay,
  .study-expander-modal,
  .study-expand-trigger {
    transition: none !important;
  }
}

/* --- #432 slice E / #810: theme-aware boundaries for the study expander ---
   The paint now comes from the --glass-* / --modal-scrim tokens, which are
   theme-scoped, so the expander follows the app theme with no per-surface
   colour overrides: only the lighter shadow and the text colours differ. */
html:not(.dark) .study-expander-modal {
  box-shadow: 0 25px 60px rgba(15, 23, 42, 0.18);
}
html:not(.dark) .study-expander-header {
  background: #FFF9E8;
  border-bottom-color: rgba(138, 120, 74, 0.35);
}
html:not(.dark) .study-expander-close,
html:not(.dark) .study-expand-trigger {
  color: #1F2937;
}
html:not(.dark) .study-expander-close:hover {
  color: #1F2937;
  background: rgba(138, 120, 74, 0.16);
}
html:not(.dark) .study-expand-trigger {
  box-shadow: 0 1px 4px rgba(15, 23, 42, 0.12);
}
html:not(.dark) .study-expand-trigger:hover {
  color: #8a6d1a;
  border-color: rgba(138, 120, 74, 0.7);
}

/* --- #432 slice E: theme-aware boundaries for the chord-strip host -------
   The strip's base rules live at style.css:1379 (#chordStrip) and 1411
   (.chord-strip-item), which are slice B's range. This block sits after
   them, so the cascade resolves the light theme here without touching
   slice B's lines.

   Measured in the light theme before the fix: the strip host painted
   rgba(28,25,23,0.85) in BOTH themes and its text sat on that near-black
   strip at 1.1:1 -- effectively invisible. */
html:not(.dark) #chordStrip {
  background: #FFF9E8;
  border-color: rgba(138, 120, 74, 0.45);
  scrollbar-color: rgba(138, 120, 74, 0.45) transparent;
}
html:not(.dark) .chord-strip-item {
  background: #FFFFFF;
  border-color: rgba(138, 120, 74, 0.45);
  color: #374151;
}
html:not(.dark) .chord-strip-item:hover {
  background: rgba(138, 120, 74, 0.12);
  border-color: rgba(138, 120, 74, 0.6);
}
html:not(.dark) .chord-strip-item.is-active {
  background: rgba(138, 120, 74, 0.2);
  border-color: rgba(138, 120, 74, 0.95);
  box-shadow: 0 0 0 1px rgba(138, 120, 74, 0.45), 0 0 10px rgba(138, 120, 74, 0.2);
}
html:not(.dark) .chord-strip-item.is-active .chord-strip-name {
  color: #8a6d1a;
}
html:not(.dark) .chord-strip-name {
  color: #8a6d1a;
}
html:not(.dark) .chord-strip-nodiagram {
  color: #57534e;
}

/* Notation segmented control (Letters / Roman / Nashville) */
.notation-btn[aria-pressed="true"] {
  border-color: rgba(212, 175, 55, 0.85);
  color: #fbbf24;
  background: rgba(68, 59, 40, 0.6);
}

/* --- RBAC graft chrome (#355/#356): icon buttons, account menu, avatar, ---
    --- name-bleed guard, branded sign-in modal. Append-only per #358.   --- */

.nav-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 0.375rem;
  color: #d6d3d1;
  cursor: pointer;
  transition: color 0.2s ease, background-color 0.2s ease;
}
.nav-icon-btn:hover {
  color: #ffffff;
  background: rgba(22, 91, 51, 0.45);
}

.nav-icon,
.nav-menu-icon {
  display: inline-flex;
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
}
.nav-icon svg,
.nav-menu-icon svg {
  width: 100%;
  height: 100%;
}

/* Name-bleed guard (#355): display names clamp to a single ellipsized line
   no matter how extreme the stored value is; the full name rides on title. */
.nav-account-name {
  display: inline-block;
  max-width: 10rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* #432 slice B: compact edge is max-width: 639px (the old max-width:
   640px collided with the min-width: 640px bands at exactly 640px). */
@media (max-width: 639px) {
  .nav-account-name {
    max-width: 7rem;
  }
}

.nav-account-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  cursor: pointer;
  transition: color 0.2s ease;
}
.nav-account-trigger:hover {
  color: #d4af37;
}

.nav-avatar {
  position: relative;
  background-color: #44403c;
  font-weight: 700;
  letter-spacing: 0.02em;
  user-select: none;
}
.nav-avatar img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Profile/user dropdown (#355/#356) — admin entry lives here for staff.
   #810: the zero-blur 0.97-alpha slab is gone — the dropdown is a real
   glass surface (the shared --glass-* recipe; this selector is in the
   #445 recipe list at the tail, in BOTH themes). */
.nav-account-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 11rem;
  padding: 0.25rem;
  border-radius: 0.5rem;
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
  z-index: 60;
}
.nav-menu-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.45rem 0.6rem;
  border-radius: 0.375rem;
  font-size: 0.75rem;
  font-weight: 500;
  color: #d6d3d1;
  text-align: left;
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
}
html:not(.dark) .nav-menu-item {
  color: #44403c;
}
.nav-menu-item:hover {
  background: rgba(22, 91, 51, 0.35);
  color: #ffffff;
}
html:not(.dark) .nav-menu-item:hover {
  background: rgba(0, 122, 46, 0.12);
  color: #1f2937;
}
/* Multi-line clamp for menu labels (#355 name-bleed guard, two lines max). */
.nav-menu-label {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}

/* #629: waveform style options (desktop popover + mobile drawer chips).
   Static SVG previews only — the selected state mirrors the active
   language button family (gold border + gold text). */
.wave-style-option[aria-pressed="true"] {
  border-color: #d4af37;
  color: #d4af37;
  background: rgba(212, 175, 55, 0.08);
}
.wave-style-option[aria-pressed="true"] .wave-style-preview {
  outline: 1px solid rgba(212, 175, 55, 0.45);
  outline-offset: 1px;
  border-radius: 3px;
}
html:not(.dark) .wave-style-option[aria-pressed="true"] {
  background: rgba(212, 175, 55, 0.12);
}

/* Branded sign-in modal (#355/#356; #810 backdrop unification).
   The backdrop shares the ONE .modal-backdrop recipe (scrim tokens +
   z-95 + [hidden] toggle — see the #810 block at the tail). The card's
   paint is the #445 glass recipe (this selector is in the recipe list at
   the tail); the stale hardcoded rgba duplicates that used to fight it
   are deleted. */
.signin-modal-card {
  position: relative;
  width: 100%;
  max-width: 22rem;
  padding: 2.25rem 1.75rem 1.5rem;
  text-align: center;
  border-radius: 1rem;
  box-shadow: 0 28px 72px rgba(0, 0, 0, 0.6);
}
.signin-modal-close {
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 9999px;
  color: #a8a29e;
  cursor: pointer;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.signin-modal-close:hover {
  color: #ffffff;
  background: rgba(255, 255, 255, 0.08);
}
.signin-modal-logo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3.5rem;
  height: 3.5rem;
  margin: 0 auto 1rem;
  border-radius: 9999px;
  background: rgba(22, 91, 51, 0.35);
  border: 1px solid rgba(212, 175, 55, 0.45);
  box-shadow: inset 0 0 18px rgba(0, 0, 0, 0.35);
  padding: 0.35rem;
}
.signin-modal-logo img,
.signin-modal-logo svg {
  width: 100%;
  height: 100%;
}
.signin-modal-card input,
.modal-glass-input {
  background: rgba(18, 24, 20, 0.75);
  border: 1px solid rgba(212, 175, 55, 0.3);
  border-radius: 0.5rem;
  color: #e7e5e4;
  padding: 0.5rem 0.75rem;
  font-size: 0.875rem;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.signin-modal-card input:focus,
.modal-glass-input:focus {
  outline: none;
  border-color: #d4af37;
  box-shadow: 0 0 0 1px #d4af37;
}

.signin-modal-title {
  font-size: 1.25rem;
  font-weight: 700;
  color: #f5f5f4;
  letter-spacing: -0.01em;
}
html:not(.dark) .signin-modal-title {
  color: #1f2937;
}
.signin-modal-subtitle {
  margin: 0.5rem 0 1.25rem;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: #a8a29e;
}
html:not(.dark) .signin-modal-subtitle {
  color: #57534e;
}
.signin-modal-primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.65rem 1rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: #12181a;
  background: linear-gradient(135deg, #d4af37, #f2c94c);
  border: 1px solid rgba(212, 175, 55, 0.6);
  border-radius: 0.625rem;
  cursor: pointer;
  transition: filter 0.15s ease, transform 0.15s ease;
}
.signin-modal-primary:hover {
  filter: brightness(1.07);
}
.signin-modal-primary:active {
  transform: translateY(1px);
}
.signin-modal-divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 1.1rem 0;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: #78716c;
}
.signin-modal-divider::before,
.signin-modal-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: rgba(212, 175, 55, 0.25);
}
.signin-modal-oauth {
  display: flex;
  gap: 0.5rem;
}
.oauth-placeholder {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  padding: 0.55rem 0.4rem;
  border: 1px solid rgba(212, 175, 55, 0.22);
  border-radius: 0.5rem;
  background: rgba(255, 255, 255, 0.03);
  opacity: 0.55;
  cursor: not-allowed;
}
.oauth-placeholder .oauth-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 9999px;
  font-size: 0.8125rem;
  font-weight: 700;
  color: #e7e5e4;
  background: rgba(255, 255, 255, 0.08);
}
.oauth-placeholder .oauth-glyph-camera {
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 0;
  background: transparent;
}
.oauth-placeholder .oauth-name {
  font-size: 0.6875rem;
  color: #a8a29e;
}
.signin-modal-foot {
  margin-top: 1.1rem;
  font-size: 0.6875rem;
  line-height: 1.5;
  color: #78716c;
}

/* --- #432 slice E: theme-aware boundaries for the sign-in modal ----------
   The card already had a light-theme background, but its INNER chrome
   (close button, divider, OAuth placeholders, footer) kept the dark
   palette's colors. Measured in the light theme: the footer and the
   divider sat at 4.02:1 on the cream card and the OAuth glyph at 1.07:1
   -- below WCAG AA (4.5:1) and, for the glyph, effectively invisible.

   The overrides mirror the app's existing `html:not(.dark) <selector>`
   token pattern. */
/* #810: the light-theme backdrop dim is the --modal-scrim token (dark
   slate 0.45) — no per-modal override needed anymore. */
html:not(.dark) .signin-modal-close {
  color: #57534e;
}
html:not(.dark) .signin-modal-close:hover {
  color: #1F2937;
  background: rgba(138, 120, 74, 0.16);
}
html:not(.dark) .signin-modal-card input,
html:not(.dark) .modal-glass-input {
  background: #FFFFFF;
  border-color: rgba(138, 120, 74, 0.45);
  color: #1F2937;
}
html:not(.dark) .signin-modal-card input::placeholder {
  color: #78716c;
}
html:not(.dark) .signin-modal-divider {
  color: #57534e;
}
html:not(.dark) .signin-modal-divider::before,
html:not(.dark) .signin-modal-divider::after {
  background: rgba(138, 120, 74, 0.35);
}
html:not(.dark) .oauth-placeholder {
  background: rgba(138, 120, 74, 0.08);
  border-color: rgba(138, 120, 74, 0.35);
}
html:not(.dark) .oauth-placeholder .oauth-glyph {
  color: #1F2937;
  background: rgba(138, 120, 74, 0.16);
}
html:not(.dark) .oauth-placeholder .oauth-name {
  color: #57534e;
}
html:not(.dark) .signin-modal-foot {
  color: #57534e;
}

/* ==========================================================================
   QA-swarm chrome fixes (BETA-1/2 swarm follow-ups: ZETA-1/2/4/6)
   ========================================================================== */

/* --- ZETA-2: the header must shrink-fit 360px viewports (#373) ------------- */
/* With html/body overflow-x:hidden dropped (ZETA-3, sticky header), the
   header itself must never exceed the viewport or overflow the row. */
#siteHeader {
  width: 100%;
  max-width: 100vw;
  min-width: 0;
}
#siteHeader .search-container,
#siteHeader #searchContainer {
  min-width: 200px;
  flex-shrink: 0;
}
@media (max-width: 639px) {
  #siteHeader .search-container,
  #siteHeader #searchContainer {
    min-width: 0;
  }
}
#siteHeader #globalSearch {
  min-width: 0;
  width: 100%;
}

/* --- ZETA-6: home-indicator / gesture-bar safe area ------------------------ */
/* Both fixed bottom bars sit flush with the screen bottom; on iPhone-class
   devices the home indicator otherwise covers the most-used controls. */
#globalBottomBar {
  z-index: 80;
}
#globalBottomBar {
  padding-bottom: max(0.75rem, env(safe-area-inset-bottom, 0px));
}

/* --- ZETA-1 (retired with #401): the global bottom bar is now the single
   universal player surface on ALL viewports; the legacy mobile bar and the
   html-level suppression marker were removed. */

/* --- ZETA-4: 44px effective touch targets (WCAG 2.5.8 / Apple HIG 44pt) ---- */
/* Selects and labelled toggles grow to a 44px min hit height; the tiny icon
   steppers keep their compact glass look and get an invisible ::after hit
   area instead (no visual bloat, no adjacent-target overlap). */
@media screen and (max-width: 700px), (pointer: coarse) {
  #selectCapo,
  #likeBtn,
  #mixerResetBtn,
  #mixerLoopBtn,
  #btnAutoScroll,
  #btnToggleChords,
  #btnChordStrip,
  .notation-btn,
  #tabLyrics,
  #tempoValueLabel,
  #displayModeSwitcher button {
    min-height: 44px;
  }

  #btnTransposeDown,
  #btnTransposeUp,
  #btnAudioFollowsTom,
  #tempoDownBtn,
  #tempoUpBtn,
  #btnFontSmaller,
  #btnFontBigger,
  #mixerPrevBtn,
  #mixerNextBtn,
  #mixerStopBtn {
    position: relative;
  }
  #btnTransposeDown::after,
  #btnTransposeUp::after,
  #btnAudioFollowsTom::after,
  #tempoDownBtn::after,
  #tempoUpBtn::after,
  #btnFontSmaller::after,
  #btnFontBigger::after,
  #mixerPrevBtn::after,
  #mixerNextBtn::after,
  #mixerStopBtn::after {
    content: '';
    position: absolute;
    top: -10px;
    bottom: -10px;
    left: -8px;
    right: -8px;
  }
}


/* ---- #401 v78.1: player-bar reference chips + tools-menu polish ---------- */
.bar-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  height: 2rem;
  padding: 0 0.65rem;
  border-radius: 9999px;
  /* #810: the bar chips paint the shared glass material (same tokens as
     .glass-chip) so the chip family cannot drift from the app material. */
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: rgb(214 211 209);                        /* stone-300 */
  cursor: default;
  user-select: none;
  white-space: nowrap;
  transition: border-color 200ms ease, color 200ms ease, transform 200ms ease;
}
.bar-chip .bar-chip-arrow { color: rgb(120 113 108); font-size: 0.625rem; }
.bar-chip .bar-chip-delta { color: rgb(196 178 108); }  /* daime gold, dimmed */
.bar-chip-active { border-color: rgb(196 178 108 / 0.65); color: rgb(231 229 228); }
.bar-chip-active .bar-chip-delta { color: rgb(217 194 106); }
.bar-chip-tone .bar-chip-key { color: rgb(231 229 228); }
.bar-chip-tone .bar-chip-key-now { color: rgb(217 194 106); }
.bar-chip-tuning { border-color: rgb(110 181 167 / 0.6); }
.bar-chip-tuning .bar-chip-hz { color: rgb(148 210 198); }

/* tools menu: open animation + smooth hover */
.bar-tools-menu { animation: barToolsMenuIn 180ms cubic-bezier(0.16, 1, 0.3, 1); transform-origin: bottom center; }
@keyframes barToolsMenuIn {
  from { opacity: 0; transform: translateY(6px) translateX(-50%) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) translateX(-50%) scale(1); }
}
@media (min-width: 701px) {
  .bar-tools-menu { animation-name: barToolsMenuInSm; }
  @keyframes barToolsMenuInSm {
    from { opacity: 0; transform: translateY(6px) scale(0.96); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
  }
}
.tools-num { font-variant-numeric: tabular-nums; }

/* wrench open state mirrors the gold hover */
#barToolsBtn[aria-expanded="true"] { color: rgb(217 194 106); border-color: rgb(196 178 108 / 0.65); }

/* retune chips pressed state (menu, live in player.js template) */
#barToolsMenu .retune-chip[aria-pressed="true"],
#barToolsMenu .retune-off-btn[aria-pressed="true"] {
  color: rgb(217 194 106);
  border-color: rgb(196 178 108 / 0.65);
  background: rgb(196 178 108 / 0.10);
}

/* ---- #411/#412/#413: bar popovers — animation, clamping, sub-menus, light,
   vertical volume --------------------------------------------------------- */

/* UR4: open/close animation for ALL bar popovers (tools, version,
   playlist, volume). #810: every flyout carries .bar-popover (the mode
   dropdown was removed by #814; its dead selectors are gone). The PAINT
   is the shared token recipe — see the #810 glass block at the tail. */
.bar-popover {
  will-change: transform, opacity;
}
.bar-popover.bar-popover-open {
  animation: barPopoverIn 180ms cubic-bezier(0.16, 1, 0.3, 1);
  transform-origin: bottom center;
}
.bar-popover.bar-popover-closing {
  animation: barPopoverOut 160ms ease-in forwards;
}
@keyframes barPopoverIn {
  from { opacity: 0; transform: translateY(8px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes barPopoverOut {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(8px) scale(0.96); }
}
@media (prefers-reduced-motion: reduce) {
  .bar-popover.bar-popover-open,
  .bar-popover.bar-popover-closing {
    animation: none;
  }
}

/* UR2: mobile sub-menu slides; >=sm flat */
.bar-tools-frame { min-height: 0; }
.bar-tools-sub {
  position: absolute;
  inset: 0;
  transform: translateX(100%);
  opacity: 0;
  /* C5 a11y: hidden subs must leave the a11y tree; visibility flips AFTER
     the exit slide (delay = longest transition) so the animation stays
     visible, and instantly on entry (see open-state rule below). */
  visibility: hidden;
  pointer-events: none;
  /* C2: sub content taller than the frame scrolls instead of clipping. */
  overflow-y: auto;
  transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1), opacity 160ms ease,
    visibility 0s linear 200ms;
}
.bar-tools-menu[data-panel="tone"] .bar-tools-sub[data-panel="tone"],
.bar-tools-menu[data-panel="tempo"] .bar-tools-sub[data-panel="tempo"],
.bar-tools-menu[data-panel="retune"] .bar-tools-sub[data-panel="retune"],
.bar-tools-menu[data-panel="mode"] .bar-tools-sub[data-panel="mode"] {
  transform: translateX(0);
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  /* Entering: no delay on visibility so the slide-in is never blank. */
  transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1), opacity 160ms ease,
    visibility 0s;
}
.bar-tools-menu[data-panel="tone"] .bar-tools-root,
.bar-tools-menu[data-panel="tempo"] .bar-tools-root,
.bar-tools-menu[data-panel="retune"] .bar-tools-root,
.bar-tools-menu[data-panel="mode"] .bar-tools-root {
  transform: translateX(-24%);
  opacity: 0.35;
  pointer-events: none;
}
.bar-tools-root { transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1), opacity 160ms ease; }
.bar-tools-back { cursor: pointer; }
/* Match the chord focus-visible convention: visible gold ring on keyboard
   focus for the tools nav rows and the glass buttons. */
.bar-tools-nav:focus-visible,
.glass-btn:focus-visible {
  outline: 2px solid #d4af37;
  outline-offset: 2px;
  border-radius: 2px;
}
/* #432 slice B: the flat (desktop) tools panel belongs to the DESKTOP band
   (min-width: 701px) — the exact complement of the mobile band
   (--bp-mobile, 700px; see the breakpoint contract block in
   web/style.css). */
@media (min-width: 701px) {
  .bar-tools-frame { position: static; overflow: visible; }
  .bar-tools-root { display: none; }
  .bar-tools-sub {
    position: static;
    inset: auto;
    transform: none;
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    padding-bottom: 0.25rem;
  }
}

/* UR5: mobile sheet + desktop anchor
   #432 slice B: the sheet band is --bp-mobile (700px) — see the breakpoint
   contract block in web/style.css — matching the JS mobile branch
   (MOBILE_MAX = 700, INCLUSIVE). The old max-width: 639px edge left
   640-700px on the desktop popover, so the same "mobile" width produced
   two different tools layouts — and the desktop popover overlapped the
   player bar (measured: sheet.bottom 618 > bar.top 593). */
@media (max-width: 700px) {
  /* absolute anchors to the small tools wrapper — the sheet must break out
     to the viewport: position:fixed above the (fixed) player bar.
     PB-04 C3: anchor to the live measured bar height (--bar-h, published by
     player.js) instead of a magic rem constant, so taller bars (font scale,
     safe area) can never overlap the sheet. */
  .bar-tools-menu {
    position: fixed !important;
    left: 0.5rem !important;
    right: 0.5rem !important;
    bottom: calc(var(--bar-h, 8.75rem) + 0.25rem) !important;
    width: auto !important;
    transform: none !important;
  }
}

/* #413: vertical volume slider */
input[type="range"].vol-vertical {
  writing-mode: vertical-lr;
  direction: rtl;
  width: 2.25rem;
  height: 8.5rem;
  background: transparent;
  border-radius: 0;
}
input[type="range"].vol-vertical::-webkit-slider-runnable-track { width: 6px; border-radius: 9999px; background: #44403c; }
input[type="range"].vol-vertical::-webkit-slider-thumb { width: 22px; height: 22px; border-radius: 9999px; background: #d4af37; border: 2px solid #1c1917; margin-left: -8px; }
input[type="range"].vol-vertical::-moz-range-track { width: 6px; border-radius: 9999px; background: #44403c; }
input[type="range"].vol-vertical::-moz-range-thumb { width: 22px; height: 22px; border-radius: 9999px; background: #d4af37; border: 2px solid #1c1917; }

/* UR8 + #412 + #810 + #813: light theme for the player surfaces.
   #810 deleted the per-id SURFACE slabs (0.94-alpha lookalike bg +
   border) — every flyout now paints the shared --glass-* token recipe
   (.bar-popover, #445 recipe list at the tail), which is theme-scoped,
   so no per-id background/border patch can drift again. #813 then moved
   the hover/slash/border UTILITY flips (hover:bg-stone-800, /NN alphas,
   border-stone-*) into the global html:not(.dark) token block in
   web/index.html, so the per-container hover/border/bg patches that
   survived only .bar-tools-menu died. What remains here are the TEXT
   refinements (mutter stone-500-ish greys the global block's
   text-stone-300/400 flip does not aim for) and the #813 content-specific
   paints (scrubber rail, error toast, puxada badge) that are not
   stone-utility tokens at all. */
html:not(.dark) .bar-popover { color: #57534e; }
html:not(.dark) .bar-tools-menu .text-stone-300,
html:not(.dark) .bar-tools-menu .text-stone-400,
html:not(.dark) #globalVolumePopover .text-stone-300,
html:not(.dark) #globalPlaylistDropdown .text-stone-300 { color: #78716c !important; }
html:not(.dark) .bar-tools-menu .text-daime-gold,
html:not(.dark) #globalVolumePopover .text-daime-gold,
html:not(.dark) .bar-chip .bar-chip-delta,
html:not(.dark) .bar-chip-active .bar-chip-delta { color: #8a6d1a !important; }
/* #813: the bar pieces whose paint is CONTENT-specific, not a stone
   utility — the seek rail (CSS rgba, issue-named "scrubber rail stays
   dark"), the audio-error toast (red-950/80 family) and the puxada
   Manual badge (amber-900/80 + amber-300). Light values from the same
   palette: bronze rail, the light red the mq surfaces use (#b91c1c),
   the light gold (#8a6d1a). */
html:not(.dark) .scrubber-track-container { background: rgba(138, 120, 74, 0.45); }
html:not(.dark) #globalErrorToast {
  background: rgba(185, 28, 28, 0.10) !important;
  border-color: rgba(185, 28, 28, 0.40) !important;
  color: #b91c1c !important;
}
html:not(.dark) #globalErrorToast #globalRetryBtn:hover { color: #7f1d1d !important; }
html:not(.dark) #globalErrorToast #globalSkipBtn { color: #8a6d1a !important; }
html:not(.dark) #globalErrorToast #globalSkipBtn:hover { color: #b45309 !important; }
html:not(.dark) #globalPuxadaBadge {
  background: rgba(180, 140, 20, 0.20) !important;
  color: #8a6d1a !important;
}
/* #810: the bar chips paint the shared glass material; only text colours
   stay chip-specific in the light theme. */
html:not(.dark) .bar-chip .bar-chip-key { color: #44403c; }
html:not(.dark) .bar-chip .bar-chip-arrow { color: #a8a29e; }
html:not(.dark) .bar-chip-tuning .bar-chip-hz { color: #0f766e; }
html:not(.dark) .bar-chip-tuning { border-color: rgba(15, 118, 110, 0.45); }
html:not(.dark) #barToolsMenu .retune-chip[aria-pressed="true"],
html:not(.dark) #barToolsMenu .retune-off-btn[aria-pressed="true"] {
  color: #8a6d1a;
  border-color: rgba(138, 120, 74, 0.6);
  background: rgba(138, 120, 74, 0.12);
}
/* #810: the flyouts' no-backdrop-filter fallback is the shared one —
   the glass @supports-not block at the tail re-points --glass-bg to an
   opaque solid, and .bar-popover consumes the token, so nothing per-id
   is needed here anymore. */

/* ---- #414: IA redesign — info line, cover badge, version menu, stems rows -- */
#globalInfoLine span + span::before { content: '·'; margin-right: 0.375rem; color: rgb(120 113 108); }
html:not(.dark) #globalInfoLine { color: #78716c; }
html:not(.dark) #globalInfoLine #infoKeyNow { color: #8a6d1a; }
html:not(.dark) #globalInfoLine #infoTuningNow { color: #0f766e; }
/* #810: #globalVersionMenu carries .bar-popover — its surface paint is the shared token recipe. #813: its hover flips moved to the global index.html token block. */
html:not(.dark) #globalVersionMenu .text-stone-300 { color: #78716c !important; }
html:not(.dark) #origSpecs { color: #78716c; }

/* #414: popovers must never exceed the viewport on short/landscape screens */
.bar-tools-menu { max-height: min(26rem, calc(100vh - 11rem)); overflow-y: auto; }
.bar-tools-frame { max-height: none; }
#globalVolumePopover { max-height: min(24rem, calc(100vh - 10rem)); overflow-y: auto; }
#globalVersionMenu { max-height: min(18rem, calc(100vh - 10rem)); overflow-y: auto; }

/* #414 CR6: 390px fit — tighten transport paddings so the center cluster
   never bleeds into the right controls column. (Skip-button sizing was
   collapsed into the single scoped block in the mobile media query above;
   PB-01/PB-04(a) D2 removed the phone hide rule.) */
@media (max-width: 639px) {
  #globalTransportCluster > button { padding: 0.375rem !important; }
}

@media (max-width: 639px) {
  /* loop cedes its slot on phones: mode cycling stays on desktop/tablet;
     the tools menu is the single controls surface on mobile (#414 CR3). */
  #globalLoopBtn { display: none; }
}

/* ==========================================================================
   #446 — ONE button style contract (radius, padding, fill)
   ==========================================================================
   Operator voice note (21-Oct-2026), issue #446:

     "In terms of buttons we have a little bit of a mix: some of them are
      squared, some of them are rounded, some of them are not rounded. We
      need some consistency here with buttons and the like button and the
      heart. We need a little bit of consistency all across the UI when it
      comes to buttons and displays because it's all around the place
      right now."

   The defect, MEASURED on the shipped app before this block: the SAME
   family of controls resolved to six different radii, depending on which
   file happened to declare them —

     4px   .mode-btn / .notation-btn / .autoscroll-toggle-btn / A- A+ steppers
     6px   .nav-icon-btn / .nav-menu-item / #likeBtn / #mixerResetBtn
     8px   .glass-btn / the mixer transport / the study tabs
     10px  .signin-modal-primary
     0px   .mq-btn / .btn-move-top / .btn-stem-thumb-* / the toast text buttons
     9999px the transport round buttons / the filter pills

   and the like/heart button the operator named was one of the 6px
   outliers while the player-bar twin of the very same control was a
   9999px pill. Six radii for one app, and the operator read it as
   "squared, rounded, not rounded".

   THE CONTRACT (normative; web/tests/test_button_style_contract.py is the
   executable copy of this section):

   1. RADIUS — exactly three radius CLASSES, and a control belongs to
      exactly one of them:

        control  var(--btn-radius)        0.5rem (8px)
                 THE DEFAULT. Every action button, tab, stepper, chip
                 button, menu row and CTA resolves to this one value.

        pill     var(--btn-radius-pill)    9999px
                 RESERVED for controls whose SHAPE is the point: the
                 transport round buttons, the play buttons, the filter
                 pills, the like/heart discs, the small overlay badges.
                 A pill is a circle or a stadium, never a rectangle with
                 big corners.

        square   var(--btn-radius-square)  0px
                 THE ONE square exception. It is reserved for the dense
                 inline GLYPH tiles of the mixer stem rows (the up / down /
                 move-to-top arrows): a square silhouette is what tells an
                 inline glyph in a tight row apart from a button the user
                 aims at, and squaring them keeps the row visually quiet.
                 This is the ONLY square family in the app — every other
                 control is control or pill.

      The three classes are applied by the TAIL CASCADE below, not by
      editing the per-family rules in place: the tail wins on source
      order and on specificity, so every family that already existed
      keeps its markup, its behaviour and its owners.

   2. PADDING — three label PROFILES, and the ICON controls keep their own
      square box (an icon control carries no label, so a padding token
      would fight its fixed width/height):

        var(--btn-pad-x) / var(--btn-pad-y)         0.625rem / 0.25rem
                 the STANDARD label control
        var(--btn-pad-x-sm) / var(--btn-pad-y-sm)   0.5rem / 0.125rem
                 the COMPACT label control (the A- / A+ / A-G steppers)
        var(--btn-pad-x-lg) / var(--btn-pad-y-lg)   1rem / 0.5rem
                 the HERO label control (the full-width modal CTAs)

      A control keeps its SIZE TIER; the contract pins the tier to a
      token, so a tier can never drift between two families again.

   3. FILL — two MATERIALS, and the choice between them is semantic:

        the GLASS material is the DEFAULT for NEUTRAL controls: the header
                 and player-bar glass buttons, and the OFF state of the
                 like/heart button. #810 folded the old --btn-glass-bg /
                 --btn-glass-border duplicates into the sitewide
                 --glass-bg / --glass-border tokens (#445 block at the
                 tail), so the button fill is LITERALLY the same paint as
                 every glass surface, in both themes; only the interaction
                 states keep button-scoped tokens (--btn-glass-*-hover,
                 --btn-glass-color*).

        the SEMANTIC material (brand green, brand gold, the state rose)
                 is for controls that CARRY MEANING: the primary CTA, the
                 active tab, the LIKED state of a heart, the destructive
                 and the review actions. These are deliberately NOT glass
                 — a saturated fill painted on a glass surface reads as a
                 mistake, and the liked/unliked heart must stay legible
                 at a glance.

      The SEMANTIC fills are therefore EXEMPT from the shared glass
      material: only the like/heart button's NEUTRAL off state is
      re-pointed at the glass token, and its liked state keeps the
      semantic rose.

   The tokens are declared EXACTLY ONCE, in the single :root block below,
   so no theme and no component can re-declare them and let them drift.
   That :root block is the machine-readable copy of this comment; the
   html:not(.dark) block below it carries the light-theme palette only.
   The light-theme values are the SAME computed colours the per-family
   rules produced before this block — the contract reorganises the
   palette, it does not repaint it.
   -------------------------------------------------------------------- */
:root {
  /* --- radius: the three classes of the contract ------------------------ */
  --btn-radius: 0.5rem;          /* control — the default */
  --btn-radius-pill: 9999px;     /* pill    — circles and stadiums */
  --btn-radius-square: 0px;      /* square  — the one documented exception */

  /* --- padding: the three label profiles -------------------------------- */
  --btn-pad-x: 0.625rem;
  --btn-pad-y: 0.25rem;
  --btn-pad-x-sm: 0.5rem;
  --btn-pad-y-sm: 0.125rem;
  --btn-pad-x-lg: 1rem;
  --btn-pad-y-lg: 0.5rem;

  /* --- fill: the INTERACTION states of the shared GLASS material ----------
     #810: the FILL itself is no longer a button-scoped duplicate — the
     neutral controls paint the sitewide --glass-bg / --glass-border
     tokens (the #445 block at the tail), in BOTH themes. What stays
     button-scoped are the INTERACTION states (the hover fill/border and
     the label colours), which have no glass-token equivalent. */
  --btn-glass-bg-hover: rgba(212, 175, 55, 0.15);
  --btn-glass-border-hover: rgba(212, 175, 55, 0.7);
  --btn-glass-color: #d6d3d1;
  --btn-glass-color-hover: #ffffff;
}
html:not(.dark) {
  /* The parchment palette of the INTERACTION states. Only these are
     re-declared here: the fill comes from the theme-scoped --glass-*
     tokens, and the radius/padding are theme-independent. */
  --btn-glass-bg-hover: rgba(179, 143, 0, 0.15);
  --btn-glass-border-hover: rgba(138, 120, 74, 0.7);
  --btn-glass-color: #57534e;
  --btn-glass-color-hover: #1c1917;
}

/* --- 1. radius: the CONTROL default ---------------------------------------
   Any button that carries a Tailwind radius utility (rounded / -md / -lg
   / -xl / -2xl / -full) is a CONTROL unless the PILL family below claims
   it. The attribute selector is what gives this rule enough specificity
   to beat the Tailwind utility (0,1,1 vs 0,1,0) WITHOUT editing the
   in-place family rules — the whole point of the tail cascade. */
button[class*="rounded"] {
  border-radius: var(--btn-radius);
}

/* The families whose radius comes from a CLASS rule rather than from a
   Tailwind utility: they are named here so they read from the token. */
button:is(.glass-btn,
          .nav-icon-btn,
          .nav-menu-item,
          .signin-modal-primary,
          .oauth-placeholder,
          .study-expand-trigger,
          .study-expander-close,
          .mq-btn) {
  border-radius: var(--btn-radius);
}
/* The same for the few controls that carry neither a radius utility nor a
   radius class of their own. */
button#globalRetryBtn,
button#globalSkipBtn,
button#barToolsCloseBtn,
button#mobileThemeToggle,
button#btnNotationRoman,
button#btnNotationNashville,
button[data-lang-btn] {
  border-radius: var(--btn-radius);
}

/* --- 2. radius: the PILL family -------------------------------------------
   Circles and stadiums only. These are the controls whose SHAPE carries
   the meaning (transport, play, the filter chips, the like/heart disc),
   so they are exempt from the control default. The ID selectors are the
   ones that also carry a Tailwind radius utility, and they outrank the
   control default above; the class selectors cover the pill families
   that have no id of their own. */
button#globalLikeBtn,
button#globalPlayBtn,
button#globalPrevBtn,
button#globalNextBtn,
button#globalBack10Btn,
button#globalFwd10Btn,
button#barToolsBtn,
button#globalCoverBadge,
button#globalVolumeMuteRow,
button#barBtnTransposeDown,
button#barBtnTransposeUp,
button#barTempoDownBtn,
button#barTempoUpBtn,
button#barBtnRetuneOff,
button#signinModalClose,
button#mixerPlayBtn,
button#pianoRollPlayBtn,
button#harpPlayBtn {
  border-radius: var(--btn-radius-pill);
}
button[class]:is(.filter-pill,
                 .play-row-btn,
                 .retune-chip,
                 .retune-off-btn,
                 .glass-skip-btn) {
  border-radius: var(--btn-radius-pill);
}

/* --- 3. radius: the ONE square exception ---------------------------------
   The dense inline glyph tiles of the mixer stem rows. A square
   silhouette marks them as inline glyphs rather than as buttons the
   user aims at. No other control in the app is square. */
button[class]:is(.btn-move-top,
                 .btn-stem-thumb-up,
                 .btn-stem-thumb-down) {
  border-radius: var(--btn-radius-square);
}

/* --- 4. padding: the three label profiles --------------------------------
   The standard profile is the ordinary label control, the compact
   profile is the stepper, the hero profile is the full-width modal CTA.
   The ICON controls are deliberately not listed: they keep their own
   square box (a fixed width/height), because a padding token would
   fight it. */
#likeBtn,
#mixerResetBtn,
#mixerLoopBtn,
#btnToggleChords,
#btnChordStrip,
#langMenuBtn {
  padding: var(--btn-pad-y) var(--btn-pad-x);
}
#btnFontSmaller,
#btnFontBigger,
#btnNotationLetters {
  padding: var(--btn-pad-y-sm) var(--btn-pad-x-sm);
}
#signinModalPrimary,
#upgradeModalDismiss,
#upgradeModalCouponCta,
#couponSubmit {
  padding: var(--btn-pad-y-lg) var(--btn-pad-x-lg);
}

/* --- 5. fill: the shared GLASS material -----------------------------------
   #810: the neutral FILL is the sitewide --glass-bg / --glass-border
   material (the .glass-btn base rule paints it, both themes); only the
   interaction states are button-scoped. The like/heart button's NEUTRAL
   off state paints the same material, and its LIKED state keeps the
   semantic rose (the liked heart must stay unmistakable). The state is
   read from aria-pressed, which web/js/likes.js already maintains. */
button#likeBtn[aria-pressed="false"] {
  background: var(--glass-bg);
  border-color: var(--glass-border);
  color: var(--btn-glass-color);
}
button.glass-btn:hover,
button#likeBtn[aria-pressed="false"]:hover {
  background: var(--btn-glass-bg-hover);
  border-color: var(--btn-glass-border-hover);
  color: var(--btn-glass-color-hover);
}

/* ==========================================================================
   #445 — glassy design language (sitewide glass surfaces)
   ==========================================================================
   Operator voice note (21-Oct-2026):

     "The glassy style has to be implemented all across the website,
      everywhere... you play a little bit with transparency and other things
      to make it look nice."
     "That login page when you put your email and password in, it's still
      kind of on the standard graphics; it should be matching... the glassy
      style and all the style of the web page in general and the logo and
      everything else."

   This block is the ONE place the glass recipe lives:

     * the TOKENS  (--glass-bg / --glass-border / --glass-blur) are declared
       for BOTH themes — the dark default on :root and the parchment variant
       on html:not(.dark) — so a glass surface follows the app theme;
     * the RECIPE  (.glass-surface) combines the translucent background, the
       backdrop blur and the subtle border;
     * the SURFACES (the tail cascade rules below) re-use the recipe, so a
       new glass surface is one class away, not another hardcoded rgba.

   The sign-in / login modal comes FIRST: the operator named it explicitly
   ("that login page ... it's still on the standard graphics"). The other
   glass surfaces follow: the benefits (entitlements) cards, the upgrade and
   coupon modals, the contribute panel and the admin tables.

   The player bar and the tools sheet are NOT touched here — they are owned
   by #432B. */

/* --- the tokens: BOTH themes --------------------------------------------- */
:root {
  /* Dark theme (the app default): a deep green-tinted glass over the
     near-black page, with the brand gold as the hairline border.
     #818 re-points these to the L2 sticky-chrome tier (0.80 rest / 0.62
     scrolled) and adds the L1 card + L3 popover elevation tokens. */
  --glass-bg: rgba(18, 24, 21, 0.80);
  --glass-border: rgba(212, 175, 55, 0.26);
  --glass-blur: 18px;
  --glass-saturate: 160%;
  /* #443/#810 scroll-glass: the bar's scrolled material — the same paint,
     measurably MORE translucent (player.js toggles .is-scrolled). */
  --glass-bg-scrolled: rgba(18, 24, 21, 0.62);
  /* #818 L1 — cards & panels (.glass-card, .hinario-card, the library
     filter toolbar): a step lighter than L2 so panels read as raised
     material, with the specular top highlight as the first shadow. */
  --glass-card-bg: rgba(22, 29, 25, 0.68);
  --glass-card-border: rgba(212, 175, 55, 0.16);
  --glass-card-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06);
  /* #818 L3 — floating popovers (#searchResults, #mobileNavDrawer,
     #navAccountMenu, .bar-popover, .glass-popover): the opaque legibility
     floor (0.90 dark / 0.94 light) that keeps text >= 7:1 over white album
     art, with blur(24px) saturate(180%). */
  --glass-popover-bg: rgba(18, 24, 21, 0.90);
  --glass-popover-border: rgba(212, 175, 55, 0.34);
  /* #810 modal backdrops: ONE scrim (one dim, one blur) for every
     full-screen modal, replacing the 0.55/0.70/0.75 + 4/10/16px drift. */
  --modal-scrim: rgba(10, 14, 12, 0.70);
  /* #811: the select family's SOLID surface tokens — the same card/border
     pair the daime palette paints, on tokens so the light theme follows
     for free. The gold accent doubles as the focus ring. */
  --select-bg: #1a231e;
  --select-border: #27362e;
  --select-accent: #d4af37;
  --select-text: #e7e5e4;
}
html:not(.dark) {
  /* Light theme: parchment glass over the cream page, with the muted
     bronze hairline the rest of the light palette uses. */
  --glass-bg: rgba(255, 249, 232, 0.84);
  --glass-border: rgba(138, 120, 74, 0.36);
  --glass-blur: 18px;
  --glass-saturate: 150%;
  --glass-bg-scrolled: rgba(255, 249, 232, 0.68);
  --glass-card-bg: rgba(255, 252, 244, 0.80);
  --glass-card-border: rgba(138, 120, 74, 0.24);
  --glass-card-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.65);
  --glass-popover-bg: rgba(255, 253, 247, 0.94);
  --glass-popover-border: rgba(138, 120, 74, 0.42);
  --modal-scrim: rgba(15, 23, 42, 0.45);
  --select-bg: #ffffff;
  --select-border: #e2d7ba;
  --select-accent: #b38f00;
  --select-text: #1f2937;
}

/* --- the recipe: one reusable glass surface -------------------------------
   ONE declaration block holds the recipe AND every surface that uses it, so
   the glass look can never drift between surfaces: a new glass surface is
   one selector away, not another hardcoded rgba.

   #810 additions to the list: the account dropdown (was a 0.97-alpha
   zero-blur slab), the study expander modal (was a 0.96-alpha slab; light
   was opaque #FFF), the bar flyouts' single .bar-popover recipe (replacing
   the bg-stone-900/95 + backdrop-blur-md lookalikes and the UR8 per-id
   patches), and the moderation surfaces (.mq-row / .mq-conflict-card /
   .mq-dialog-surface — previously ZERO CSS).
   #searchResults and
   #mobileNavDrawer consume the recipe through the .glass-surface class;
   #chordPopover and #tokenBlamePopover go through .glass-popover (#811),
   which adds the shared radius/elevation base. */
.glass-surface,
.glass-popover,
.bar-popover,
.signin-modal-card,
.signin-modal-logo,
.oauth-placeholder,
#upgradeModal > div,
#couponModal > div,
#entitlementsPanel > div,
#contributePanel,
#adminTableContainer,
.nav-account-menu,
.study-expander-modal,
.mq-row,
.mq-conflict-card,
.mq-dialog-surface {
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  border: 1px solid var(--glass-border);
}

/* Solid-color fallback for engines without backdrop-filter (the same
   convention the header/player bar already use). Every token consumer
   flips opaque at once by re-pointing the tokens, so no per-surface
   fallback is ever needed again (#810). */
@supports not ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))) {
  :root {
    --glass-bg: #1a231e;
    --glass-bg-scrolled: #1a231e;
    /* #818: the L1/L3 tiers flip opaque too — a translucent card without
       blur behind it is just mud. */
    --glass-card-bg: #161d19;
    --glass-popover-bg: #121815;
    --modal-scrim: rgba(10, 14, 12, 0.92);
  }
  html:not(.dark) {
    --glass-bg: #fffcf3;
    --glass-bg-scrolled: #fffcf3;
    --glass-card-bg: #fffcf4;
    --glass-popover-bg: #fffdf7;
    --modal-scrim: rgba(15, 23, 42, 0.75);
  }
}
/* --- #811: the shared popover base (.glass-popover) -------------------------
   #chordPopover and #tokenBlamePopover paint the roster's glass material
   (the entry above) through ONE base class; radius + elevation live here,
   from tokens — never mutated inline by JS anymore (the bottom-sheet
   branch toggles .glass-popover-bottom-sheet instead; the chord sheet's
   own #chordPopover.chord-popover-bottom-sheet rule keeps its !important
   geometry pin). */
.glass-popover {
  border-radius: 0.75rem;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); /* the shadow-2xl elevation */
}

.glass-popover-bottom-sheet {
  border-radius: 1rem 1rem 0 0 !important;
}

/* --- #818: the 4-tier glass elevation system -------------------------------
   L0 — the body canvas (radial ambient depth; see the body rules at the top
        of this sheet and the html:not(.dark) body pin in index.html).
   L1 — cards & panels: .glass-card / .hinario-card (the library filter
        toolbar, the catalog tiles, the #/settings cards). One step lighter
        than L2 so panels read as raised material; blur(12px) saturate(140%)
        dark / saturate(130%) light; the specular top highlight leads the
        shadow stack.
   L2 — sticky chrome: .glass-header / .glass-bar via their ::before layers
        (--glass-bg 0.80 / --glass-bg-scrolled 0.62, blur 18px).
   L3 — floating popovers: everything below re-points the shared #810
        recipe surfaces that FLOAT above the page (#searchResults and
        #mobileNavDrawer through .glass-surface, .glass-popover, the bar
        flyouts, #navAccountMenu) to --glass-popover-bg (0.90 dark / 0.94
        light) + blur(24px). The #445 modal surfaces (sign-in, paywalls,
        moderation) stay on the L2 --glass-bg material on purpose — the
        test-visible alpha ceiling (MAX_VISIBLE_ALPHA = 0.90) is respected
        by both tiers in both themes. */
.glass-card,
.hinario-card {
  background: var(--glass-card-bg);
  border: 1px solid var(--glass-card-border);
  box-shadow: var(--glass-card-highlight), 0 10px 28px -8px rgba(0, 0, 0, 0.45);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  backdrop-filter: blur(12px) saturate(140%);
}

html:not(.dark) .glass-card,
html:not(.dark) .hinario-card {
  -webkit-backdrop-filter: blur(12px) saturate(130%);
  backdrop-filter: blur(12px) saturate(130%);
  box-shadow: var(--glass-card-highlight), 0 10px 28px -12px rgba(28, 25, 23, 0.18);
}

.glass-surface,
.glass-popover,
.bar-popover,
.nav-account-menu {
  background: var(--glass-popover-bg);
  border-color: var(--glass-popover-border);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  backdrop-filter: blur(24px) saturate(180%);
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.65), inset 0 1px 0 rgba(255, 255, 255, 0.10);
}

html:not(.dark) .glass-surface,
html:not(.dark) .glass-popover,
html:not(.dark) .bar-popover,
html:not(.dark) .nav-account-menu {
  -webkit-backdrop-filter: blur(24px) saturate(160%);
  backdrop-filter: blur(24px) saturate(160%);
  box-shadow: 0 18px 42px rgba(28, 25, 23, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.65);
}

/* --- #811: the ONE select recipe (.glass-select) ---------------------------
   Thirteen divergent select recipes collapsed into one: every <select> in
   the app (index.html markup + orgadmin/app.js templates) carries
   .glass-select and NOTHING else paint-wise — the recipe owns the surface
   (solid --select-bg card, hairline border), the canonical gold chevron
   (appearance:none + one data-URI arrow, so every select shows the same
   brand arrow on every platform), the geometry, the mono chrome font and
   the focus ring. Light theme follows the tokens. Two DOCUMENTED variants:
     * default (dense chrome): text-xs mono, px-2.5/py-1, rounded-lg —
       filters, capo, translations, tab versions, org rows, card versions;
     * .glass-select--form (form fields): text-sm, SANS (the app voice for
       user-editable fields), px-3/py-2 — contribute, entitlement, profile.
   Guard: web/tests/test_select_recipe.py. */
.glass-select {
  appearance: none;
  -webkit-appearance: none;
  background-color: var(--select-bg);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23D4AF37'%3E%3Cpath fill-rule='evenodd' d='M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z' clip-rule='evenodd'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.45rem center;
  background-size: 0.7rem;
  border: 1px solid var(--select-border);
  border-radius: 0.5rem;
  color: var(--select-text);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  line-height: 1.25;
  padding: 0.25rem 1.35rem 0.25rem 0.625rem;
  cursor: pointer;
  transition: border-color 150ms ease, box-shadow 150ms ease;
}

.glass-select:hover {
  border-color: var(--select-accent);
}

/* #818: filter/sort selects in the catalog toolbar get their width from
   their longest option (author names); cap them so 390px viewports never
   scroll horizontally. */
#libraryFilterToolbar select.glass-select {
  max-width: 11rem;
  min-width: 0;
}

.glass-select:focus {
  outline: none;
  border-color: var(--select-accent);
  box-shadow: 0 0 0 1px var(--select-accent);
}

/* The form variant: bigger, sans (user-editable fields read as prose). */
.glass-select--form {
  font-family: var(--font-sans);
  font-size: 0.875rem;
  padding: 0.5rem 1.75rem 0.5rem 0.75rem;
}

/* Option panels follow the app surface (UA default would flash white in
   the dark theme). */
.glass-select option {
  background-color: var(--select-bg);
  color: var(--select-text);
}

/* --- #810: the modal-backdrop recipe + the z-ladder ------------------------
   ONE recipe for every full-screen modal backdrop (one dim, one blur —
   the --modal-scrim tokens above), replacing three dims (0.55/0.70/0.75)
   and three blurs (4/10/16px). It also fixes the stacking contradiction:
   the paywall backdrops sat at z-60, BELOW the study expander (z-70),
   the chord popover (z-75) and the always-visible bar (z-80).

   The z-ladder (documented once, used everywhere):
     50  the header row + its dropdowns (search results, settings menus)
     70  the study expander overlay — deliberately BELOW the popovers and
         the bar so the study surface never covers the transport
     75  the chord/token-blame popovers + the toast (above the expander,
         below the bar)
     79  the next-up card (below the always-visible bar)
     80  #globalBottomBar (always visible, #443)
     90  the bar flyouts (.bar-popover) + the chord bottom sheet
     95  full-screen modal backdrops (.modal-backdrop): sign-in, privacy,
         upgrade, coupon — a modal sits ABOVE every always-visible layer
   (.mq-dialog rides the native <dialog> top layer, above the ladder.) */
.modal-backdrop,
.signin-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 95;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: var(--modal-scrim);
  -webkit-backdrop-filter: blur(var(--modal-scrim-blur)) saturate(140%);
  backdrop-filter: blur(var(--modal-scrim-blur)) saturate(140%);
}
.modal-backdrop[hidden],
.signin-modal-backdrop[hidden] {
  display: none;
}
/* --- the sign-in / login modal (operator's FIRST priority) ----------------
   The card was a near-opaque slab (alpha 0.92 over a dim scrim), so the
   login page read as "standard graphics" instead of the site's glass.
   It now uses the shared recipe, so it matches the header, the player bar
   and every other glass surface. */

/* The two sign-in selectors that already carried a MORE SPECIFIC light
   theme rule (the `html:not(.dark) ...` blocks above) need their own
   light override, otherwise the old hardcoded cream would win over the
   glass token. */
html:not(.dark) .signin-modal-card {
  background: var(--glass-bg);
  border-color: var(--glass-border);
}
html:not(.dark) .oauth-placeholder {
  background: var(--glass-bg);
  border-color: var(--glass-border);
}

/* The logo disc keeps its brand green fill, but gains the glass blur so the
   mark reads as part of the same material as the card. The green is a TINT
   painted over the glass by the ::before layer, so the disc's own surface
   stays the shared --glass-bg material (and the blur still samples the page
   behind it). */
.signin-modal-logo {
  position: relative;
}
.signin-modal-logo::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 9999px;
  background: rgba(22, 91, 51, 0.35);
  pointer-events: none;
}
.signin-modal-logo img,
.signin-modal-logo svg {
  position: relative;
  z-index: 1;
}
html:not(.dark) .signin-modal-logo::before {
  background: rgba(22, 91, 51, 0.22);
}

/* The sign-in input fields sit ON the glass card: they get the same
   material, one step flatter, so the form reads as one surface. */
.signin-modal-card input,
.modal-glass-input {
  background: var(--glass-bg);
  border-color: var(--glass-border);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  backdrop-filter: blur(10px) saturate(140%);
}
html:not(.dark) .signin-modal-card input,
html:not(.dark) .modal-glass-input {
  background: var(--glass-bg);
  border-color: var(--glass-border);
}

/* ---- #450: staged scroll-collapse header (reworked by #809) -------------

   #809 Top Bar Mandate removed the stage-2 collapse panel and its second
   hamburger: the sticky bar keeps logo + search + hamburger +
   login/avatar at EVERY stage, and all navigation lives in the unified
   #mobileNavDrawer (opened by #mobileNavToggleBtn, page-chrome.js). The
   stages now only fade the brand:

     stage-0 (top)       the full header.
     stage-1 / stage-2   the logo fades and collapses; its space is handed
                         to the search bar, which REMAINS in the bar.

   The stage classes are written by web/js/header-collapse.js; the rules
   below only describe what each stage LOOKS like. Everything is scoped
   to #siteHeader so no other surface is affected. */

/* The brand is the header's flex item that holds the logo + wordmark. It
   fades AND collapses, so the space it frees is handed back to the
   search bar instead of leaving a hole in the row. */
#siteHeader .header-brand {
  max-width: 20rem;
  overflow: hidden;
  opacity: 1;
  transition: opacity 220ms ease, max-width 220ms ease;
}

/* --- stage 0: the full header ------------------------------------------ */
#siteHeader.header-stage-0 .header-brand {
  opacity: 1;
  max-width: 20rem;
}

/* --- stage 1 and 2: the logo fades out, the search bar stays ------------ */
#siteHeader.header-stage-1 .header-brand,
#siteHeader.header-stage-2 .header-brand {
  opacity: 0;
  max-width: 0;
  pointer-events: none;
}
/* ===================================================================
   Issue #393 — animated guitar fretboard (Guitar-Hero-style TAB
   playback). The board is an inline SVG drawn by web/js/fretboard.js;
   these rules only tune its look. Appended at the TAIL so the cascade
   never reshapes the committed surfaces.
   =================================================================== */
.fretboard-root {
  width: 100%;
  min-height: 200px;
  display: block;
}
.fretboard-svg {
  width: 100%;
  height: auto;
  min-width: 520px;
  display: block;
}
.fretboard-fret {
  opacity: 0.55;
}
.fretboard-string {
  opacity: 0.65;
}
.fretboard-string-number,
.fretboard-fret-number {
  font-family: var(--font-mono);
}
/* The marker states: idle → upcoming (gold) → active (green); the notes
   that already went by gray out (the operator's "graying out the ones
   that are coming next"). */
.fretboard-marker {
  transition: fill 120ms linear, r 120ms linear;
}
.fretboard-marker-idle {
  opacity: 0.55;
}
.fretboard-marker-upcoming {
  opacity: 1;
  filter: drop-shadow(0 0 4px rgba(212, 175, 55, 0.75));
}
.fretboard-marker-active {
  opacity: 1;
  filter: drop-shadow(0 0 7px rgba(16, 185, 129, 0.95));
}
.fretboard-marker-played {
  opacity: 0.75;
}
.fretboard-capo {
  opacity: 0.9;
}
@media (prefers-reduced-motion: reduce) {
  .fretboard-marker {
    transition: none;
  }
}

/* Intro / Outro instrumental placeholder rows (issue #700 / epic #703) */
.instrumental-row {
  display: flex;
  align-items: center;
  font-family: var(--font-lyric);
  font-style: italic;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: #78716c; /* text-stone-500 */
  padding: 0.25rem 0.5rem;
  margin: 0.25rem 0;
  border-radius: 0.375rem;
  user-select: none;
  transition: color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
}

.instrumental-row.is-active {
  color: #fbbf24; /* daime-gold / amber-400 */
  background: rgba(212, 175, 55, 0.12);
  box-shadow: 0 0 0 1px rgba(212, 175, 55, 0.25);
}

html:not(.dark) .instrumental-row {
  color: #a8a29e; /* text-stone-400 */
}

html:not(.dark) .instrumental-row.is-active {
  color: #8a6d1a;
  background: rgba(138, 120, 74, 0.12);
  box-shadow: 0 0 0 1px rgba(138, 120, 74, 0.25);
}

/* An open-ended outro whose track ends at the last sung word has no span. */
.instrumental-row[hidden] {
  display: none;
}

/* --- Repetition gutter & pass glow (Epic #703 / Issue #702 / LDR-01..04) --- */
:root {
  --rep-pass-1: #eab308;       /* Yellow - 1st repetition pass */
  --rep-pass-2: #f97316;       /* Orange - 2nd repetition pass */
  --rep-pass-3: #ea580c;       /* Orange-red - 3rd repetition pass */
  --rep-pass-4: #ef4444;       /* Red - 4th repetition pass */
  --rep-pass-done: #78716c;    /* Gray - previous block completed */
  --rep-bar-idle: rgba(120, 113, 108, 0.35);
  --rep-bar-w: 0.25rem;
  --rep-col-step: 0.75rem;
  --rep-gutter-left: 1rem;
  --rep-clearance: 1rem;
}

#lyricsContainer.has-repetition-gutter {
  padding-left: var(--rep-gutter-total-pad, 4.5rem) !important;
}

.repetition-gutter {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--rep-gutter-w, 3.5rem);
  pointer-events: none;
  user-select: none;
  z-index: 5;
}

.rep-bar {
  position: absolute;
  width: var(--rep-bar-w, 0.25rem);
  min-width: 4px;
  border-radius: 9999px;
  background-color: var(--rep-bar-idle, rgba(120, 113, 108, 0.35));
  opacity: 0.8;
  /* Issue #712: compositor-only — the glow/background snap instead of
     interpolating on every pass change. */
  transition: opacity 0.2s ease-out;
}

/* Lit state while sung (LDR-01) */
.rep-bar.is-lit,
.rep-bar.rep-bar-lit {
  opacity: 1;
}

/* LDR-02: Pass color escalation tokens */
.rep-bar.rep-bar-pass-1,
.rep-bar.rep-bar-lit.rep-bar-pass-1 {
  background-color: var(--rep-pass-1, #eab308);
  box-shadow: 0 0 10px rgba(234, 179, 8, 0.75);
}

.rep-bar.rep-bar-pass-2,
.rep-bar.rep-bar-lit.rep-bar-pass-2 {
  background-color: var(--rep-pass-2, #f97316);
  box-shadow: 0 0 10px rgba(249, 115, 22, 0.75);
}

.rep-bar.rep-bar-pass-3,
.rep-bar.rep-bar-lit.rep-bar-pass-3 {
  background-color: var(--rep-pass-3, #ea580c);
  box-shadow: 0 0 10px rgba(234, 88, 12, 0.75);
}

.rep-bar.rep-bar-pass-4,
.rep-bar.rep-bar-lit.rep-bar-pass-4 {
  background-color: var(--rep-pass-4, #ef4444);
  box-shadow: 0 0 12px rgba(239, 68, 68, 0.8);
}

/* LDR-03: Completed previous block turns gray (done) and stays visible */
.rep-bar.rep-bar-done {
  background-color: var(--rep-pass-done, #78716c) !important;
  box-shadow: none !important;
  opacity: 0.6 !important;
}

/* Inferred-only groups in Ambos mode render dashed */
.rep-bar.rep-bar-inferred,
.rep-bar.is-dashed {
  background: repeating-linear-gradient(
    to bottom,
    var(--rep-bar-color, currentColor),
    var(--rep-bar-color, currentColor) 6px,
    transparent 6px,
    transparent 12px
  ) !important;
  border-left: 2px dashed var(--rep-bar-color, currentColor);
  background-color: transparent !important;
}

/* Whole-hymn continuous outer bar (LDR-04) */
.rep-bar.rep-bar-whole-hymn {
  opacity: 0.9;
}

/* Source toggle buttons (Prescrito / Inferido / Ambos) */
.rep-source-toggle {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  background-color: var(--glass-bg); /* #810: shared glass material */
  border: 1px solid rgba(68, 64, 60, 0.5);
  border-radius: 0.375rem;
  padding: 2px;
  font-size: 0.6875rem;
  font-weight: 500;
  pointer-events: auto;
}

.rep-source-btn {
  padding: 2px 6px;
  border-radius: 0.25rem;
  color: rgb(168, 162, 158);
  background: transparent;
  border: none;
  cursor: pointer;
  transition: all 0.15s ease;
}

.rep-source-btn:hover {
  color: rgb(245, 245, 244);
}

.rep-source-btn.is-active,
.rep-source-btn[aria-pressed="true"] {
  background-color: rgb(41, 37, 36);
  color: var(--daime-gold, #F6E05E);
  font-weight: 600;
}


/* ==========================================================================
   Karaoke Line Editor (Issue #704 / Epic #703 / S5)
   ========================================================================== */

.line-editor-host {
  z-index: 9999;
}

.line-editor-modal {
  box-sizing: border-box;
  animation: lineEditorFadeIn 0.15s ease-out;
}

@keyframes lineEditorFadeIn {
  from {
    opacity: 0;
    transform: scale(0.97);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.line-editor-modal.is-invalid,
.line-editor-modal.timing-error {
  border-color: rgba(239, 68, 68, 0.85) !important;
  box-shadow: 0 0 20px rgba(239, 68, 68, 0.25), 0 20px 25px -5px rgba(0, 0, 0, 0.5) !important;
}

/* ==========================================================================
   Whole-Hymn Syllable Alignment Editor (Issue #710 / Epic #703 / S9)
   ========================================================================== */

.hymn-editor-host {
  z-index: 9999;
  box-sizing: border-box;
}

.hymn-editor-host.is-invalid,
.hymn-editor-host.timing-error,
.hymn-editor-host .timing-error,
.hymn-editor-host .is-invalid {
  border-color: rgba(239, 68, 68, 0.85) !important;
  box-shadow: 0 0 20px rgba(239, 68, 68, 0.25), 0 20px 25px -5px rgba(0, 0, 0, 0.5) !important;
}

.editor-syllable-block {
  box-sizing: border-box;
  touch-action: none;
  user-select: none;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.editor-syllable-block:hover {
  z-index: 10;
}

.resize-handle-left,
.resize-handle-right {
  touch-action: none;
  user-select: none;
  z-index: 20;
}

.line-edit-btn {
  opacity: 0.6;
}

.lyrics-line:hover .line-edit-btn,
.line-edit-btn:hover,
.line-edit-btn:focus-visible {
  opacity: 1;
}
/* Edit pipeline & moderation timing diff (epic #703 / issue #699) */
.pending-change-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.125rem 0.5rem;
  border-radius: 9999px;
  background: rgba(245, 158, 11, 0.12);
  border: 1px solid rgba(245, 158, 11, 0.35);
  color: #fbbf24;
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1rem;
}

.mq-timing-diff {
  width: 100%;
}

.mq-diff-side-by-side {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.mq-diff-col-base,
.mq-diff-col-proposed {
  flex: 1 1 0%;
  border-radius: 0.375rem;
  padding: 0.5rem;
  font-size: 0.75rem;
}

.mq-diff-col-base {
  background: rgba(127, 29, 29, 0.25);
  border: 1px solid rgba(185, 28, 28, 0.4);
  color: #fca5a5;
}

.mq-diff-col-proposed {
  background: rgba(6, 78, 59, 0.25);
  border: 1px solid rgba(4, 120, 87, 0.4);
  color: #6ee7b7;
}

.mq-diff-col-hdr {
  display: block;
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.25rem;
}

.mq-diff-col-base .mq-diff-col-hdr {
  color: #f87171;
}

.mq-diff-col-proposed .mq-diff-col-hdr {
  color: #34d399;
}

.mq-diff-time {
  font-family: var(--font-mono);
}

/* Whole-Hymn Alignment Editor Lane Labels & Sticky Columns (Issue #710) */
.timeline-lane-label[data-line-no]::before {
  content: attr(data-line-no) ". ";
  color: #78716c;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  margin-right: 0.35rem;
  flex-shrink: 0;
}

.timeline-lane-label {
  user-select: none;
}

/* --- #736: "Next up" card (countdown ring) ------------------------------ */
/* A compact glass card pinned bottom-right ABOVE the player bar:
   #globalBottomBar is fixed bottom-0 at z-80 and player.js publishes its
   measured height (safe-area padding included) as --bar-h, so the card
   offsets by that + a gap and never overlaps the bar or its transport
   cluster. z-79 keeps it above page content and the maximized view (z-70)
   but UNDER the bar's stacking layer, so the bar's flyouts (volume + stem
   mixer, tools, version, playlist) always paint over it — and while any of
   them or the maximized view is open the card yields entirely. Surface =
   the shared --glass-* tokens; type scale mirrors the bar title/meta. */
.next-up-card {
  position: fixed;
  right: max(0.75rem, env(safe-area-inset-right, 0px));
  bottom: calc(var(--bar-h, 5rem) + 0.875rem);
  z-index: 79;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 18rem;
  max-width: calc(100vw - 1.5rem - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
  padding: 0.5rem 0.375rem 0.5rem 0.625rem;
  border-radius: 0.875rem;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  box-shadow: 0 12px 32px -10px rgba(0, 0, 0, 0.6), 0 2px 6px rgba(0, 0, 0, 0.25);
  color: #e7e5e4;
  transform-origin: bottom right;
}
.next-up-card[data-state="in"] {
  animation: next-up-enter 280ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.next-up-card[data-state="out"] {
  animation: next-up-exit 180ms ease-in both;
  pointer-events: none;
}
@keyframes next-up-enter {
  from { opacity: 0; transform: translate3d(0, 0.75rem, 0) scale(0.97); }
  to { opacity: 1; transform: none; }
}
@keyframes next-up-exit {
  from { opacity: 1; }
  to { opacity: 0; }
}
/* #810: every flyout carries .bar-popover, so one selector covers them
   all (the dead #globalModeDropdownMenu line is gone with its markup). */
body:has(#studyExpanderOverlay:not(.hidden)) .next-up-card,
body:has(#globalBottomBar .bar-popover:not(.hidden)) .next-up-card {
  visibility: hidden;
}

/* Countdown ring: SVG circle, pathLength 100, rotated so it starts at 12
   o'clock; next-up.js drives stroke-dashoffset from the transport clock
   (0 = full, 100 = empty) and the short linear transition bridges the
   200 ms tick cadence. The seconds sit centered inside the ring. */
.next-up-ring {
  position: relative;
  flex: none;
  width: 2.5rem;
  height: 2.5rem;
}
.next-up-ring-svg {
  display: block;
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}
.next-up-ring-track,
.next-up-ring-arc {
  fill: none;
  stroke-width: 3;
}
.next-up-ring-track {
  stroke: rgba(212, 175, 55, 0.18);
}
.next-up-ring-arc {
  stroke: #d4af37;
  stroke-linecap: round;
  stroke-dasharray: 100;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 0.25s linear;
}
.next-up-ring-num {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8125rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: #f2c94c;
}

.next-up-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
  line-height: 1.2;
}
.next-up-label {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(212, 175, 55, 0.9);
}
.next-up-title {
  margin-top: 0.125rem;
  font-size: 0.8125rem;
  font-weight: 700;
  color: #fafaf9;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.next-up-dismiss {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--btn-radius-pill, 9999px);
  color: #a8a29e;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.next-up-dismiss svg {
  width: 0.875rem;
  height: 0.875rem;
}
.next-up-dismiss:hover {
  color: #ffffff;
  background: rgba(212, 175, 55, 0.14);
}
.next-up-dismiss:focus-visible {
  outline: 2px solid #d4af37;
  outline-offset: 1px;
}
/* ZETA-4: 44px effective touch target without growing the card. */
@media (pointer: coarse) {
  .next-up-dismiss {
    position: relative;
  }
  .next-up-dismiss::after {
    content: "";
    position: absolute;
    inset: -0.375rem;
  }
}

/* Light theme: parchment glass via the tokens; bronze accents. */
html:not(.dark) .next-up-card {
  color: #44403c;
  box-shadow: 0 12px 28px -12px rgba(87, 83, 78, 0.45), 0 2px 6px rgba(87, 83, 78, 0.15);
}
html:not(.dark) .next-up-label { color: #8a6d1a; }
html:not(.dark) .next-up-title { color: #1c1917; }
html:not(.dark) .next-up-ring-track { stroke: rgba(138, 120, 74, 0.22); }
html:not(.dark) .next-up-ring-arc { stroke: #b38f00; }
html:not(.dark) .next-up-ring-num { color: #8a6d1a; }
html:not(.dark) .next-up-dismiss { color: #78716c; }
html:not(.dark) .next-up-dismiss:hover { color: #1c1917; background: rgba(138, 120, 74, 0.16); }

@supports not ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))) {
  .next-up-card { background: #1a231e; }
  html:not(.dark) .next-up-card { background: #fffcf3; }
}

@media (prefers-reduced-motion: reduce) {
  .next-up-card,
  .next-up-card[data-state="in"] {
    animation: none;
  }
  .next-up-card[data-state="out"] {
    animation: none;
    opacity: 0;
  }
  .next-up-ring-arc,
  .next-up-dismiss {
    transition: none;
  }
}

/* ==========================================================================
   #810 — Moderation review queue + reject dialog (glass)
   ==========================================================================
   web/js/moderation-queue.js ships SEMANTIC classes only (.mq-*); before
   #810 this sheet did not exist, so the whole review queue rendered as
   unstyled UA default and the reject dialog as the browser's white sheet.

   The contract, same as everywhere else:
     * SURFACES (.mq-row / .mq-conflict-card / .mq-dialog-surface) paint
       the shared glass recipe — they are in the #445 recipe list, both
       themes, no hardcoded panel rgba;
     * BUTTONS (.mq-btn) follow the #446 contract: the glass fill
       (--glass-bg / --glass-border), --btn-radius, the padding tokens,
       and a SEMANTIC fill for the primary CTA (confirm-reject) which is
       deliberately not glass;
     * the dialog backdrop is the native ::backdrop painted with the same
       --modal-scrim material the .modal-backdrop recipe uses.
   The queue is data-dense: 12-13px type, tabular rhythm, no decoration. */

.mq-title {
  font-size: 1.125rem;
  font-weight: 700;
  color: #e7e5e4;
}
html:not(.dark) .mq-title { color: #1f2937; }

.mq-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* Queue cards: paint from the #445 recipe list (glass, both themes). */
.mq-row,
.mq-conflict-card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.75rem;
  border-radius: 0.75rem;
}

.mq-item-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.75rem;
  color: #a8a29e;
}
html:not(.dark) .mq-item-meta { color: #57534e; }
.mq-hymn-title { font-weight: 700; color: #e7e5e4; }
html:not(.dark) .mq-hymn-title { color: #1c1917; }
.mq-target-path {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: #78716c;
  word-break: break-all;
}
.mq-submitted-by,
.mq-created-at,
.mq-state { color: #78716c; }
.mq-state { text-transform: capitalize; }

.mq-risk {
  display: inline-flex;
  align-items: center;
  padding: 0.05rem 0.4rem;
  border-radius: var(--btn-radius-pill);
  font-size: 0.6875rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  border: 1px solid rgba(212, 175, 55, 0.45);
  color: #d4af37;
}
.mq-risk-destructive {
  border-color: rgba(185, 28, 28, 0.55);
  background: rgba(127, 29, 29, 0.25);
  color: #fca5a5;
}
html:not(.dark) .mq-risk { color: #8a6d1a; border-color: rgba(138, 120, 74, 0.45); }
html:not(.dark) .mq-risk-destructive {
  border-color: rgba(185, 28, 28, 0.45);
  background: rgba(127, 29, 29, 0.10);
  color: #b91c1c;
}

/* The diff body: a flat inset panel ON the glass card. */
.mq-diff {
  border-radius: 0.375rem;
  padding: 0.5rem 0.625rem;
  font-size: 0.75rem;
  line-height: 1.5;
  background: rgb(0 0 0 / 0.25);
  color: #e7e5e4;
  overflow-wrap: anywhere;
}
html:not(.dark) .mq-diff {
  background: rgb(138 120 74 / 0.10);
  color: #1f2937;
}
.mq-diff ins, .mq-ins { text-decoration: none; color: #6ee7b7; background: rgb(6 78 59 / 0.25); }
.mq-diff del, .mq-del { text-decoration: line-through; color: #fca5a5; background: rgb(127 29 29 / 0.20); }
html:not(.dark) .mq-diff ins, html:not(.dark) .mq-ins { color: #047857; background: rgb(6 78 59 / 0.10); }
html:not(.dark) .mq-diff del, html:not(.dark) .mq-del { color: #b91c1c; background: rgb(127 29 29 / 0.08); }
.mq-diff-row { display: flex; flex-wrap: wrap; gap: 0.375rem; align-items: baseline; }
.mq-diff-key {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: #78716c;
}
.mq-diff-unchanged { color: #a8a29e; }

.mq-item-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* #446 button contract: glass fill + radius token; semantic primary. */
.mq-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: var(--btn-pad-y) var(--btn-pad-x);
  border: 1px solid var(--glass-border);
  border-radius: var(--btn-radius);
  background: var(--glass-bg);
  color: var(--btn-glass-color);
  font-size: 0.75rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.mq-btn:hover {
  background: var(--btn-glass-bg-hover);
  border-color: var(--btn-glass-border-hover);
  color: var(--btn-glass-color-hover);
}
.mq-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.mq-btn-primary {
  background: #165b33;
  border-color: rgba(22, 91, 51, 0.9);
  color: #ffffff;
}
.mq-btn-primary:hover {
  background: #1d7a46;
  border-color: #1d7a46;
  color: #ffffff;
}
html:not(.dark) .mq-btn-primary {
  background: #007a2e;
  border-color: #007a2e;
  color: #ffffff;
}
html:not(.dark) .mq-btn-primary:hover { background: #089140; }

.mq-lock-info { font-size: 0.75rem; color: #fbbf24; }
html:not(.dark) .mq-lock-info { color: #8a6d1a; }
.mq-superseded-notice {
  font-size: 0.75rem;
  font-style: italic;
  opacity: 0.75;
}

.mq-empty,
.mq-error {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.75rem;
  border-radius: 0.5rem;
  font-size: 0.8125rem;
  color: #a8a29e;
}
.mq-error p { color: #fca5a5; }
html:not(.dark) .mq-empty, html:not(.dark) .mq-error { color: #57534e; }
html:not(.dark) .mq-error p { color: #b91c1c; }
.mq-claim-unavailable-notice { font-size: 0.75rem; color: #fbbf24; }
.mq-claim-error { font-size: 0.75rem; color: #fca5a5; }
html:not(.dark) .mq-claim-unavailable-notice { color: #8a6d1a; }
html:not(.dark) .mq-claim-error { color: #b91c1c; }

/* Conflict Group Cards: canonical base + competing proposals. */
.mq-conflict-header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
}
.mq-conflict-label {
  font-size: 0.625rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #d4af37;
}
html:not(.dark) .mq-conflict-label { color: #8a6d1a; }
.mq-canonical {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.5rem 0.625rem;
  border-radius: 0.375rem;
  background: rgb(0 0 0 / 0.25);
  font-size: 0.75rem;
}
html:not(.dark) .mq-canonical { background: rgb(138 120 74 / 0.10); }
.mq-canonical-label {
  font-size: 0.625rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #78716c;
}
.mq-canonical-value { word-break: break-word; }
.mq-proposals {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.mq-proposal {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.6rem;
  border: 1px dashed rgba(212, 175, 55, 0.35);
  border-radius: 0.625rem;
}
html:not(.dark) .mq-proposal { border-color: rgba(138, 120, 74, 0.4); }

/* The reject dialog: native <dialog>, transparent host so the glass
   .mq-dialog-surface (recipe list) is the only painted sheet. The
   ::backdrop carries the same --modal-scrim material as .modal-backdrop.
   #813: the contribute-terms dialog joins the same scrim rule — its raw
   backdrop:bg-black/70 utility painted a near-black scrim in the light
   theme while every .modal-backdrop had the light --modal-scrim. */
.mq-dialog {
  padding: 0;
  background: transparent;
  border: 0;
}
.mq-dialog::backdrop,
#contributeTermsModal::backdrop {
  background: var(--modal-scrim);
  -webkit-backdrop-filter: blur(var(--modal-scrim-blur)) saturate(140%);
  backdrop-filter: blur(var(--modal-scrim-blur)) saturate(140%);
}
.mq-dialog-surface {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-width: min(26rem, calc(100vw - 2rem));
  max-height: min(85vh, 40rem);
  overflow-y: auto;
  padding: 1.25rem;
  border-radius: 1rem;
  box-shadow: 0 28px 72px rgb(0 0 0 / 0.6);
  color: #e7e5e4;
}
html:not(.dark) .mq-dialog-surface {
  color: #1f2937;
  box-shadow: 0 28px 72px rgb(15 23 42 / 0.18);
}
.mq-dialog-surface h2 {
  font-size: 1rem;
  font-weight: 700;
}
.mq-reason-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 0;
  padding: 0;
  border: 0;
}
.mq-reason-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
  cursor: pointer;
}
.mq-reason-option input[type="radio"] { accent-color: #d4af37; }
#mq-reject-note {
  min-height: 4.5rem;
  resize: vertical;
  padding: 0.5rem 0.625rem;
  border-radius: 0.5rem;
  border: 1px solid var(--glass-border);
  background: var(--glass-bg);
  color: inherit;
  font: inherit;
  font-size: 0.8125rem;
}
.mq-reject-error { font-size: 0.75rem; color: #fca5a5; }
html:not(.dark) .mq-reject-error { color: #b91c1c; }
.mq-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}

/* ---- #818 Slice 03: visual-QA polish (docs/prove-it/epic-818) -----------
   Defects found by inspecting the live 1440x900 / 390x844 screenshots. */

/* The drawer hangs under its trigger: from sm up its right edge tracks the
   header's max-w-7xl (80rem) content box instead of the viewport edge, so
   on wide screens it no longer floats 80px right of the hamburger. Below
   sm the right-3 utility already matches the header's px-3. */
@media (min-width: 640px) {
  #mobileNavDrawer {
    right: max(1rem, calc((100% - 80rem) / 2));
  }
}

/* Light theme: the top-bar login/account icon buttons painted the dark
   theme's #d6d3d1 glyph on the cream bar (1.4:1, read as disabled). */
html:not(.dark) .nav-icon-btn {
  color: #44403c;
}
html:not(.dark) .nav-icon-btn:hover {
  color: #1c1917;
  background: rgba(138, 120, 74, 0.16);
}

/* Phones (< 480px): the wordmark squeezed #globalSearch to ~88px at 390px
   ("Buscar h…"). The round brand mark stays; the search gets the room —
   the same trade the scrolled compact stage already makes. */
@media (max-width: 479px) {
  #siteHeader .header-logo-text {
    display: none;
  }
}

/* Light theme: the drawer's settings link kept the #d4af37 brand gold on a
   pale green tint (2.0:1); a deep gold (#6b5314) reads 5.1:1 on that tint. */
html:not(.dark) #navSettings {
  color: #6b5314;
}

/* Light theme: the bottom bar's rgba(0,0,0,0.6) upward shadow smeared a
   grey band over the cream footer; a soft warm shadow keeps the lift. */
html:not(.dark) #globalBottomBar {
  box-shadow: 0 -6px 24px rgba(28, 25, 23, 0.08);
}

/* Light theme: the destructive reset row kept its dark-theme red-950 slab
   with red-300 text (unreadable on cream); a light danger tint instead. */
html:not(.dark) #btnResetAllSettings {
  background-color: #fdecea;
  border-color: #f2b8b5;
  color: #b91c1c;
}
html:not(.dark) #btnResetAllSettings:hover {
  background-color: #b91c1c;
  border-color: #b91c1c;
  /* !important: out-ranks the inline light flip of .hover:text-white. */
  color: #ffffff !important;
}
