/* SPDX-FileCopyrightText: 2026 S.F. Cyris */
/* SPDX-License-Identifier: AGPL-3.0-or-later */

/* ── Reset & tokens ───────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* Belt-and-braces hidden override.  Author rules like ``.empty-state {
   display: flex }`` win against the UA's ``[hidden] { display: none }``
   on specificity ties + source order, which was the root cause of the
   STATE-AUDIT bug class.  Asserting !important here means JS toggling
   ``el.hidden = true`` ALWAYS hides — no future per-component override
   can leak.  mobile.css and multiroom.css already had this; desktop now
   matches. */
[hidden], .hidden { display: none !important; }

:root {
  /* Base palette */
  --bg0: #0d0f12;
  --bg1: #13161a;
  --bg2: #1a1d22;
  --bg3: #22262d;
  --bg4: #2d3139;
  --accent: #f0722a;
  --accent-dim: #a84d1a;
  --accent-glow: rgba(240,114,42,0.18);
  --danger: #e55;
  /* Embedded-visualization palette (scan flow, cache cascade, FTP lanes,
     signal chain, transcode packets …).  Shared so every viz reads from
     one cohesive set; flow colours match docs/architecture.html. */
  --viz-req:    #6bc8f0;   /* request / downstream */
  --viz-res:    #74e09a;   /* response / cache hit */
  --viz-evt:    #f0a23a;   /* async event */
  --viz-err:    #ef6b8d;   /* cold miss / error */
  --viz-phosphor: #50ff8c; /* retro CRT green */
  --viz-copper:   #1f6b42; /* PCB trace */
  /* Canonical "cool blue" used for art-placeholder glow, keyboard-focus
     ring, and now-playing tints.  Replaces 14+ raw rgba(107,200,240,…)
     spellings — change here once to restyle the whole language. */
  --art-glow: rgba(107,200,240,0.82);
  --art-glow-soft: rgba(107,200,240,0.55);
  --art-glow-faint: rgba(107,200,240,0.25);
  /* Semantic state tokens — disambiguate the overloaded --accent.
     - --state-focus: used for :focus-visible rings (keyboard focus).
     - --state-current: marks "currently active" (now-playing, sort
       column, selected nav).  Active state stays orange; focus moves
       to the cool blue so the two don't collide visually. */
  --state-focus:   #6bc8f0;
  --state-current: var(--accent);
  --text0: #e8eaed;
  --text1: #a8adb6;
  /* Bumped #9aa1b3 → #b5bccb so the dim label colour reliably clears
     WCAG AA 4.5:1 against every dark surface we use, including the
     lighter --bg3/--bg4 panels (the previous value measured 4.4:1
     against --bg4 in QA which is below threshold). */
  --text2: #b5bccb;
  --border: rgba(255,255,255,0.07);
  --border-bright: rgba(255,255,255,0.12);
  --row-hover: rgba(255,255,255,0.07);
  --row-active: rgba(30,80,140,0.35);
  --row-playing: rgba(20,80,20,0.30);
  --scrollbar: rgba(255,255,255,0.10);

  /* Glass */
  --glass-bg:      rgba(18,21,26,0.82);
  --glass-border:  rgba(255,255,255,0.09);
  --glass-blur:    blur(24px) saturate(160%);
  --glass-shadow:  0 8px 32px rgba(0,0,0,0.55);
  /* Opaque-ish overlay panel backgrounds (Track Info, EQ, Admin,
     Now-Playing-large-art).  Single variable so the light-mode override
     can flip all of them in one spot without touching each panel's
     individual selector. */
  --panel-bg:      rgba(14,17,23,0.97);

  /* Type scale (rem-relative, derived from a 14 px / 0.875rem body) — gives
     designers a 6-step palette instead of the 23 ad-hoc font-sizes the
     codebase grew (8/9/10/10.5/11/12/12.5/13/14/15/16/17/18/20/22/24/28
     /38/44/55/76/80/96 px).  Existing px values are kept where they're
     correct; new components should reach for these tokens. */
  --text-xs:   0.6875rem; /*  11 px */
  --text-sm:   0.8125rem; /*  13 px */
  --text-base: 0.875rem;  /*  14 px */
  --text-md:   1rem;      /*  16 px */
  --text-lg:   1.125rem;  /*  18 px */
  --text-xl:   1.5rem;    /*  24 px */
  --text-2xl:  2.25rem;   /*  36 px */

  /* Spacing scale — replaces 70+ ad-hoc padding combinations.  Use these
     for new components; existing rules deliberately untouched. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  /* Radius scale — replaces 13 different ``border-radius`` values. */
  --radius-sm:   4px;
  --radius-md:   8px;
  --radius-lg:  12px;
  --radius-pill: 999px;

  --header-h: 52px;
  --player-h: 152px;
  --sidebar-w: 220px;

  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif;
  /* Bumped 13 → 14px so the base body matches the --text-base scale token
     and clears the WCAG AAA 14px small-text floor.  Existing px values
     are kept per-component; new rules should use rem so they scale with
     this root.  Audit: any 9px/10px declarations should bump to ≥11px. */
  font-size: 14px;
  color: var(--text0);
  -webkit-font-smoothing: antialiased;
}

/* Light-mode palette — opt-in via Settings → System → Theme.
   Dark mode is the default; we no longer auto-flip on
   prefers-color-scheme so the brand identity stays consistent unless
   the user explicitly picks light. */
:root[data-theme="light"] {
  --bg0: #f5f6f8;
  --bg1: #ffffff;
  --bg2: #eef0f3;
  --bg3: #e4e7ec;
  --bg4: #d6dae0;
  --text0: #1a1d22;
  --text1: #3a4050;
  /* Darker tone clears 4.5:1 across bg0…bg4 in light mode.  Bumped
     toward #3d4250 — the previous #555c6b dipped just under threshold
     on the very lightest --bg1 (pure white) surface. */
  --text2: #3d4250;
  --border: rgba(0,0,0,0.08);
  --border-bright: rgba(0,0,0,0.14);
  --row-hover: rgba(0,0,0,0.04);
  --row-active: rgba(30,80,140,0.18);
  --row-playing: rgba(20,80,20,0.18);
  --scrollbar: rgba(0,0,0,0.18);
  --glass-bg: rgba(255,255,255,0.82);
  --glass-border: rgba(0,0,0,0.10);
  --glass-shadow: 0 8px 32px rgba(0,0,0,0.18);
  --panel-bg: rgba(248,249,251,0.97);
}

/* Honour the user's "reduce motion" preference — turns off the logo
   pulse, scan-pulse, converting-pulse, VU-pulse, and the 15-ish other
   ambient animations that triggered vestibular discomfort.  Note: we
   *only* kill animations and scroll-behaviour here.  Killing every
   ``transition-duration`` also broke focus-ring slide-in and modal
   fade-ins, which are accessibility affordances rather than motion. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
  /* Drawer slide is parallax-style motion — exactly what 2.3.3 flags for
     vestibular-disorder users.  Suppress both the panel transform and the
     scrim fade; keep focus / modal transitions intact (those are
     affordances, not motion). */
  #sidebar, #sidebar-scrim { transition: none !important; }
  /* Kill transform-based keyframes outright.  The 0.001ms cap above
     still flashes one frame of "scale(0.95) translateY(-10px)" which
     reads as a jolt to vestibular-disorder users.  Listed animations:
     dialog-in (admin/eq), queue-slide-in, np-art-in (large art),
     ti-in (track-info), sel-bar-in (selection bar). */
  .admin-dialog,
  #eq-panel,
  #queue-panel,
  #playlist-panel,
  .now-playing-art,
  #ti-panel,
  #selection-bar,
  .playlist-add-dropdown { animation: none !important; }
  /* Lyrics line transitions are pure parallax: colour shift + scale up
     of the active line.  Reduced-motion users should see the highlight
     instantly without the scale/translate easing. */
  .lyrics-line, .lrc-line {
    transition: none !important;
    transform: none !important;
  }
}

/* Disabled state — single global affordance.  Covers both the native
   ``disabled`` attribute (preferred — JS team should migrate
   className-based ``.disabled`` to native ``disabled`` so screen
   readers see the disabled state via aria-disabled inference) and the
   legacy ``.disabled`` className still used in some buttons.  Forms
   under :disabled remain interactive for tooltips; for click suppression
   we set pointer-events: none. */
:disabled,
button:disabled,
.disabled,
[aria-disabled="true"] {
  cursor: not-allowed !important;
  pointer-events: none;
}
/* NOTE for JS team: please migrate .disabled className uses to the
   native `disabled` attribute (where the element is a button / input /
   select / fieldset) so assistive tech reports the state correctly.
   Some custom non-button widgets will keep .disabled — those should
   also set aria-disabled="true" so screen readers announce it. */

/* Visible focus ring for keyboard users.  Each `outline: none` below
   intentionally suppresses the default ring on click — pair every one with
   a :focus-visible style so Tab-navigation still shows where you are.
   Uses --state-focus (blue) so focus is visually distinct from the orange
   --accent which marks "currently active" (now-playing / sort / nav). */
:focus-visible {
  outline: 2px solid var(--state-focus) !important;
  outline-offset: 2px !important;
}
/* Sidebar nav items (<li role="button">), album cards, and any
   keyboard-focusable affordance: explicit focus styling so Tab-navigation
   has a high-contrast indicator that doesn't fight the orange .active
   highlight underneath. */
.nav-item:focus-visible,
li[role="button"]:focus-visible,
.album-card:focus-visible,
.keyboard-focus {
  outline: 2px solid var(--state-focus) !important;
  outline-offset: 2px !important;
  border-radius: 5px;
}
/* Override on accent-coloured buttons — an orange ring on an orange
   background reads as no focus state at all.  Use a high-contrast dark
   outline (or light in dark mode) so the focus is unmistakable. */
.auth-submit:focus-visible,
.password-dialog-actions button[type="submit"]:focus-visible,
.btn-play-pause:focus-visible {
  outline-color: #1a1d22 !important;
}
:root[data-theme="light"] .auth-submit:focus-visible,
:root[data-theme="light"] .password-dialog-actions button[type="submit"]:focus-visible,
:root[data-theme="light"] .btn-play-pause:focus-visible {
  outline-color: #1a1d22 !important;
}
/* Light mode: blue accent vs near-white bg still hits ~3:1 — use the
   same darker fallback as the buttons above so focus is unmistakable. */
:root[data-theme="light"] :focus-visible {
  outline-color: #1a1d22 !important;
}

/* Hover-only controls on touch — reveal them by default when the input
   modality is touch so iPad/Surface users can actually reach them
   (Visual-Test #2 caught hidden drag handles + queue-remove + folder-remove). */
@media (hover: none) {
  .queue-drag-handle,
  .queue-remove-btn,
  .tree-remove,
  .playlist-delete-btn,
  .pl-open-btn,
  .playlist-row .pl-open-btn {
    opacity: 1 !important;
    display: inline-flex !important;
  }
}
/* Keyboard parity for the hover-reveal controls: when a row contains
   focus (e.g. focus moved onto the remove button itself, or a child
   input was tabbed into) the contextual actions must be visible too.
   NOTE for JS team: CSS can't sync aria-hidden — when these buttons
   reveal on :focus-within, JS should toggle aria-hidden="false" so
   screen readers see them as active controls. */
tr:focus-within .queue-remove-btn,
.queue-row:focus-within .queue-remove-btn,
.queue-row:focus-within .queue-drag-handle,
.playlist-row:focus-within .playlist-delete-btn,
.playlist-row:focus-within .pl-open-btn,
.tree-root:focus-within > .tree-node .tree-remove {
  opacity: 1;
  display: inline-flex;
}

/* Skip-to-main-content link: invisible until a keyboard user focuses it,
   then it slides in at the top so they can bypass the header and sidebar
   in one Tab + Enter. */
.skip-to-main {
  position: fixed;
  top: -40px;
  left: 12px;
  /* Dark slate background instead of accent-orange so white text clears
     WCAG AA 4.5:1 in both light and dark mode — the orange/white combo
     was only ~3:1. */
  background: #1a1d22;
  color: #fff;
  padding: 8px 14px;
  border-radius: 4px;
  font-weight: 600;
  text-decoration: none;
  z-index: 10000;
  transition: top 120ms ease-out;
}
.skip-to-main:focus,
.skip-to-main:focus-visible {
  top: 8px;
}

html, body {
  height: 100%;
  /* iOS Safari URL-bar collapse: ``100vh`` overflows the player bar when
     the chrome retracts.  ``100dvh`` matches the actual visible viewport
     where supported.  Browsers without dvh keep the original 100% fallback
     above. */
  min-height: 100dvh;
  background: var(--bg0);
  overflow: hidden;
}
#app {
  /* ``height:100vh`` on #app was fighting the dvh min-height on body —
     on iOS Safari the URL bar retract pushed #app 60px below the visible
     viewport.  Use 100% (inherits dvh from body) and let flexbox handle
     the column distribution; #app is a flex parent and its children carry
     their own intrinsic heights. */
  height: 100%;
  min-height: 100dvh;
  background: var(--bg0);
  overflow: hidden;
}

/* Firefox scrollbar shim — the bespoke ::-webkit-scrollbar rules below
   only style Webkit/Blink; Firefox renders default chunky scrollbars.
   These two declarations apply to every scrollable surface in the app. */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar) transparent;
}

/* Noise texture overlay on body */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
  background-size: 180px 180px;
  z-index: 0;
}

/* ── Scrollbars ───────────────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 3px; }

/* Fallback for browsers without backdrop-filter (older Firefox without
   layout.css.backdrop-filter.enabled, older Safari, some embedded
   webviews).  Without this, the .glass-bg rgba surfaces render
   semi-transparent over the body noise texture which is unreadable.
   Switch to the opaque --bg1 fill so panels stay legible. */
@supports not (backdrop-filter: blur(1px)) {
  #header,
  #sidebar,
  #player-bar,
  .admin-dialog,
  #ti-panel,
  #queue-panel,
  #playlist-panel,
  #lyrics-panel,
  #similar-panel { background: var(--bg1) !important; }
}

/* content-visibility: auto on hidden overlay panels skips paint + layout
   for off-screen content, cutting first-paint cost when the overlay isn't
   actually open.  ``contain-intrinsic-size: 0 0`` makes the placeholder
   collapse so it doesn't reserve scroll space. */
.admin-overlay.hidden,
#admin-overlay.hidden,
#queue-panel.hidden,
#playlist-panel.hidden,
#lyrics-panel[hidden],
#shortcuts-overlay.hidden,
#ti-overlay.hidden {
  content-visibility: auto;
  contain-intrinsic-size: 0 0;
}

/* Light-mode glow parity: the layered drop-shadows used for album-art
   placeholders, sidebar/header glow, archive cards, and the auth overlay
   icon were tuned for dark backgrounds — on near-white surfaces they
   smear into a dirty haze.  Strip them (or reduce to ~30% strength). */
:root[data-theme="light"] .logo-icon-glow,
:root[data-theme="light"] .art-thumb .art-placeholder,
:root[data-theme="light"] .ti-art-placeholder,
:root[data-theme="light"] .sp-art .sp-art-ph,
:root[data-theme="light"] .queue-row-art .qr-art-ph,
:root[data-theme="light"] .col-cover .row-art-placeholder,
:root[data-theme="light"] .auth-brand-icon,
:root[data-theme="light"] .archive-card,
:root[data-theme="light"] .archive-card:hover {
  filter: none;
}
:root[data-theme="light"] .logo-title { text-shadow: none; }
:root[data-theme="light"] .meta-title,
:root[data-theme="light"] .ctrl-toggle,
:root[data-theme="light"] .btn-track-info,
:root[data-theme="light"] #btn-lyrics.on,
:root[data-theme="light"] #btn-eq.on {
  /* Soft-blur text-shadow glows wash out into a muddy bloom on light
     mode — kill them so the foreground type stays crisp. */
  text-shadow: none;
}

/* ── Layout skeleton ──────────────────────────────────────────────────────── */
/* Note: height/min-height are defined in the html/body/#app block above
   using dvh.  This rule provides layout (flex column + stacking context)
   only — the height: 100vh that used to live here was overflowing the
   bottom-anchored player bar on iOS Safari (see comment in body block). */
#app { display: flex; flex-direction: column; position: relative; z-index: 1; }

#header {
  height: var(--header-h);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--glass-border);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 16px;
  flex-shrink: 0;
  z-index: 200;
  position: relative;
  /* overflow: visible so search preview can extend below the header */
}

/* Ambient album-art glow behind header (matches sidebar-glow) */
#header-glow {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-position: center;
  filter: blur(50px) saturate(200%) brightness(0.14);
  opacity: 0;
  transition: opacity 1s ease, background-image 0.6s ease;
  pointer-events: none;
  /* Contain the blur bleed without clipping the parent */
  overflow: hidden;
  clip-path: inset(0);
}
#header-glow.active { opacity: 1; }
#header > *:not(#header-glow) { position: relative; z-index: 1; }

#main-layout {
  flex: 1;
  display: flex;
  overflow: hidden;
}

#sidebar {
  width: var(--sidebar-w);
  min-width: 140px;
  max-width: 480px;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-right: 1px solid var(--glass-border);
  overflow: hidden;
  flex-shrink: 0;
  padding: 8px 0;
  position: relative;
  display: flex;
  flex-direction: column;
}

/* ── Sidebar resize handle ────────────────────────────────────────────────── */
#sidebar-resize {
  width: 4px;
  cursor: col-resize;
  background: transparent;
  flex-shrink: 0;
  transition: background 0.15s;
  position: relative;
  z-index: 10;
}
#sidebar-resize:hover,
#sidebar-resize.dragging {
  background: var(--accent);
}

#content {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg0);
  position: relative;
}

#player-bar {
  height: var(--player-h);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-top: 1px solid var(--glass-border);
  display: flex;
  align-items: center;
  padding: 0 16px;
  gap: 16px;
  flex-shrink: 0;
  position: relative;
  overflow: hidden;
}

/* ── Header ───────────────────────────────────────────────────────────────── */
/* ── Logo ─────────────────────────────────────────────────────────────────── */
@keyframes logo-pulse {
  0%,100% { text-shadow: 0 0 6px rgba(240,114,42,0.55), 0 0 18px rgba(240,114,42,0.30), 0 0 38px rgba(240,114,42,0.12); }
  50%     { text-shadow: 0 0 10px rgba(240,114,42,0.85), 0 0 28px rgba(240,114,42,0.55), 0 0 60px rgba(240,114,42,0.28); }
}

.logo {
  display: flex;
  flex-direction: row;
  align-items: center;
  white-space: nowrap;
  gap: 7px;
}
.logo-text {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0;
}
/* Speaker emoji — glowing muted blue with plain overlay */
.logo-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  font-size: 24px;
  line-height: 1;
  flex-shrink: 0;
}
.logo-icon-glow {
  display: inline-block;
  color: rgba(107,200,240,0.84);
  filter:
    drop-shadow(0 0 3px rgba(107,200,240,0.8))
    drop-shadow(0 0 8px rgba(107,200,240,0.55))
    drop-shadow(0 0 16px rgba(107,200,240,0.3));
  /* Pulse only on hover/focus — a perpetual breathing glow in the corner
     of every page is the kind of low-priority ambient motion that wears
     down attention.  The hover one-shot still rewards interaction. */
}
.logo:hover .logo-icon-glow,
.logo:focus-within .logo-icon-glow {
  animation: logo-icon-pulse 3.2s ease-in-out 1;
}
.logo-icon-top {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
}
@keyframes logo-icon-pulse {
  0%, 100% {
    filter:
      drop-shadow(0 0 3px rgba(107,200,240,0.8))
      drop-shadow(0 0 8px rgba(107,200,240,0.55))
      drop-shadow(0 0 16px rgba(107,200,240,0.3));
  }
  50% {
    filter:
      drop-shadow(0 0 5px rgba(107,200,240,0.95))
      drop-shadow(0 0 13px rgba(107,200,240,0.7))
      drop-shadow(0 0 24px rgba(107,200,240,0.45));
  }
}

.logo-title {
  font-size: 16px;
  font-weight: 800;
  color: var(--accent);
  letter-spacing: -0.5px;
  line-height: 1;
  /* See .logo-icon-glow above — perpetual pulse retired in favour of a
     hover/focus one-shot.  Static glow comes from the text-shadow that
     logo-pulse 0%/100% defines (carried over below). */
  text-shadow:
    0 0 6px rgba(240,114,42,0.55),
    0 0 18px rgba(240,114,42,0.30),
    0 0 38px rgba(240,114,42,0.12);
}
.logo:hover .logo-title,
.logo:focus-within .logo-title {
  animation: logo-pulse 3.2s ease-in-out 1;
}

.logo-byline {
  /* Bumped 8 → 10px (still smaller than min-text 11 floor by design — this
     is a stylized brand-tag, not body copy; use color+weight for hierarchy
     elsewhere). */
  font-size: 10px;
  font-weight: 400;
  color: var(--accent-dim);
  letter-spacing: 0.3px;
  line-height: 1;
  text-shadow: 0 0 6px rgba(240,114,42,0.3);
  text-align: right;
}

.search-wrap {
  flex: 1;
  max-width: 380px;
  position: relative;
}

#search-input {
  width: 100%;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  color: var(--text0);
  padding: 6px 30px 6px 12px;
  font-size: 12px;
  outline: none;
  transition: border-color 0.15s, background 0.15s;
  backdrop-filter: blur(8px);
}
#search-input:focus {
  border-color: var(--accent);
  background: rgba(255,255,255,0.09);
}
#search-input::placeholder { color: var(--text2); }

.search-clear {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
  color: var(--text2);
  font-size: 15px;
  line-height: 1;
  /* Reset the native <button> chrome introduced when this was upgraded
     from a <span> to a real focusable button — otherwise the user agent
     stylesheet paints a gray box inside the search field. */
  background: transparent;
  border: 0;
  padding: 0;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

.header-actions button,
.header-actions a.header-link {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 6px;
  color: var(--text1);
  padding: 5px 12px;
  cursor: pointer;
  font-size: 12px;
  transition: background 0.12s, color 0.12s;
  backdrop-filter: blur(8px);
}
/* The docs link is an <a> (native new-tab / middle-click) styled to match its
   button siblings. */
.header-actions a.header-link {
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  line-height: 1;
}
.header-actions button:hover,
.header-actions a.header-link:hover { background: rgba(255,255,255,0.13); color: var(--text0); }
/* The "?" help button is text, not an emoji glyph — bump weight so it reads
   as a deliberate icon alongside its emoji siblings. */
.btn-help { font-weight: 700; }

/* Admin gear button */
#btn-admin {
  background: none !important;
  border: none !important;
  padding: 4px 6px !important;
  font-size: 17px !important;
  color: var(--text2) !important;
  line-height: 1;
}
#btn-admin:hover { color: var(--text0) !important; transform: rotate(30deg); transition: transform 0.3s, color 0.15s; }

.scan-badge {
  font-size: 11px;
  color: var(--accent);
  background: rgba(240,114,42,0.14);
  border-radius: 10px;
  padding: 3px 9px;
  white-space: nowrap;
  border: 1px solid rgba(240,114,42,0.25);
}

/* ── Sidebar ──────────────────────────────────────────────────────────────── */
/* Generic section base — specificity (0,1,1) so per-section rules below
   need a #sidebar prefix to win on `flex` and `overflow`. */
#sidebar section { padding: 4px 0 8px; overflow: hidden; flex: none; }
/* `flex: 0 1 auto` lets library/smart shrink when the sidebar is short
   (otherwise they keep their content height and squash folders/playlists). */
#sidebar #section-library  { min-height: 40px; overflow-y: auto; flex: 0 1 auto; }
#sidebar #section-folders  { min-height: 40px; display: flex; flex-direction: column; flex: 1; }
#sidebar #section-smart    { min-height: 40px; overflow-y: auto; flex: 0 1 auto; }

#sidebar h3 {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text2);
  padding: 6px 14px 4px;
}

.section-hdr-collapsible {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-right: 8px !important;
}
.btn-section-toggle {
  background: none;
  border: none;
  color: var(--text2);
  cursor: pointer;
  font-size: 11px;
  padding: 2px 4px;
  transition: transform 0.15s;
}
.btn-section-toggle.collapsed { transform: rotate(-90deg); }

#sidebar ul { list-style: none; }
#nav-library li {
  display: flex;
  align-items: center;
  padding: 6px 14px;
  cursor: pointer;
  color: var(--text1);
  border-radius: 5px;
  margin: 1px 6px;
  transition: background 0.1s, color 0.1s;
  font-size: 12.5px;
}
#nav-library li:hover { background: rgba(255,255,255,0.06); color: var(--text0); }
#nav-library li.active {
  background: rgba(240,114,42,0.14);
  color: var(--accent);
  font-weight: 500;
  border: 1px solid rgba(240,114,42,0.2);
}

/* ── Browse filter (sub-view search) ─────────────────────────────────────── */
#browse-filter-wrap {
  padding: 6px 10px 4px;
}
#browse-filter {
  width: 100%;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--glass-border);
  border-radius: 7px;
  padding: 5px 10px;
  font-size: 12px;
  color: var(--text0);
  outline: none;
  transition: border-color 0.15s, background 0.15s;
}
#browse-filter:focus {
  border-color: rgba(240,114,42,0.4);
  background: rgba(255,255,255,0.09);
}
#browse-filter::placeholder { color: var(--text2); }

/* ── Folder tree ──────────────────────────────────────────────────────────── */
#folder-tree-wrap { overflow-y: auto; flex: 1; min-height: 0; }

.folder-tree,
.folder-tree ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
/* Override the above for nested children — must be more specific */
.folder-tree .tree-children {
  padding-left: 28px;
  margin-left: 14px;
}

.folder-tree > li.tree-root { margin: 1px 0; }

/* Pulsing glow on root folders while scanning */
.tree-root-scanning > .tree-node {
  animation: scan-pulse 2s ease-in-out infinite;
}
.tree-root-scanning > .tree-node .tree-icon {
  animation: scan-icon-spin 1.5s linear infinite;
}
@keyframes scan-pulse {
  0%, 100% { background: transparent; color: var(--text1); }
  50%      { background: rgba(240,114,42,0.12); color: var(--accent); }
}
@keyframes scan-icon-spin {
  0%   { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.tree-node {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px 4px 0;
  cursor: pointer;
  border-radius: 5px;
  margin: 0 4px;
  color: var(--text1);
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  position: relative;
  user-select: none;
  transition: background 0.1s, color 0.1s;
}
.tree-node:hover { background: rgba(255,255,255,0.06); color: var(--text0); }
.tree-node.active { background: rgba(240,114,42,0.14); color: var(--accent); }

.tree-chevron {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  font-size: 8px;
  color: var(--text2);
  transition: transform 0.12s;
}
.tree-chevron.open { transform: rotate(90deg); }
.tree-chevron.leaf { visibility: hidden; }

.tree-icon {
  flex-shrink: 0;
  font-size: 11px;
  color: var(--text2);
}
.tree-node.active .tree-icon,
.tree-node:hover .tree-icon { color: var(--accent); }

.tree-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }

.tree-root > .tree-node .tree-label { font-weight: 500; color: var(--text0); }
.tree-root > .tree-node.active .tree-label { color: var(--accent); }

.tree-remove {
  display: none;
  background: none;
  border: none;
  color: var(--text2);
  cursor: pointer;
  font-size: 13px;
  padding: 0 2px;
  line-height: 1;
  margin-left: auto;
  flex-shrink: 0;
}
.tree-root:hover > .tree-node .tree-remove { display: inline; }
.tree-remove:hover { color: #e55; }

/* Indentation per nesting level — padding/margin in .folder-tree .tree-children above */
.tree-children {
  display: none;
  border-left: 1px solid var(--border);
}
.tree-children.open { display: block; }

.tree-audio-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
  opacity: 0.6;
}

.tree-empty {
  padding: 10px 14px;
  font-size: 11px;
  color: var(--text2);
  line-height: 1.6;
}

/* ── Sidebar horizontal splitters ─────────────────────────────────────────── */
.sidebar-h-splitter,
#sidebar-splitter {
  height: 4px;
  cursor: row-resize;
  background: transparent;
  flex-shrink: 0;
  transition: background 0.15s;
  border-top: 1px solid var(--border);
}
.sidebar-h-splitter:hover,
.sidebar-h-splitter.dragging,
#sidebar-splitter:hover,
#sidebar-splitter.dragging {
  background: var(--accent);
}
/* Keyboard reachability: splitters are draggable affordances with no
   visible text — keyboard users need a high-contrast outline to know
   focus landed there.  NOTE for HTML/JS team: splitters should also
   carry role="separator" + aria-orientation="vertical|horizontal" and
   tabindex="0" so they're actually focusable / announce correctly. */
.sidebar-h-splitter:focus-visible,
#sidebar-splitter:focus-visible,
#sidebar-resize:focus-visible,
#lyrics-panel .lyrics-resize:focus-visible {
  outline: 2px solid var(--state-focus);
  outline-offset: 1px;
  background: rgba(107,200,240,0.20);
}

/* ── Sidebar playlists ───────────────────────────────────────────────────── */
/* #sidebar prefix is required so this rule's specificity (0,2,0) beats
   the generic `#sidebar section` rule (0,1,1) on the `flex` property. */
#sidebar #section-playlists {
  flex: 1;
  min-height: 40px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
#section-playlists h3 {
  padding-right: 8px !important;
}
.btn-section-action {
  background: none;
  border: 1px solid rgba(240,114,42,0.22);
  color: var(--accent);
  cursor: pointer;
  font-size: 13px;
  padding: 0 5px;
  line-height: 1.2;
  border-radius: 4px;
  margin-left: auto;
  transition: background 0.12s;
}
.btn-section-action:hover { background: rgba(240,114,42,0.12); }

.sidebar-playlist-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  flex: 1;
}
.sidebar-playlist-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 14px;
  cursor: pointer;
  font-size: 12px;
  color: var(--text1);
  transition: background 0.1s, color 0.1s;
  user-select: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sidebar-playlist-item:hover { background: var(--row-hover); color: var(--text0); }
.sidebar-playlist-item.active { color: var(--accent); background: rgba(240,114,42,0.08); }
.sidebar-playlist-item .pl-count {
  font-size: 10px;
  color: var(--text2);
  margin-left: auto;
  flex-shrink: 0;
}
.sidebar-playlist-empty {
  padding: 8px 14px;
  font-size: 11px;
  color: var(--text2);
}

/* ── Browse header ────────────────────────────────────────────────────────── */
/* ── Group filter bar (content-area filter: Artists/Albums/Genres/etc.) ──── */
#group-filter-bar[hidden] { display: none; }
#group-filter-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 14px;
  border-bottom: 1px solid var(--border);
  background: rgba(255,255,255,0.018);
  flex-shrink: 0;
}
#group-filter-label {
  font-size: 11px;
  color: var(--text2);
  white-space: nowrap;
  min-width: 80px;
}
.group-filter-search {
  flex: 1;
  max-width: 320px;
  position: relative;
}
#group-filter-input {
  width: 100%;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--glass-border);
  border-radius: 7px;
  padding: 5px 10px;
  font-size: 12px;
  color: var(--text0);
  outline: none;
  transition: border-color 0.15s, background 0.15s;
  box-sizing: border-box;
}
#group-filter-input:focus {
  border-color: rgba(240,114,42,0.5);
  background: rgba(255,255,255,0.09);
}
#group-filter-input::placeholder { color: var(--text2); }

#browse-header[hidden] { display: none; }
#browse-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  border-bottom: 1px solid var(--border);
  background: rgba(255,255,255,0.025);
  flex-shrink: 0;
  backdrop-filter: blur(8px);
}
#browse-crumb { color: var(--text1); font-size: 13px; }
#browse-back {
  background: none;
  border: none;
  color: var(--accent);
  cursor: pointer;
  font-size: 12px;
  margin-left: auto;
}
.btn-export-csv {
  background: rgba(107,200,240,0.10);
  border: 1px solid rgba(107,200,240,0.22);
  border-radius: 6px;
  color: var(--accent);
  cursor: pointer;
  font-size: 11px;
  padding: 3px 10px;
  white-space: nowrap;
  transition: background 0.12s;
}
.btn-export-csv:hover { background: rgba(107,200,240,0.22); }
#browse-back:hover { text-decoration: underline; }
/* Play all / Shuffle all — lead the browse header on track-list views. */
.browse-play-btn {
  background: rgba(107,200,240,0.10);
  border: 1px solid rgba(107,200,240,0.22);
  border-radius: 6px;
  color: var(--accent);
  cursor: pointer;
  font-size: 11.5px;
  font-weight: 600;
  padding: 4px 11px;
  white-space: nowrap;
  transition: background 0.12s;
}
.browse-play-btn[hidden] { display: none; }
.browse-play-btn:hover { background: rgba(107,200,240,0.22); }
/* A–Z jump rail — a full-height gutter down the LEFT edge of the results, its
   27 letters spread in equal slices top→bottom (minimises mouse travel).  ``top``
   is synced in JS to the results area; ``bottom:0`` anchors the base. */
.has-az-rail { position: relative; }
#az-rail {
  position: absolute; left: 0; bottom: 0;
  display: flex; flex-direction: column;
  width: 24px; z-index: 6; padding: 2px 0; user-select: none;
  background: linear-gradient(90deg, rgba(255,255,255,0.035), transparent);
}
#az-rail[hidden] { display: none; }
.az-letter {
  flex: 1 1 0; min-height: 0;                 /* equal slices spanning full height */
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer;
  color: var(--text2); font-weight: 700; font-size: 12px; line-height: 1;
  padding: 0; margin: 1px 2px; border-radius: 4px;
  transition: background 0.1s, color 0.1s;
}
.az-letter:hover { color: var(--accent); background: rgba(107,200,240,0.22); }
/* Reserve the gutter so table rows don't slide under the rail. */
#track-list-wrap.az-padded { padding-left: 26px; }

/* ── Track table ──────────────────────────────────────────────────────────── */
#track-list-wrap {
  flex: 1;
  overflow-y: auto;
  padding-bottom: 4px;
  position: relative;
  /* CRITICAL for virtual scroll: opt out of Chrome/Edge/Firefox scroll
     anchoring.  The virtual-scroll pool resizes the top spacer the moment
     ``_vsStart`` advances past 0 (around row 35 with the default buffer).
     With ``overflow-anchor: auto`` (the browser default) the engine reads
     "content above the viewport just got taller" and adds the same amount
     to scrollTop to keep visible content stable.  That scrollTop bump
     fires another scroll event, which advances ``_vsStart`` again, which
     grows the top spacer again — a feedback loop that drifts scrollTop
     downward at ~ROW_H per frame until the list bottoms out.  The user
     sees this as the list "running away" once they scroll past row ~35,
     accelerating with each frame, slowing when they fight it (the
     same anchor that's pulling them down now resists their input), and
     stopping only when the list ends (``_vsStart`` can't grow further).
     Our spacer math is already self-consistent — the pool rows shift
     down by the same amount the top spacer grew, so unanchored scroll
     keeps the same tracks visible.  Just disable the anchor.  */
  overflow-anchor: none;
}

#track-table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
}
/* Column visibility — driven by ``hide-col-*`` classes on the table container
   (set once in _applyColVisibility on load / column-menu change) so hiding a
   column is a single class toggle and freshly-pooled virtual-scroll rows
   inherit it, instead of an inline-style scan of every cell every fling frame. */
#track-table.hide-col-num .col-num,
#track-table.hide-col-cover .col-cover,
#track-table.hide-col-title .col-title,
#track-table.hide-col-album-artist .col-album-artist,
#track-table.hide-col-artist .col-artist,
#track-table.hide-col-album .col-album,
#track-table.hide-col-track .col-track,
#track-table.hide-col-year .col-year,
#track-table.hide-col-dur .col-dur,
#track-table.hide-col-format .col-format,
#track-table.hide-col-location .col-location,
#track-table.hide-col-rating .col-rating { display: none !important; }

#track-table thead {
  position: sticky;
  top: 0;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  z-index: 2;
}

#track-table th {
  text-align: left;
  padding: 7px 10px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text2);
  border-bottom: 1px solid var(--border);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  user-select: none;
}
#track-table th[data-sort] { cursor: pointer; }
#track-table th[data-sort]:hover { color: var(--text1); }
#track-table th.sorted { color: var(--accent); }

/* Sort arrows */
#track-table th[data-sort]::after {
  content: " \2195"; /* ↕ */
  font-size: 9px;
  opacity: 0.4;
}
#track-table th[data-sort].sorted-asc::after {
  content: " \2191"; /* ↑ */
  opacity: 1;
  color: var(--accent);
}
#track-table th[data-sort].sorted-desc::after {
  content: " \2193"; /* ↓ */
  opacity: 1;
  color: var(--accent);
}

.col-num          { width: 36px; }
.col-cover        { width: 36px; padding: 2px 4px; }
/* The cover frame is a fixed 28×28 inline-block holding a format-emoji
   placeholder and the actual cover <img> as absolutely-positioned
   siblings.  Wrapping them in a frame (vs. positioning both off the
   ``<td>``) gives them a single coordinate space — ``<td>`` content is
   ``vertical-align: middle`` by default, which would offset the
   in-flow <img> from the absolutely-positioned placeholder by a few
   pixels.  Both children fill the frame exactly, so they pixel-overlap.
   The placeholder is always rendered, and the <img> starts transparent
   until ``.loaded`` — covers the placeholder when a real bitmap decoded,
   otherwise the glyph shows through (no broken-image glyph ever). */
.col-cover .col-cover-frame {
  position: relative;
  display: inline-block;
  width: 28px;
  height: 28px;
  border-radius: 4px;
  background: var(--bg3);
  vertical-align: middle;
  overflow: hidden;
}
.col-cover .row-art-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  line-height: 1;
  color: rgba(107,200,240,0.82);
  filter: drop-shadow(0 0 4px rgba(107,200,240,0.85))
          drop-shadow(0 0 10px rgba(107,200,240,0.45));
  user-select: none;
  pointer-events: none;
}
.col-cover .row-cover-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transition: opacity 120ms ease;
}
.col-cover .row-cover-img.loaded { opacity: 1; }
.col-title        { width: 22%; }
.col-album-artist { width: 16%; }
.col-artist       { width: 16%; }
.col-album        { width: 18%; }
.col-track        { width: 54px; text-align: center; font-variant-numeric: tabular-nums; }
.col-year         { width: 48px; }
.col-dur          { width: 60px; text-align: right; }
.col-format       { width: 96px; text-align: left; color: var(--text2); font-size: 11px; }
.col-location     { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: var(--text2); }

#track-table tbody tr {
  cursor: pointer;
  transition: background 0.07s;
}
#track-table tbody tr:hover { background: var(--row-hover); }
#track-table tbody tr.playing { background: var(--row-playing); }
/* Selection tint matches the queue/playlist visual language (cyan tint
   + accent stripe) so a user who selects rows in the library and
   switches panels doesn't see a different selection style.
   Because the table uses ``border-collapse: collapse``, ``tr`` backgrounds
   don't paint reliably in every browser — colour the ``td`` cells instead
   and put the accent stripe on the leftmost cell. */
#track-table tbody tr.selected > td,
#track-table tbody tr.multi-selected > td {
  background: rgba(107,200,240,0.10);
}
#track-table tbody tr.selected > td:first-child,
#track-table tbody tr.multi-selected > td:first-child {
  box-shadow: inset 2px 0 0 var(--art-glow-soft);
}

/* The "currently playing" tint must win over both multi-select and
   keyboard-focus tints, otherwise a row that's both playing AND in the
   user's selection looks indistinguishable from a plain selection.
   The .multi-selected / .kb-focused rules live later in the file with
   the same specificity, so we need a higher-specificity override here. */
#track-table tbody tr.playing.selected > td,
#track-table tbody tr.playing.multi-selected > td,
#track-table tbody tr.playing.kb-focused > td {
  background: var(--row-playing);
}
#track-table tbody tr.playing.selected > td:first-child,
#track-table tbody tr.playing.multi-selected > td:first-child,
#track-table tbody tr.playing.kb-focused > td:first-child {
  box-shadow: inset 3px 0 0 var(--accent);
}

#track-table td {
  padding: 5px 10px;
  color: var(--text1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12.5px;
}
#track-table td.col-title { color: var(--text0); font-weight: 500; }
#track-table td.col-dur { text-align: right; color: var(--text2); font-variant-numeric: tabular-nums; }
#track-table td.col-num { color: var(--text2); }

/* Playing-row marker is the single most important state indicator in the
   track table — 8px disappeared on Retina (UX/UI #2). 11px stays inline
   with the row number column without dominating it. */
tr.playing td.col-num::after { content: "▶"; font-size: 11px; color: var(--accent); margin-left: 2px; }
tr.playing td.col-num { color: transparent; }
tr.playing td.col-title { color: var(--accent); }
/* Live now-playing marker: animated equalizer bars replace the static ▶. */
tr.playing td.col-num:has(.row-eq)::after { content: none; }
.row-eq { display: inline-flex; align-items: flex-end; gap: 1.5px; height: 12px; vertical-align: middle; }
.row-eq i { width: 2px; height: 100%; background: var(--accent); transform-origin: bottom; animation: row-eq-bar 0.9s ease-in-out infinite; }
.row-eq i:nth-child(2) { animation-delay: 0.3s; }
.row-eq i:nth-child(3) { animation-delay: 0.15s; }
@keyframes row-eq-bar { 0%, 100% { transform: scaleY(0.28); } 50% { transform: scaleY(1); } }
@media (prefers-reduced-motion: reduce) {
  .row-eq i { animation: none; }
  .row-eq i:nth-child(1) { height: 55%; }
  .row-eq i:nth-child(3) { height: 72%; }
}
/* Locate: briefly pulse the row we jumped to. */
tr.locate-pulse > td { animation: locate-pulse-row 1.4s ease-out; }
@keyframes locate-pulse-row { 0% { background: rgba(107,200,240,0.35); } 100% { background: transparent; } }
#btn-locate { background: none; border: none; color: var(--text2); cursor: pointer; font-size: 15px; padding: 0 4px; line-height: 1; }
#btn-locate:hover { color: var(--accent); }

/* Row hover swap: row number → ▶ play glyph.  Discoverability fix —
   the dblclick-to-play behaviour was previously signalled only by the
   cursor pointer.  Now every hovered row shows a visible play hint. */
.row-play-glyph { display: none; color: var(--accent); font-size: 11px; }
#track-table tbody tr:hover .row-play-glyph,
#track-table tbody tr:focus-within .row-play-glyph { display: inline; }
#track-table tbody tr:hover .row-num,
#track-table tbody tr:focus-within .row-num { display: none; }
/* Playing row keeps its ▶ marker, drop the hover glyph to avoid double. */
#track-table tbody tr.playing .row-play-glyph { display: none; }
#track-table tbody tr.playing .row-num { display: inline; }

/* Unscanned files (filesystem-discovered, metadata not yet in store) */
tr.unscanned td { opacity: 0.5; }
tr.unscanned td.col-title { font-style: italic; }

/* Dim dashes for empty metadata — reduces visual noise */
td.col-empty { color: var(--text2) !important; opacity: 0.4; }

/* ── Skeleton loading rows ────────────────────────────────────────────────── */
tr.skeleton td { pointer-events: none; }
.skel-bar {
  display: inline-block;
  height: 10px;
  border-radius: 4px;
  background: linear-gradient(90deg, var(--bg3) 25%, var(--bg4) 50%, var(--bg3) 75%);
  background-size: 200% 100%;
  animation: skel-shimmer 1.4s ease-in-out infinite;
}
@keyframes skel-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
/* Stations list loading skeleton (perceived-perf 2.8) */
.st-skel-row { display: flex; align-items: center; gap: 14px; padding: 10px 4px; }
.st-skel-art { width: 40px; height: 40px; border-radius: 8px; flex: 0 0 auto; }
.st-skel-line { height: 14px; width: 42%; }
/* Track-Info artist-bio loading skeleton (perceived-perf 2.4) */
.ti-bio-skel { padding: 4px 0; overflow: hidden; }
.ti-bio-skel-img { float: left; width: 72px; height: 72px; border-radius: 8px; margin: 0 12px 6px 0; }
.ti-bio-skel .skel-bar { display: block; height: 12px; margin: 0 0 8px; width: 100%; }

/* ── Optimistic fade during view transitions ──────────────────────────────── */
#track-list-wrap.view-loading {
  opacity: 0.45;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
#track-list-wrap {
  transition: opacity 0.12s ease;
}

/* ── Clickable metadata cells ─────────────────────────────────────────────── */
td.col-album-artist .cell-link,
td.col-album .cell-link {
  cursor: pointer;
  border-bottom: 1px solid transparent;
  transition: color 0.1s, border-color 0.1s;
}
td.col-album-artist .cell-link:hover,
td.col-album .cell-link:hover {
  color: var(--accent);
  border-bottom-color: var(--accent-dim);
}

/* ── Sidebar count badges ─────────────────────────────────────────────────── */
#nav-library .nav-count {
  margin-left: auto;
  font-size: 10px;
  color: var(--text2);
  font-variant-numeric: tabular-nums;
}

#track-empty, #track-loading {
  padding: 40px;
  text-align: center;
  color: var(--text2);
}

/* Reusable empty-state component.  Use ``<div class="empty-state"
   role="status" aria-live="polite">`` with a leading emoji span, an
   ``<h4>`` heading, optional ``<p>`` body, and an optional CTA button.
   The aria-live ensures screen-reader users are notified when an
   asynchronous fetch yields zero rows. */
.empty-state {
  /* Author rules have higher precedence than the UA's ``[hidden]``
     stylesheet, so a bare ``display: flex`` here would shadow the
     ``hidden`` attribute and leave the empty state painted below a
     non-empty list.  Gate the flex layout on ``:not([hidden])`` so the
     attribute actually toggles visibility. */
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 48px 24px;
  text-align: center;
  color: var(--text2);
}
.empty-state:not([hidden]) {
  display: flex;
}
.empty-state .empty-icon {
  font-size: 38px;
  line-height: 1;
  opacity: 0.65;
}
.empty-state h4 {
  font-size: 15px;
  font-weight: 600;
  color: var(--text1);
  margin: 0;
}
.empty-state p {
  font-size: 13px;
  color: var(--text2);
  max-width: 380px;
  line-height: 1.5;
  margin: 0;
}
.empty-state .empty-cta {
  margin-top: 8px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 8px 18px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.empty-state .empty-cta:hover { background: var(--accent-dim); }

/* ── Similar panel ────────────────────────────────────────────────────────── */
#similar-panel {
  width: 240px;
  border-left: 1px solid var(--border);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  overflow-y: auto;
  padding: 10px 0;
  flex-shrink: 0;
}
#similar-panel h3 {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--text2);
  padding: 0 12px 8px;
  letter-spacing: 0.06em;
}
#similar-list { list-style: none; }
#similar-list li {
  padding: 8px 12px;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  transition: background 0.08s;
}
#similar-list li:hover { background: rgba(255,255,255,0.05); }
.sim-title  { font-size: 12px; color: var(--text0); }
.sim-artist { font-size: 11px; color: var(--text2); }
.sim-score  { font-size: 10px; color: var(--accent); float: right; }

/* ── Lyrics panel ─────────────────────────────────────────────────────────── */
#lyrics-panel {
  width: 450px;
  min-width: 200px;
  max-width: 700px;
  border-left: 1px solid var(--border);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative;
}
#lyrics-panel .lyrics-resize {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 5px;
  cursor: col-resize;
  z-index: 10;
}
#lyrics-panel .lyrics-resize:hover,
#lyrics-panel .lyrics-resize.dragging {
  background: var(--accent);
  opacity: 0.4;
}
#lyrics-panel[hidden] { display: none; }
.lyrics-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 10px 14px 6px;
  flex-shrink: 0;
}
.lyrics-header h3 {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--text2);
  letter-spacing: 0.06em;
  margin: 0;
}
.lyrics-source {
  /* Bumped 9 → 11px to clear the 11px small-text floor.  Use opacity for
     hierarchy rather than dropping below the readable threshold. */
  font-size: 11px;
  color: var(--text2);
  opacity: 0.6;
}
.lyrics-body {
  flex: 1;
  overflow-y: auto;
  padding: 0 14px 14px;
  scroll-behavior: smooth;
}
.lyrics-content {
  font-size: 13px;
  line-height: 2;
  color: var(--text1);
  white-space: pre-wrap;
  word-wrap: break-word;
}
.lyrics-line {
  padding: 3px 8px 3px 10px;
  border-radius: 4px;
  border-left: 3px solid transparent;
  transition: color 0.25s, background 0.25s, border-color 0.25s, opacity 0.25s;
  cursor: default;
}
.lyrics-line.synced { cursor: pointer; }
.lyrics-line.active {
  color: #fff;
  background: rgba(240,114,42,0.22);
  border-left-color: var(--accent);
  font-weight: 600;
  text-shadow: 0 0 8px rgba(240,114,42,0.3);
}
.lyrics-line.past {
  color: var(--text2);
  opacity: 0.45;
}
.lyrics-loading {
  color: var(--text2);
  font-size: 12px;
  padding: 20px 0;
  text-align: center;
}
.lyrics-empty {
  color: var(--text2);
  font-size: 12px;
  padding: 20px 0;
  text-align: center;
  opacity: 0.6;
}
#btn-lyrics.on { color: var(--accent); text-shadow: 0 0 6px rgba(240,114,42,0.7), 0 0 16px rgba(240,114,42,0.4); }
#btn-lyrics.disabled {
  opacity: 0.25;
  cursor: not-allowed;
}

/* ── Player bar ───────────────────────────────────────────────────────────── */

/* Ambient art background — sits behind all player bar content */
#player-art-bg {
  position: absolute;
  inset: -20px;
  z-index: 0;
  background-size: cover;
  background-position: center;
  filter: blur(40px) saturate(180%) brightness(0.25);
  opacity: 0;
  transition: opacity 0.8s ease, background-image 0.5s ease;
  pointer-events: none;
}
#player-art-bg.active { opacity: 1; }

.player-art-info {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 260px;
  max-width: 420px;
  width: 32%;
  position: relative;
  z-index: 1;
}

/* ℹ Track info button */
.btn-track-info {
  background: none;
  border: none;
  color: #6bc8f0;
  cursor: pointer;
  font-size: 17px;
  padding: 2px 4px;
  line-height: 1;
  flex-shrink: 0;
  transition: color 0.15s, text-shadow 0.15s;
  border-radius: 4px;
  text-shadow: 0 0 6px rgba(107,200,240,0.6), 0 0 14px rgba(107,200,240,0.28);
}
.btn-track-info:hover {
  color: #9adcf8;
  text-shadow: 0 0 9px rgba(107,200,240,0.9), 0 0 22px rgba(107,200,240,0.5);
}

/* Player bar album art — fills the full player-bar height */
.art-thumb {
  width: calc(var(--player-h) - 2px);
  height: calc(var(--player-h) - 2px);
  border-radius: 6px;
  overflow: hidden;
  background: var(--bg3);
  flex-shrink: 0;
  position: relative;
  border: 1px solid var(--glass-border);
}
.art-thumb img {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
/* Placeholder: format-specific emoji when no album art — glowing muted blue */
.art-thumb .art-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(var(--player-h) * 0.52);
  line-height: 1;
  user-select: none;
  /* Visible emoji with a layered blue glow */
  color: rgba(107,200,240,0.82);
  filter: drop-shadow(0 0 8px  rgba(107,200,240,0.85))
          drop-shadow(0 0 20px rgba(107,200,240,0.55))
          drop-shadow(0 0 40px rgba(107,200,240,0.25));
}
.art-thumb svg {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 28px; height: 28px;
}

.player-meta { overflow: hidden; min-width: 0; }
.meta-title {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--text0);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Album Artist / Artist clickable meta line */
.meta-tags {
  display: block;
  font-size: 10px;
  color: var(--text2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 1px;
}
.meta-tags:empty { display: none; }
.meta-tags-label {
  opacity: 0.6;
  margin-right: 2px;
}
.meta-tags-sep {
  margin: 0 5px;
  opacity: 0.35;
}
a.meta-link {
  color: var(--text1);
  text-decoration: none;
  cursor: pointer;
  border-bottom: 1px solid transparent;
  transition: color 0.12s, border-color 0.12s;
}
a.meta-link:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* Playing Now breadcrumb */
.meta-path-crumb {
  display: block;
  font-size: 10px;
  color: var(--text2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}
.meta-path-crumb:empty { display: none; }
.crumb-label {
  opacity: 0.5;
  margin-right: 4px;
}
a.crumb-link {
  color: var(--text2);
  text-decoration: none;
  cursor: pointer;
  transition: color 0.12s;
}
a.crumb-link:hover { color: var(--accent); }
.crumb-sep {
  margin: 0 3px;
  opacity: 0.35;
  font-size: 10px;
}
.crumb-file {
  color: var(--text1);
  opacity: 0.7;
}

.player-center {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  position: relative;
  z-index: 1;
  /* TODO: refactor to flex centring — the 34px nudge accounts for the
     time-stamp row pushing the seek bar visually downward.  Full fix is a
     row+spacer flex layout but that wants visual testing.  Tracked under
     UX/UI #2 #13. */
  margin-bottom: 34px;
}
/* ── Now Playing large art ─────────────────────────────────────────────── */
.now-playing-art {
  position: absolute;
  inset: 0;
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--panel-bg);
  backdrop-filter: blur(20px);
  cursor: pointer;
  animation: np-art-in 0.3s ease-out;
}
.now-playing-art[hidden] { display: none; }

@keyframes np-art-in {
  from { opacity: 0; transform: scale(0.95); }
  to   { opacity: 1; transform: scale(1); }
}

.now-playing-art img {
  max-width: min(80%, 500px);
  max-height: 60vh;
  border-radius: 12px;
  box-shadow: 0 12px 48px rgba(0,0,0,0.7), 0 0 100px rgba(240,114,42,0.08);
  object-fit: contain;
  /* Hide until ``.loaded``.  Combined with the JS ``onerror`` handler
     that strips ``src``, this guarantees the browser's broken-image
     glyph never paints inside the overlay. */
  opacity: 0;
  transition: opacity 200ms ease;
}
.now-playing-art img.loaded { opacity: 1; }
.now-playing-art img:not([src]) { display: none; }

.now-playing-art-info {
  text-align: center;
  margin-top: 20px;
  max-width: 500px;
}
.np-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text0);
  margin-bottom: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.np-artist {
  font-size: 14px;
  color: var(--text1);
  margin-bottom: 2px;
}
.np-album {
  font-size: 12px;
  color: var(--text2);
  font-style: italic;
}

/* Converting badge: was viewport-centred at 55px which overflowed narrow
   panes.  Now docked top-right of the content pane at a saner 26px,
   shrinking to ~22px on phones via the responsive override below. */
.converting-badge {
  position: absolute;
  top: 16px;
  right: 16px;
  background: rgba(180, 50, 50, 0.85);
  color: #eee;
  font-size: 26px;
  font-weight: 600;
  padding: 8px 22px;
  border-radius: 12px;
  letter-spacing: 1px;
  z-index: 500;
  animation: converting-pulse 2.4s ease-in-out infinite;
  pointer-events: auto;   /* allow inline cancel button to receive clicks */
  max-width: calc(100% - 32px);
}
/* Inline cancel button — hidden by default; JS sets .cancellable when the
   transcoder has cooperative cancel support so the X becomes interactive. */
.converting-badge .badge-cancel {
  display: none;
  background: rgba(0,0,0,0.30);
  border: 1px solid rgba(255,255,255,0.25);
  border-radius: 999px;
  color: #fff;
  cursor: pointer;
  width: 22px;
  height: 22px;
  font-size: 13px;
  line-height: 1;
  margin-left: 10px;
  vertical-align: middle;
}
.converting-badge.cancellable .badge-cancel { display: inline-flex; align-items: center; justify-content: center; }
.converting-badge .badge-cancel:hover { background: rgba(0,0,0,0.55); }
.converting-badge[hidden] { display: none; }
@keyframes converting-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.5; }
}

/* Determinate variant — promoted by player.js as soon as transcode-status
   returns a non-zero percent.  The pulse stops because the moving bar
   itself is the animation: a determinate signal is psychologically more
   reassuring than a pulsing-but-static one (Hofman 2009 / Card 1983). */
.converting-badge.has-progress[hidden] { display: none; }
.converting-badge.has-progress {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  font-size: 24px;
  letter-spacing: 0.6px;
  padding: 16px 28px;
  min-width: 320px;
  animation: none;
}
.converting-badge.has-progress .progress-label {
  font-weight: 600;
  text-align: center;
}
.converting-badge.has-progress .progress-bar {
  position: relative;
  width: 100%;
  height: 10px;
  background: rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  overflow: hidden;
}
.converting-badge.has-progress .progress-fill {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, #ffd1c2, #ffffff);
  border-radius: 999px;
  transition: width 0.45s cubic-bezier(0.22, 0.61, 0.36, 1);
  box-shadow: 0 0 8px rgba(255, 255, 255, 0.55);
}
.converting-badge.has-progress .progress-eta {
  font-size: 16px;
  font-weight: 500;
  text-align: center;
  opacity: 0.92;
  min-height: 1em;     /* reserve the row so the badge doesn't jump
                          between "n s left" and "" */
}
@media (prefers-reduced-motion: reduce) {
  /* Respect the user's preference — skip the easing on the fill so the
     bar snaps instead of animating.  The reset is intentional: a
     reduced-motion user has explicitly opted out of even subtle
     transition curves. */
  .converting-badge.has-progress .progress-fill { transition: none; }
  .converting-badge.has-progress .progress-bar.viz-packets .progress-fill { animation: none; }
}

/* Transcode "packet assembly" skin (viz #8) — render the determinate fill
   as discrete blocks that accrue as the WAV is assembled, with a shimmer
   sweep reading as "writing".  Pure CSS over the existing fill; the
   progressbar a11y attributes are untouched.  Gated on the now-playing
   viz group (player.js adds .viz-packets). */
.converting-badge.has-progress .progress-bar.viz-packets .progress-fill {
  background:
    linear-gradient(90deg, rgba(255,255,255,0.55), rgba(255,255,255,0.0) 60%) 0 0 / 22px 100% no-repeat,
    repeating-linear-gradient(90deg,
      #ffe6db 0, #ffe6db 8px,
      rgba(255,255,255,0.35) 8px, rgba(255,255,255,0.35) 10px);
  background-color: #ffd1c2;
  animation: viz-packet-shimmer 1.1s linear infinite;
}
@keyframes viz-packet-shimmer {
  from { background-position: -22px 0, 0 0; }
  to   { background-position: 60px 0, 0 0; }
}

/* Buffering badge — smaller, less alarming than the converting badge.
   Shown briefly while we wait for the preload buffer to fill. */
.buffering-badge {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(40, 40, 50, 0.78);
  color: #f0f0f0;
  font-size: 22px;
  font-weight: 500;
  padding: 8px 22px;
  border-radius: 12px;
  letter-spacing: 0.6px;
  z-index: 500;
  animation: converting-pulse 1.6s ease-in-out infinite;
  pointer-events: none;
}
.buffering-badge[hidden] { display: none; }

/* ── Transport controls — frosted pill ─────────────────────────────────── */
.player-controls {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 2px 24px;   /* flatter + wider */
  border-radius: 999px;
  background: linear-gradient(
    to bottom,
    rgba(255,255,255,0.07) 0%,
    rgba(255,255,255,0.03) 100%
  );
  border: 1px solid rgba(255,255,255,0.10);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow:
    0 4px 18px rgba(0,0,0,0.35),
    inset 0 1px 0 rgba(255,255,255,0.06),
    0 0 32px rgba(240,114,42,0.07);
}
.player-controls button {
  display: flex;
  align-items: center;
  justify-content: center;
  /* 36px desktop floor — was 32px which is under the loose 36px hit-target
     minimum.  The @media (max-width:1024px) block below promotes this to
     44px on touch.  Play-pause has its own 42px declaration above. */
  width: 36px;
  height: 36px;
  background: none;
  border: none;
  /* Themed colour so the transport stays visible in light mode — the
     previous hardcoded ``rgba(255,255,255,0.65)`` rendered 65%-opacity
     white on a near-white player-bar (UX/UI #2 + Visual-Test #1). */
  color: var(--text1);
  cursor: pointer;
  font-size: 13px;
  border-radius: 50%;
  transition: color 0.15s, background 0.15s;
  flex-shrink: 0;
}
.player-controls button:hover {
  color: var(--text0);
  background: var(--row-hover);
}

/* Play-pause: !important removed so the @media (max-width:1024px) touch
   block can promote width/height to 44px on tablets/phones.  The original
   !important were defending against the .player-controls button selector
   above; we win on specificity (.btn-play-pause is more specific) so the
   bangs were always redundant. */
.btn-play-pause {
  width: 42px;
  height: 42px;
  background: var(--accent);
  color: #fff;
  border-radius: 50%;
  font-size: 17px;
  box-shadow:
    0 0 0 3px rgba(240,114,42,0.22),
    0 0 22px rgba(240,114,42,0.45);
  transition: background 0.15s, box-shadow 0.15s;
}
.btn-play-pause:hover {
  background: var(--accent-dim);
  box-shadow:
    0 0 0 4px rgba(240,114,42,0.30),
    0 0 30px rgba(240,114,42,0.55);
}

.player-progress {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  max-width: 480px;
}
.player-progress span {
  font-size: 11px;
  color: var(--text2);
  font-variant-numeric: tabular-nums;
  min-width: 34px;
}
#seek-bar, #volume-bar { flex: 1; }

/* Seek bar: fill colour stops at --pct (set by JS on input events) so
   the progress is visible behind the thumb without needing per-frame
   background-position math in JS.  Default 0% means the bar shows just
   the neutral track tint at rest. */
#seek-bar {
  background: linear-gradient(
    to right,
    var(--accent) 0%,
    var(--accent) var(--pct, 0%),
    rgba(255,255,255,0.12) var(--pct, 0%),
    rgba(255,255,255,0.12) 100%
  );
}

/* ── Range inputs ─────────────────────────────────────────────────────────── */
input[type="range"] {
  -webkit-appearance: none;
  height: 3px;
  background: rgba(255,255,255,0.12);
  border-radius: 2px;
  outline: none;
  cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--text0);
  cursor: pointer;
  transition: background 0.1s, transform 0.1s;
}
input[type="range"]:hover::-webkit-slider-thumb {
  background: var(--accent);
  transform: scale(1.15);
}
/* Firefox slider thumb — without this partner rule the seek + volume
   bars rendered with the default chunky native thumb (Visual-Test #2). */
input[type="range"]::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border: none;
  border-radius: 50%;
  background: var(--text0);
  cursor: pointer;
  transition: background 0.1s, transform 0.1s;
}
input[type="range"]:hover::-moz-range-thumb {
  background: var(--accent);
  transform: scale(1.15);
}

/* Touch / small-viewport: bump the thumb so a fingertip can land it
   reliably.  Track also gets thicker so the bigger thumb doesn't dwarf
   the rail.  Precision-pointer devices keep the smaller 12px puck. */
@media (hover: none), (max-width: 1024px) {
  input[type="range"] {
    height: 6px;
  }
  input[type="range"]::-webkit-slider-thumb {
    width: 22px;
    height: 22px;
  }
  input[type="range"]::-moz-range-thumb {
    width: 22px;
    height: 22px;
  }
}

/* ── Player right ─────────────────────────────────────────────────────────── */
.player-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12px;
  flex-shrink: 0;
  /* Mirror .player-art-info's sizing (width:32%, max 420px) so the two flanks
     are SYMMETRIC and the flex:1 .player-center is truly centred in the bar.
     Previously this was content-width (260–320px) while the left flank was 32%,
     so the centre column — and the radio-mode transport pill in it — sat ~80px
     right of centre.  Worse, content-width varies by engine (control glyph
     metrics differ), so Chromium looked roughly centred while Gecko/WebKit
     drifted further — the cross-browser misalignment the user saw.  Controls
     stay flex-end (right-aligned); the extra width is just empty space. */
  width: 32%;
  min-width: 260px;
  max-width: 420px;
  position: relative;
  z-index: 1;
}
.player-extra-controls { display: flex; gap: 5px; align-items: center; flex-wrap: nowrap; justify-content: flex-end; }
.ctrl-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: #6bc8f0;
  cursor: pointer;
  font-size: 17px;
  padding: 7px 9px;
  border-radius: 8px;
  transition: color 0.15s, text-shadow 0.15s, background 0.12s;
  white-space: nowrap;
  text-shadow: 0 0 6px rgba(107,200,240,0.55), 0 0 14px rgba(107,200,240,0.25);
}
.ctrl-toggle:hover {
  color: #9adcf8;
  background: rgba(107,200,240,0.09);
  text-shadow: 0 0 8px rgba(107,200,240,0.8), 0 0 20px rgba(107,200,240,0.4);
}
.ctrl-toggle.on {
  color: var(--accent);
  text-shadow: 0 0 6px rgba(240,114,42,0.7), 0 0 16px rgba(240,114,42,0.4);
}
.ctrl-toggle.on:hover {
  text-shadow: 0 0 8px rgba(240,114,42,0.9), 0 0 22px rgba(240,114,42,0.55);
}

/* EQ button */
#btn-eq {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 6px 10px;
  border-radius: 7px;
  border: 1px solid rgba(255,255,255,0.13);
}
#btn-eq.on {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  box-shadow: 0 0 12px rgba(240,114,42,0.45);
}
#btn-eq.on:hover { background: var(--accent-dim); }

.volume-wrap {
  display: flex;
  align-items: center;
  gap: 5px;
  width: 100%;   /* match the full width of .player-right, same as controls row */
}
.volume-wrap span { font-size: 13px; color: var(--text2); flex-shrink: 0; }
#volume-bar { flex: 1; min-width: 0; }

#visualizer-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
  display: block;
  opacity: 0;
  transition: opacity 0.6s ease;
}

/* The visualizer canvas (z-index:0, above #content's background) must sit BEHIND
   the content panels.  Only #track-list-wrap was positioned, so it alone painted
   above the canvas; these static panels painted UNDER it, letting a screen-
   filling visualizer (synthwave/lavalamp/…) wash their text out.  Lift them above
   the canvas — they're transparent, so the visualizer still shows through behind.
   (Listed explicitly rather than ``#content > *`` so we don't clobber the
   #now-playing-art overlay's own z-index:10.)
   #galaxy-view is itself a content visualization with an OPAQUE dark scene
   (#070a0f), so once lifted it fully COVERS the ambient visualizer rather than
   letting it bleed through — that's intended: the Galaxy owns the screen, two
   visualizations must not fight.  It was missing from this list, so a playing
   visualizer washed the constellation out. */
#stations-view,
#album-grid,
#browse-header,
#group-filter-bar,
#selection-bar,
#browse-toolbar,
#galaxy-view {
  position: relative;
  z-index: 1;
}

/* ── Equalizer overlay ───────────────────────────────────────────────────── */
#eq-overlay {
  position: fixed;
  inset: 0;
  z-index: 700;
  background: rgba(0,0,0,0.65);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
}
#eq-overlay.hidden { display: none; }

#eq-panel {
  background: var(--panel-bg);
  backdrop-filter: blur(40px) saturate(180%);
  -webkit-backdrop-filter: blur(40px) saturate(180%);
  border: 1px solid var(--glass-border);
  border-radius: 20px;
  box-shadow: 0 24px 64px rgba(0,0,0,0.75), 0 0 0 1px rgba(255,255,255,0.04) inset;
  padding: 28px 32px 34px;
  width: 740px;
  max-width: 96vw;
  animation: eq-in 0.2s ease;
}
@keyframes eq-in {
  from { opacity: 0; transform: scale(0.94) translateY(-10px); }
  to   { opacity: 1; transform: none; }
}

/* Header */
.eq-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 30px;
}
.eq-title {
  font-size: 16px;
  font-weight: 700;
  color: var(--text0);
  flex: 1;
  letter-spacing: -0.2px;
}
#eq-preset {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 7px;
  color: var(--text1);
  font-size: 12px;
  padding: 5px 10px;
  cursor: pointer;
  outline: none;
}
#eq-preset:focus { border-color: var(--accent); }
#eq-reset, #eq-close {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 7px;
  color: var(--text1);
  font-size: 12px;
  padding: 5px 12px;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
#eq-close { font-size: 18px; padding: 3px 10px; }
#eq-reset:hover, #eq-close:hover { background: rgba(255,255,255,0.14); color: var(--text0); }

/* Body */
.eq-body {
  display: flex;
  gap: 16px;
  align-items: flex-start;
}

/* ReplayGain (volume-leveling) mode selector in the EQ panel footer */
.eq-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid var(--border, rgba(255, 255, 255, 0.1));
}
.eq-rg-label {
  font-size: 13px;
  opacity: 0.85;
}
#eq-rg {
  min-width: 110px;
}

/* dB scale */
.eq-scale {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  /* Total height = db-val label (20px) + gap (10px) + track (200px) + gap (10px) + freq label (18px) */
  /* We only need to span the track itself: from just below db-val+gap to just above freq+gap */
  height: 200px;
  margin-top: 30px; /* skip db-val (20px) + gap (10px) */
  align-items: flex-end;
  flex-shrink: 0;
}
.eq-scale span {
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  color: var(--text2);
  line-height: 1;
  min-width: 26px;
  text-align: right;
}

/* Bands row */
.eq-bands {
  display: flex;
  justify-content: space-around;
  gap: 0;
  flex: 1;
}

/* Band column */
.eq-band {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  flex: 1;
}

/* dB value label */
.eq-db-val {
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--text2);
  text-align: center;
  height: 20px;
  line-height: 20px;
  min-width: 38px;
  transition: color 0.12s;
}

/* ── Custom vertical track ───────────────────────────────────────────────── */
.eq-track {
  position: relative;
  width: 44px;
  height: 200px;
  border-radius: 6px;
  background: rgba(255,255,255,0.07);
  cursor: pointer;
  user-select: none;
  touch-action: none;
  flex-shrink: 0;
}

/* 0 dB centre line */
.eq-track::before {
  content: '';
  position: absolute;
  left: 6px;
  right: 6px;
  top: 100px;         /* exactly half of 200px */
  height: 1px;
  background: rgba(255,255,255,0.22);
  border-radius: 1px;
  pointer-events: none;
}

/* Coloured fill between 0 dB line and thumb */
.eq-fill {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 6px;
  border-radius: 3px;
  background: var(--accent);
  pointer-events: none;
  transition: background 0.12s;
}

/* Draggable thumb */
.eq-thumb {
  position: absolute;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--text0);
  border: 2px solid rgba(255,255,255,0.35);
  box-shadow: 0 2px 14px rgba(0,0,0,0.6);
  cursor: grab;
  pointer-events: none;   /* track handles events, thumb is visual-only */
  transition: background 0.1s, box-shadow 0.1s, transform 0.05s;
  /* Start at 0 dB (top: 100px) set by JS */
}
.eq-track:hover .eq-thumb {
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: 0 0 18px rgba(240,114,42,0.5);
}
.eq-track:active .eq-thumb {
  cursor: grabbing;
  transform: translate(-50%, -50%) scale(1.15);
}

/* Frequency label */
.eq-freq {
  font-size: 11px;
  color: var(--text2);
  text-align: center;
  white-space: nowrap;
  height: 18px;
  line-height: 18px;
}

/* ── Admin overlay ────────────────────────────────────────────────────────── */
#admin-overlay {
  position: fixed;
  inset: 0;
  z-index: 999;
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
}
#admin-overlay.hidden { display: none; }
/* When styledConfirm is invoked from outside the admin context the overlay
   shouldn't paint as the full admin backdrop — fade to a much lighter
   modal scrim so the underlying playlist/library panel stays visible. */
#admin-overlay.confirm-standalone {
  background: rgba(0,0,0,0.35);
  backdrop-filter: blur(2px);
}

/* Auth dialog */
.admin-dialog {
  background: rgba(22,26,32,0.92);
  backdrop-filter: blur(40px) saturate(180%);
  -webkit-backdrop-filter: blur(40px) saturate(180%);
  border: 1px solid var(--glass-border);
  border-radius: 16px;
  box-shadow: var(--glass-shadow), 0 0 0 1px rgba(255,255,255,0.04) inset;
  padding: 32px 36px;
  width: 360px;
  max-width: 90vw;
  animation: dialog-in 0.22s ease;
}
@keyframes dialog-in {
  from { opacity: 0; transform: scale(0.94) translateY(-8px); }
  to   { opacity: 1; transform: none; }
}
.admin-dialog h2 {
  font-size: 16px;
  font-weight: 600;
  color: var(--text0);
  margin-bottom: 6px;
}
.admin-dialog .dialog-sub {
  font-size: 12px;
  color: var(--text2);
  margin-bottom: 22px;
}
.admin-dialog label {
  display: block;
  font-size: 11px;
  color: var(--text2);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  margin-bottom: 5px;
}
.admin-dialog input {
  width: 100%;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  color: var(--text0);
  padding: 8px 12px;
  font-size: 13px;
  outline: none;
  margin-bottom: 14px;
  transition: border-color 0.15s;
}
.admin-dialog input:focus { border-color: var(--accent); }
.admin-dialog .dialog-error {
  color: #e55;
  font-size: 12px;
  margin-bottom: 10px;
  min-height: 16px;
}
.dialog-actions {
  display: flex;
  gap: 10px;
  margin-top: 4px;
}
.btn-primary {
  flex: 1;
  background: var(--accent);
  border: none;
  border-radius: 8px;
  color: white;
  font-size: 13px;
  font-weight: 600;
  padding: 9px;
  cursor: pointer;
  transition: background 0.12s;
}
.btn-primary:hover { background: var(--accent-dim); }
.btn-secondary {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  color: var(--text1);
  font-size: 13px;
  padding: 9px 16px;
  cursor: pointer;
  transition: background 0.12s;
}
.btn-secondary:hover { background: rgba(255,255,255,0.12); }

/* Admin panel (shown after auth) */
#admin-panel {
  background: var(--panel-bg);
  backdrop-filter: blur(40px) saturate(180%);
  -webkit-backdrop-filter: blur(40px) saturate(180%);
  border: 1px solid var(--glass-border);
  border-radius: 16px;
  box-shadow: var(--glass-shadow);
  /* Width: bumped from 580px → 720px so the share-edit form + the
     new FTP Connection Pools panel can both render comfortably without
     wrapping the slider rows.  ``max-width: 94vw`` still keeps the
     panel from breaching the viewport on narrow displays. */
  width: 720px;
  max-width: 94vw;
  height: 75vh;               /* fixed height — no resize when switching tabs */
  max-height: 85vh;
  display: flex;
  flex-direction: column;
  animation: dialog-in 0.22s ease;
  overflow: hidden;
}

/* ── FTP Connection Pools section ───────────────────────────────────────
   New in v27.  One card per FTP share; the operator drags the scan +
   stream sliders to tune the per-pool budget.  Detected cap is
   read-only (auto-learned via 421/530 trips).  See admin.js
   _renderFtpPools / _attachFtpPoolHandlers and api/admin.py
   /admin/ftp-pool/* for the wiring. */
.admin-ftp-pool-list { display: flex; flex-direction: column; gap: 10px; }
.admin-ftp-pool-card {
  border: 1px solid var(--border-bright);
  border-radius: 10px;
  padding: 12px 14px;
  background: var(--bg2);
}
.admin-ftp-pool-card .pool-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-bottom: 10px;
}
.admin-ftp-pool-card .pool-name { font-weight: 600; font-size: 13.5px; }
.admin-ftp-pool-card .pool-host { font-size: 12px; color: var(--text2); margin-left: 6px; }
.admin-ftp-pool-card .pool-detected {
  font-size: 12px; color: var(--text2);
  display: flex; align-items: center; gap: 8px;
}
.admin-ftp-pool-card .pool-detected b { color: var(--text1); }
.admin-ftp-pool-card .pool-detected.warn b { color: var(--accent); }
.admin-ftp-pool-card .pool-slider-row {
  display: grid;
  grid-template-columns: 110px 1fr 44px;
  align-items: center;
  gap: 10px;
  margin: 4px 0;
}
.admin-ftp-pool-card .pool-slider-row label { font-size: 12.5px; color: var(--text2); }
.admin-ftp-pool-card .pool-slider-row input[type=range] {
  width: 100%; accent-color: var(--accent);
}
.admin-ftp-pool-card .pool-slider-row .pool-val {
  text-align: right; font-feature-settings: "tnum"; font-size: 13px;
}
.admin-ftp-pool-card .pool-effective {
  font-size: 12px; color: var(--text2); margin: 6px 0 8px;
}
.admin-ftp-pool-card .pool-actions {
  display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap;
}
.admin-ftp-pool-card .pool-actions button {
  font-size: 12px; padding: 5px 10px; border-radius: 6px;
}
.admin-ftp-pool-card .pool-live {
  font-size: 11.5px; color: var(--text3, var(--text2));
  font-family: ui-monospace, SF Mono, Menlo, monospace;
  margin-top: 4px;
}

/* ── Scan-progress section: Pause/Resume button ──────────────────────── */
#admin-scan-section h3 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.scan-pause-btn {
  font-size: 12px;
  padding: 4px 12px;
  border-radius: 6px;
  font-weight: 500;
}
.scan-pause-btn.is-paused {
  /* When paused, swap to the accent colour so the Resume action stands
     out — the badge text is the cue the user looks for, the button is
     the action they need to take.  Matches the "primary action when
     workflow is interrupted" pattern used by the AirPlay pair modal. */
  background: var(--accent, #f0722a);
  color: #000;
  border-color: transparent;
}
#admin-panel.hidden { display: none; }
#admin-alias-dialog.hidden { display: none; }
#confirm-dialog.hidden { display: none; }
.btn-danger-action {
  flex: 1;
  background: #c0392b;
  border: none;
  border-radius: 8px;
  color: white;
  font-size: 13px;
  font-weight: 600;
  padding: 9px;
  cursor: pointer;
  transition: background 0.12s;
}
.btn-danger-action:hover { background: #e74c3c; }

.admin-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 24px 14px;
  border-bottom: 1px solid var(--border);
}
.admin-panel-header h2 {
  font-size: 15px;
  font-weight: 600;
  color: var(--text0);
}
.btn-close {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 6px;
  color: var(--text1);
  cursor: pointer;
  font-size: 16px;
  padding: 3px 8px;
  line-height: 1;
}
.btn-close:hover { background: rgba(255,255,255,0.13); color: var(--text0); }

.admin-panel-body {
  flex: 1;
  overflow-y: auto;
  /* Clip horizontally so absolutely-positioned tooltips can never push the
     panel into a horizontal scroll — vertical scrolling is preserved. */
  overflow-x: clip;
  padding: 18px 24px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

.admin-section + .admin-section {
  margin-top: 10px;
  padding-top: 14px;
  border-top: 1px solid rgba(255,255,255,0.06);
}

.admin-section h3 {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text2);
  margin-bottom: 12px;
}

/* Scan progress */
.scan-progress-wrap { display: flex; flex-direction: column; gap: 6px; }
.scan-progress-bar {
  height: 6px;
  background: rgba(255,255,255,0.08);
  border-radius: 3px;
  overflow: hidden;
}
.scan-progress-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 3px;
  transition: width 0.3s ease;
  width: 0%;
}
.scan-progress-text {
  font-size: 12px;
  color: var(--text1);
  font-variant-numeric: tabular-nums;
}
.scan-progress-file {
  font-size: 11px;
  color: var(--text2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.scan-queue-list {
  font-size: 11px;
  color: var(--text2);
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-top: 4px;
}
.scan-queue-item {
  display: flex;
  align-items: center;
  gap: 6px;
}
.scan-queue-item .sq-icon { font-size: 10px; }
.scan-queue-item .sq-icon.active { color: var(--accent); }
.scan-queue-item .sq-icon.queued { color: var(--text2); }
.scan-queue-item .sq-path {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Stats grid */
.stats-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.stat-card {
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--glass-border);
  border-radius: 10px;
  padding: 12px 14px;
}
.stat-card .stat-value {
  font-size: 22px;
  font-weight: 700;
  color: var(--text0);
  line-height: 1;
  margin-bottom: 4px;
}
.stat-card .stat-label {
  font-size: 11px;
  color: var(--text2);
}
.stat-ok   { color: #4caf7a !important; }
.stat-warn { color: #e5963a !important; }

/* Dir list */
.admin-dir-list { display: flex; flex-direction: column; gap: 6px; }
.admin-dir-row {
  display: flex;
  align-items: center;
  gap: 8px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  padding: 8px 12px;
}
.admin-dir-path {
  flex: 1;
  font-size: 12px;
  color: var(--text1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.admin-dir-count {
  font-size: 11px;
  color: var(--text2);
  white-space: nowrap;
}
.btn-danger {
  background: rgba(220,60,60,0.14);
  border: 1px solid rgba(220,60,60,0.25);
  border-radius: 6px;
  color: #e06060;
  cursor: pointer;
  font-size: 11px;
  padding: 3px 8px;
  white-space: nowrap;
  transition: background 0.12s;
}
.btn-danger:hover { background: rgba(220,60,60,0.28); }

.btn-alias-edit {
  background: rgba(107,200,240,0.12);
  border: 1px solid rgba(107,200,240,0.25);
  border-radius: 6px;
  color: var(--accent);
  cursor: pointer;
  font-size: 11px;
  padding: 3px 8px;
  white-space: nowrap;
  transition: background 0.12s;
}
.btn-alias-edit:hover { background: rgba(107,200,240,0.25); }

/* Add folder row */
.admin-add-row {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}
.admin-add-row input {
  flex: 1;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  color: var(--text0);
  padding: 7px 12px;
  font-size: 12px;
  outline: none;
  transition: border-color 0.15s;
}
.admin-add-row input:focus { border-color: var(--accent); }
.admin-add-row button {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  color: var(--text1);
  cursor: pointer;
  font-size: 12px;
  padding: 7px 14px;
  white-space: nowrap;
  transition: background 0.12s;
}
.admin-add-row button:hover { background: rgba(255,255,255,0.13); color: var(--text0); }

/* Source toggle (Local / Network) */
.admin-source-toggle {
  margin-top: 14px;
  display: flex;
  gap: 0;
  margin-bottom: 8px;
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  overflow: hidden;
  width: fit-content;
}
.source-tab {
  background: transparent;
  border: none;
  color: var(--text2);
  font-size: 12px;
  padding: 6px 16px;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
.source-tab:hover { color: var(--text1); }
.source-tab.active {
  background: rgba(255,255,255,0.1);
  color: var(--text0);
}

/* Network share form */
.admin-add-network {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.net-form-row {
  display: flex;
  gap: 6px;
  align-items: center;
}
.net-form-row input,
.net-form-row select {
  background: var(--bg2);
  border: 1px solid var(--glass-border);
  border-radius: 6px;
  color: var(--text0);
  font-size: 12px;
  padding: 7px 10px;
  flex: 1;
  min-width: 0;
}
.net-form-row select { flex: 0 0 auto; width: 80px; cursor: pointer; }
.net-form-row input:focus { border-color: var(--accent); outline: none; }
.net-form-actions { margin-top: 2px; }
/* Anonymous / Guest toggle */
.net-anon-label {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  color: var(--text1);
  white-space: nowrap;
  cursor: pointer;
  flex-shrink: 0;
}
.net-anon-label input[type="checkbox"] { accent-color: var(--accent); }
.net-auth-row input:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.net-form-actions button {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  color: var(--text1);
  cursor: pointer;
  font-size: 12px;
  padding: 7px 14px;
  white-space: nowrap;
  transition: background 0.12s;
}
.net-form-actions button:hover { background: rgba(255,255,255,0.13); color: var(--text0); }
.net-status-msg { font-size: 11px; color: var(--text2); margin-left: 4px; }
.net-status-msg.net-ok { color: #4caf50; }
.net-status-msg.net-err { color: #e55; }

/* Dir row: status dot for network shares.
   Shape carries meaning, not just colour — color-blind users can read the
   connection state at a glance. */
.dir-status-dot {
  display: inline-block;
  width: 9px; height: 9px;
  flex-shrink: 0;
  margin-right: 4px;
  position: relative;
}
.dot-green {
  background: #4caf50;
  border-radius: 50%;
  box-shadow: 0 0 4px rgba(76,175,80,0.5);
}
/* Disconnected: red square with a strike through to differentiate by shape
   in addition to colour. */
.dot-red {
  background: #e55;
  border-radius: 2px;
  box-shadow: 0 0 4px rgba(238,85,85,0.5);
}
.dot-red::after {
  content: '';
  position: absolute;
  left: 1px; right: 1px; top: 50%;
  height: 1.5px;
  background: rgba(0,0,0,0.55);
  transform: translateY(-50%) rotate(-45deg);
  pointer-events: none;
}
.dir-unavailable .admin-dir-path { opacity: 0.5; }
.btn-reconnect {
  background: rgba(76,175,80,0.15);
  border: 1px solid rgba(76,175,80,0.3);
  border-radius: 6px;
  color: #4caf50;
  font-size: 11px;
  padding: 3px 10px;
  cursor: pointer;
  transition: background 0.12s;
}
.btn-reconnect:hover { background: rgba(76,175,80,0.25); }

/* Folder tree: unavailable root (network share OR local mount that's offline) */
.tree-unavailable > .tree-node { opacity: 0.55; }
.tree-unavail-badge {
  margin-left: 6px;
  padding: 0 6px;
  font-size: 10px;
  font-weight: 600;
  line-height: 15px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--warn, #e0a13a);
  border: 1px solid var(--warn, #e0a13a);
  border-radius: 8px;
  opacity: 0.9;
  white-space: nowrap;
}

/* ZIP scan checkbox */
.admin-zip-label {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  color: var(--text2);
  white-space: nowrap;
  cursor: pointer;
}
.admin-zip-label input[type="checkbox"] {
  accent-color: var(--accent);
}

/* Export/import row */
.admin-io-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.admin-io-row button {
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  color: var(--text1);
  cursor: pointer;
  font-size: 12px;
  padding: 7px 16px;
  transition: background 0.12s;
}
.admin-io-row button:hover { background: rgba(255,255,255,0.12); color: var(--text0); }

/* Reindex button */
.btn-reindex {
  background: rgba(240,114,42,0.12);
  border: 1px solid rgba(240,114,42,0.22);
  border-radius: 8px;
  color: var(--accent);
  cursor: pointer;
  font-size: 12px;
  padding: 7px 16px;
  transition: background 0.12s;
}
.btn-reindex:hover { background: rgba(240,114,42,0.22); }
.btn-reindex:disabled { opacity: 0.5; cursor: default; }

/* Per-dir Index / Re-Index buttons */
.btn-index-small {
  background: rgba(76,175,122,0.14);
  border: 1px solid rgba(76,175,122,0.28);
  border-radius: 6px;
  color: #4caf7a;
  cursor: pointer;
  font-size: 11px;
  padding: 3px 9px;
  white-space: nowrap;
  transition: background 0.12s;
}
.btn-index-small:hover { background: rgba(76,175,122,0.28); }
.btn-index-small:disabled { opacity: 0.5; cursor: default; }

.btn-reindex-small {
  background: rgba(240,114,42,0.12);
  border: 1px solid rgba(240,114,42,0.22);
  border-radius: 6px;
  color: var(--accent);
  cursor: pointer;
  font-size: 11px;
  padding: 3px 9px;
  white-space: nowrap;
  transition: background 0.12s;
}
.btn-reindex-small:hover { background: rgba(240,114,42,0.22); }
.btn-reindex-small:disabled { opacity: 0.5; cursor: default; }

.admin-msg {
  font-size: 12px;
  padding: 8px 12px;
  border-radius: 8px;
  margin-top: 4px;
}
/* Glyph prefixes so the status carries meaning beyond colour — colour-blind
   users (deuteranopia, protanopia) can't reliably distinguish the green/
   amber/red tints alone.  The leading symbol gives a shape-based cue. */
.admin-msg.ok::before   { content: '✓ '; font-weight: 700; }
.admin-msg.warn::before { content: '! '; font-weight: 700; }
.admin-msg.err::before  { content: '✕ '; font-weight: 700; }
.admin-msg.ok   { background: rgba(76,175,122,0.14); color: #4caf7a; border: 1px solid rgba(76,175,122,0.2); }
.admin-msg.warn { background: rgba(230,180,60,0.14); color: #e6b43c; border: 1px solid rgba(230,180,60,0.25); }
.admin-msg.err  { background: rgba(220,80,80,0.14);  color: #e06060; border: 1px solid rgba(220,80,80,0.2); }

/* Renderer status grid */
.renderer-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}
.renderer-item {
  display: flex;
  align-items: center;
  gap: 8px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 12px;
  color: var(--text1);
}
.renderer-icon {
  font-size: 14px;
  width: 18px;
  text-align: center;
  color: var(--text2);
}
.renderer-icon.renderer-ok   { color: #4caf7a; }
.renderer-icon.renderer-missing { color: #e06060; }
/* Warn tier — binary exists but is incomplete (e.g. ffmpeg lacks
   libmp3lame / libvorbis).  Different colour from missing so the
   user can distinguish "not installed" from "installed but limited". */
.renderer-icon.renderer-warn { color: #f0b840; }
/* In-flight cue while the probe is running — without animation users
   couldn't distinguish "still probing" from "stuck on ellipsis". */
.renderer-icon.renderer-loading {
  color: var(--text1);
  animation: renderer-pulse 1.2s ease-in-out infinite;
}
@keyframes renderer-pulse {
  0%, 100% { opacity: 0.4; }
  50%      { opacity: 1; }
}
.renderer-name { white-space: nowrap; }

/* Soundfont list */
.admin-sf-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.admin-sf-row {
  display: flex;
  align-items: center;
  gap: 8px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  padding: 7px 12px;
  transition: border-color 0.15s;
}
.admin-sf-row.sf-active {
  border-color: rgba(76,175,122,0.35);
  background: rgba(76,175,122,0.06);
}
.sf-select-btn {
  background: none;
  border: none;
  color: var(--text2);
  cursor: pointer;
  font-size: 16px;
  padding: 0 2px;
  line-height: 1;
  transition: color 0.12s;
}
.sf-select-btn:hover { color: var(--text0); }
.sf-select-btn.sf-selected { color: #4caf7a; }
.sf-name {
  flex: 1;
  font-size: 12px;
  color: var(--text1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sf-size {
  font-size: 11px;
  color: var(--text2);
  white-space: nowrap;
}
.sf-delete {
  font-size: 14px;
  padding: 2px 6px;
}

/* Soundfont actions */
.admin-sf-actions {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}
.btn-sf-upload {
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  color: var(--text1);
  cursor: pointer;
  font-size: 12px;
  padding: 7px 14px;
  transition: background 0.12s;
}
.btn-sf-upload:hover { background: rgba(255,255,255,0.12); color: var(--text0); }

/* Known soundfonts */
.admin-sf-known {
  margin-top: 14px;
}
.admin-sf-known h4 {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text2);
  margin-bottom: 8px;
}
.admin-sf-known-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.sf-known-row {
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--glass-border);
  border-radius: 8px;
  padding: 10px 12px;
}
.sf-known-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.sf-known-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text0);
}
.sf-known-desc {
  font-size: 11px;
  color: var(--text2);
}
.sf-known-meta {
  font-size: 10px;
  color: var(--text2);
  opacity: 0.7;
}
.sf-known-btn {
  background: rgba(76,175,122,0.14);
  border: 1px solid rgba(76,175,122,0.28);
  border-radius: 6px;
  color: #4caf7a;
  cursor: pointer;
  font-size: 11px;
  padding: 5px 12px;
  white-space: nowrap;
  transition: background 0.12s;
}
.sf-known-btn:hover { background: rgba(76,175,122,0.28); }
.sf-known-btn:disabled { opacity: 0.6; cursor: default; }
.sf-known-btn.sf-known-installed {
  background: rgba(76,175,122,0.08);
  border-color: rgba(76,175,122,0.15);
  color: #4caf7a;
  opacity: 0.7;
}

/* ═══════════════════════════════════════════════════════════════════════════
   Track Info Panel
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Tab bar ─────────────────────────────────────────────────────────────── */
.ti-tabs {
  display: flex;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  background: rgba(255,255,255,0.02);
  padding: 0 8px;
}
.ti-tab-btn {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text2);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  padding: 11px 16px 9px;
  cursor: pointer;
  transition: color 0.12s, border-color 0.12s;
  margin-bottom: -1px;
}
.ti-tab-btn:hover { color: var(--text1); }
.ti-tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }

/* ── Metadata pane (default active) ─────────────────────────────────────── */
.ti-meta-pane {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 20px 24px 28px;
}
.ti-meta-pane.hidden { display: none; }

/* ── Lyrics pane ─────────────────────────────────────────────────────────── */
.ti-lyrics-pane {
  flex: 1;
  overflow-y: auto;
  padding: 28px 36px 36px;
  display: none;
  flex-direction: column;
  align-items: center;
}
.ti-lyrics-pane.active { display: flex; }

.ti-lyrics-text {
  font-size: 15px;
  line-height: 2;
  color: var(--text0);
  white-space: pre-wrap;
  text-align: center;
  max-width: 520px;
  width: 100%;
}
.ti-lyrics-source {
  font-size: 10px;
  color: var(--text2);
  margin-top: 28px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  width: 100%;
  max-width: 520px;
  text-align: right;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.ti-lyrics-state {
  color: var(--text2);
  font-size: 13px;
  text-align: center;
  padding: 40px 20px;
  margin: auto;
}
.ti-lyrics-state .ti-lyrics-spinner {
  display: inline-block;
  width: 18px;
  height: 18px;
  border: 2px solid rgba(255,255,255,0.12);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: ti-spin 0.7s linear infinite;
  margin-bottom: 10px;
}

/* ── Synced lyrics ──────────────────────────────────────────────────────────── */
.ti-lyrics-synced {
  font-size: 15px;
  line-height: 2.2;
  text-align: center;
  max-width: 520px;
  width: 100%;
  scroll-behavior: smooth;
}
.lrc-line {
  color: var(--text2);
  transition: color 0.25s ease, transform 0.25s ease, font-size 0.25s ease;
  cursor: default;
  padding: 1px 0;
}
.lrc-line.active {
  color: var(--text0);
  font-weight: 600;
  font-size: 17px;
  transform: scale(1.04);
}

/* ── Scene pane (retro/demoscene tracks) ───────────────────────────────────── */
.ti-scene-pane {
  flex: 1;
  overflow-y: auto;
  padding: 20px 24px 28px;
  display: none;
  flex-direction: column;
}
.ti-scene-pane.active { display: flex; }
.ti-scene-pane.hidden { display: none; }

.ti-scene-artist { margin-bottom: 18px; }
.ti-scene-name {
  font-size: 19px;
  font-weight: 700;
  color: var(--text0);
  line-height: 1.2;
}
.ti-scene-sub  { font-size: 13px; color: var(--text1); margin-top: 3px; }
.ti-scene-aka  { font-size: 12px; color: var(--text2); margin-top: 2px; }

.ti-scene-block {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.ti-scene-h {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text2);
  margin-bottom: 8px;
}
.ti-scene-rel-title { font-size: 15px; font-weight: 600; color: var(--text0); }
.ti-scene-meta      { font-size: 12.5px; color: var(--text1); margin-top: 3px; }
.ti-scene-compo     { font-size: 12.5px; color: var(--text1); margin-top: 5px; }

.ti-scene-chips { margin-top: 9px; display: flex; flex-wrap: wrap; gap: 5px; }
.ti-scene-chip {
  font-size: 11px;
  padding: 2px 9px;
  border-radius: 10px;
  background: var(--border-bright, rgba(255,255,255,0.12));
  color: var(--text1);
  text-decoration: none;
  white-space: nowrap;
}
.ti-scene-chip:hover { background: var(--accent); color: #fff; }

.ti-scene-disco { list-style: none; margin: 0; padding: 0; }
.ti-scene-disco li {
  font-size: 13px;
  color: var(--text0);
  padding: 3px 0;
  border-bottom: 1px solid rgba(255,255,255,0.04);
}
.ti-scene-disco li a { color: var(--text0); text-decoration: none; }
.ti-scene-disco li a:hover { color: var(--accent); text-decoration: underline; }
.ti-scene-disco-m { font-size: 11.5px; color: var(--text2); }
.ti-scene-more {
  display: inline-block;
  margin-top: 9px;
  font-size: 12px;
  color: var(--accent);
  text-decoration: none;
}
.ti-scene-more:hover { text-decoration: underline; }

.ti-scene-baseline .ti-scene-b-row {
  display: flex;
  gap: 10px;
  font-size: 12px;
  color: var(--text2);
  padding: 2px 0;
}
.ti-scene-b-k { min-width: 96px; color: var(--text2); opacity: 0.8; }
.ti-scene-empty {
  color: var(--text2);
  font-size: 13px;
  padding: 22px 8px;
  text-align: center;
}

/* Year row corrected from a matched Demozoo production — a subtle accent so the
   overwrite is visible without shouting. */
.ti-value.ti-year-scene {
  color: var(--accent);
  cursor: help;
  border-bottom: 1px dotted var(--accent);
}

/* ── Sidebar ambient glow ──────────────────────────────────────────────────── */
#sidebar-glow {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 160px;
  z-index: 0;
  background-size: cover;
  background-position: center;
  filter: blur(50px) saturate(200%) brightness(0.18);
  opacity: 0;
  transition: opacity 1s ease, background-image 0.6s ease;
  pointer-events: none;
  mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
}
#sidebar-glow.active { opacity: 1; }

/* Ensure sidebar children sit above the glow */
#sidebar > section,
#sidebar > h3 { position: relative; z-index: 1; }

#ti-overlay {
  position: fixed;
  inset: 0;
  z-index: 800;
  background: rgba(0,0,0,0.60);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
}
#ti-overlay.hidden { display: none; }

/* JS toggles body.ti-open when the Track Info overlay opens; the original
   intent was a scroll-lock + body-dim that never had matching CSS. */
body.ti-open {
  overflow: hidden;
}

#ti-panel {
  background: var(--panel-bg);
  backdrop-filter: blur(40px) saturate(180%);
  -webkit-backdrop-filter: blur(40px) saturate(180%);
  border: 1px solid var(--glass-border);
  border-radius: 20px;
  box-shadow: 0 28px 72px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.04) inset;
  width: 720px;
  max-width: 96vw;
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: ti-in 0.22s ease;
}
@keyframes ti-in {
  from { opacity: 0; transform: scale(0.95) translateY(-10px); }
  to   { opacity: 1; transform: none; }
}

/* ── Navigation header ────────────────────────────────────────────────────── */
.ti-nav {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 14px 20px 12px;
  border-bottom: 1px solid var(--border);
  background: rgba(255,255,255,0.02);
  flex-shrink: 0;
}
.ti-nav-btn {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 7px;
  color: var(--text1);
  font-size: 11px;
  width: 34px;
  height: 30px;
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.ti-nav-btn:hover:not(:disabled) { background: rgba(255,255,255,0.14); color: var(--text0); }
.ti-nav-btn:disabled,
.ti-nav-btn.ti-btn-disabled {
  opacity: 0.3;
  cursor: default;
}
.ti-nav-label {
  flex: 1;
  font-size: 12px;
  font-weight: 600;
  color: var(--text1);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 0 8px;
}
.ti-close {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 7px;
  color: var(--text1);
  font-size: 18px;
  line-height: 1;
  padding: 3px 10px;
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
  flex-shrink: 0;
}
.ti-close:hover { background: rgba(255,255,255,0.14); color: var(--text0); }

/* ── Body: two columns ────────────────────────────────────────────────────── */
.ti-body {
  display: flex;
  gap: 0;
  flex: 1;
  overflow: hidden;
}

/* ── Left column: artwork + stats ────────────────────────────────────────── */
.ti-left {
  width: 300px;
  flex-shrink: 0;
  padding: 24px 20px 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  border-right: 1px solid var(--border);
  background: rgba(255,255,255,0.012);
}

.ti-art-box {
  width: 260px;
  height: 260px;
  border-radius: 12px;
  overflow: hidden;
  background: var(--bg3);
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  flex-shrink: 0;
}
.ti-art-box img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: 11px;
}
/* Format-specific emoji placeholder in the Track Info art box (260×260 px) */
.ti-art-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: 96px;
  line-height: 1;
  user-select: none;
  color: rgba(107,200,240,0.82);
  filter: drop-shadow(0 0 10px rgba(107,200,240,0.85))
          drop-shadow(0 0 26px rgba(107,200,240,0.55))
          drop-shadow(0 0 50px rgba(107,200,240,0.22));
}
/* Hide the placeholder once real art has loaded */
.ti-art-box.ti-has-art .ti-art-placeholder { display: none; }

/* No-art overlay (injected on fetch error — hides if placeholder already covers it) */
.ti-art-no-art { display: none; }
.ti-art-loading::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent);
  animation: ti-shimmer 1.4s infinite;
  border-radius: 12px;
}
@keyframes ti-shimmer {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}
@keyframes ti-spin {
  to { transform: rotate(360deg); }
}

/* Audio stats under artwork */
.ti-stats {
  width: 100%;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--glass-border);
  border-radius: 10px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.ti-stat-row {
  font-size: 11px;
  color: var(--text2);
  text-align: center;
  font-variant-numeric: tabular-nums;
  line-height: 1.3;
}
.ti-stat-row:first-child {
  font-size: 12px;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0.03em;
}
.ti-stat-size { color: var(--text2); }

/* ── Right column: scrollable metadata ───────────────────────────────────── */
.ti-right {
  flex: 1;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 0;
}


/* Section */
.ti-section {
  margin-bottom: 20px;
}
.ti-section-hdr {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text2);
  padding-bottom: 6px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}

/* Field row */
.ti-field {
  display: flex;
  align-items: baseline;
  gap: 0;
  padding: 5px 0;
  border-bottom: 1px solid rgba(255,255,255,0.03);
}
.ti-field:last-child { border-bottom: none; }

.ti-label {
  font-size: 11px;
  color: var(--text2);
  min-width: 110px;
  flex-shrink: 0;
  padding-right: 10px;
  text-align: right;
}
.ti-value {
  font-size: 12px;
  color: var(--text0);
  flex: 1;
  line-height: 1.4;
  word-break: break-word;
}
.ti-value.ti-empty { color: var(--text2); }
.ti-value-wrap { white-space: pre-wrap; }
.ti-value-path {
  font-family: "SF Mono", "Menlo", "Fira Code", monospace;
  font-size: 10px;
  color: var(--text2);
  word-break: break-all;
}
.ti-field-path { align-items: flex-start; }

/* ── Instrument list (tracker/SID/MIDI) ──────────────────────────────────── */
.ti-field-instruments { align-items: flex-start; }
.ti-instrument-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-top: 4px;
  max-height: 200px;
  overflow-y: auto;
  font-family: "SF Mono", "Menlo", "Monaco", monospace;
}
.ti-instrument {
  font-size: 10px;
  color: var(--text2);
  padding: 2px 0;
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Subsong picker (SID/SNDH/AHX/… multi-tune files) ─────────────── */
.ti-sub-hdrrow { display: flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap; }
.ti-sub-count { color: var(--text2); font-weight: 400; font-size: var(--text-sm); }
.ti-sub-actions { display: flex; gap: 6px; }
.ti-sub-abtn {
  font-size: var(--text-sm); padding: 4px 9px; border: 1px solid var(--border-bright);
  background: transparent; color: var(--text0); border-radius: var(--radius-sm);
  cursor: pointer; line-height: 1.2; white-space: nowrap;
}
.ti-sub-abtn:hover { border-color: var(--accent); color: var(--accent); }
.ti-sub-abtn-icon { padding: 4px 8px; }
.ti-sub-list {
  margin-top: 8px; max-height: 260px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.ti-sub-row {
  display: flex; align-items: center; gap: 10px; padding: 6px 10px;
  border-bottom: 1px solid var(--border); cursor: pointer; outline: none;
}
.ti-sub-row:last-child { border-bottom: none; }
.ti-sub-row:hover, .ti-sub-row:focus-visible { background: var(--accent-glow); }
.ti-sub-row.playing { background: var(--accent-glow); }
.ti-sub-row.playing .ti-sub-name { color: var(--accent); }
.ti-sub-eq { display: inline-flex; align-items: flex-end; gap: 2px; width: 14px; height: 13px; flex-shrink: 0; visibility: hidden; }
.ti-sub-row.playing .ti-sub-eq { visibility: visible; }
.ti-sub-eq i { width: 3px; background: var(--accent); border-radius: 1px; }
.ti-sub-eq i:nth-child(1) { height: 6px;  animation: ti-sub-eq 0.9s ease-in-out infinite; }
.ti-sub-eq i:nth-child(2) { height: 12px; animation: ti-sub-eq 0.9s ease-in-out 0.15s infinite; }
.ti-sub-eq i:nth-child(3) { height: 8px;  animation: ti-sub-eq 0.9s ease-in-out 0.30s infinite; }
@keyframes ti-sub-eq { 0%, 100% { transform: scaleY(0.5); } 50% { transform: scaleY(1); } }
@media (prefers-reduced-motion: reduce) { .ti-sub-eq i { animation: none; } }
.ti-sub-name { flex: 1; font-size: var(--text-sm); min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ti-sub-def { font-size: var(--text-xs); color: var(--accent); border: 1px solid var(--accent-dim); border-radius: var(--radius-sm); padding: 0 5px; margin-left: 6px; }
.ti-sub-len { font-size: var(--text-sm); color: var(--text2); font-variant-numeric: tabular-nums; }
.ti-sub-rowacts { display: flex; gap: 4px; opacity: 0; transition: opacity 0.12s; }
.ti-sub-row:hover .ti-sub-rowacts, .ti-sub-row:focus-visible .ti-sub-rowacts, .ti-sub-row.playing .ti-sub-rowacts { opacity: 1; }
.ti-sub-rbtn { border: none; background: transparent; color: var(--text2); cursor: pointer; font-size: var(--text-base); padding: 2px 5px; border-radius: var(--radius-sm); line-height: 1; }
.ti-sub-rbtn:hover { color: var(--accent); background: var(--accent-glow); }
.ti-sub-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; font-size: var(--text-sm); color: var(--text2); gap: 10px; }
.ti-sub-jump { display: inline-flex; align-items: center; gap: 6px; }
.ti-sub-jump input { width: 54px; text-align: center; }
/* HVSC STIL per-tune title, shown after "Tune N" in the picker rows. Muted so
   the tune number stays the primary anchor; ellipsises with the name. */
.ti-sub-title { color: var(--text2); font-weight: normal; }
.ti-sub-row.playing .ti-sub-title { color: var(--accent); opacity: 0.85; }
/* "Tune N" tag on a subsong entry inside a playlist/queue row */
.qr-subsong { font-size: var(--text-xs); color: var(--accent); border: 1px solid var(--accent-dim); border-radius: var(--radius-sm); padding: 0 5px; margin-left: 6px; white-space: nowrap; flex-shrink: 0; }

/* SID chip badge (6581 / 8580) in the module-details section. */
.ti-sid-chip {
  display: inline-block; font-family: "SF Mono", "Menlo", "Fira Code", monospace;
  font-size: var(--text-xs); letter-spacing: 0.04em; color: var(--accent);
  border: 1px solid var(--accent-dim); border-radius: var(--radius-sm); padding: 1px 7px;
}

/* HVSC STIL commentary panel — prose, so it wraps (unlike the ASCII-art song
   message panel, which is white-space: pre with horizontal scroll). */
.ti-stil-src { font-size: var(--text-xs); color: var(--text2); font-weight: normal; }
.ti-stil-wrap {
  border: 1px solid var(--border); border-radius: 6px;
  background: var(--bg1); padding: 8px 12px; max-height: 320px; overflow-y: auto;
}
.ti-stil-body {
  margin: 0; font-size: var(--text-sm); line-height: 1.55; color: var(--text1);
  white-space: pre-wrap; word-break: break-word; font-family: inherit;
}

/* ── Pattern grid (tracker modules) ──────────────────────────────────────── */
.ti-pat-hdrrow { display: flex; justify-content: space-between; align-items: center; }
.ti-pat-follow {
  font-size: 10px; font-weight: 600; letter-spacing: normal; text-transform: none;
  padding: 2px 10px; border-radius: 10px; cursor: pointer;
  border: 1px solid var(--border); background: transparent; color: var(--text2);
}
.ti-pat-follow[aria-pressed="true"] {
  color: var(--accent); border-color: var(--accent); background: var(--accent-glow);
}
.ti-pat-order {
  display: flex; gap: 4px; overflow-x: auto; padding: 8px 0;
  scrollbar-width: thin;
}
.ti-pat-chip {
  font-family: "SF Mono", "Menlo", "Fira Code", monospace;
  font-size: 10px; padding: 1px 6px; flex-shrink: 0; cursor: pointer;
  border: 1px solid var(--border); border-radius: 4px;
  background: transparent; color: var(--text2);
}
.ti-pat-chip.active {
  color: var(--accent); border-color: var(--accent); background: var(--accent-glow);
}
.ti-pat-chip.ti-pat-marker { opacity: 0.45; pointer-events: none; }
.ti-pat-gridwrap {
  border: 1px solid var(--border); border-radius: 6px;
  max-height: 340px; overflow: auto; background: var(--bg1);
}
.ti-pat-grid {
  border-collapse: separate; border-spacing: 0;
  font-family: "SF Mono", "Menlo", "Fira Code", monospace;
  font-size: 11px; line-height: 1.5; white-space: nowrap;
}
.ti-pat-grid th, .ti-pat-grid td { padding: 1px 7px; text-align: left; }
.ti-pat-grid th {
  position: sticky; top: 0; z-index: 2; background: var(--bg2);
  font-weight: 600; color: hsl(var(--h, 20), 60%, 55%);
  border-bottom: 1px solid var(--border);
}
.ti-pat-grid th.tprn, .ti-pat-grid td.tprn {
  position: sticky; left: 0; z-index: 1; background: var(--bg1);
  color: var(--text2); min-width: 26px;
}
.ti-pat-grid th.tprn { z-index: 3; background: var(--bg2); }
/* Per-channel VU backdrop — --h is the column hue (set once at build),
   --vc its live alpha (0..~0.16, driven from the VU tick).  Beat and
   playhead tints ride background-image so they stack with the VU color. */
.ti-pat-grid td.tpc { background-color: hsla(var(--h, 20), 70%, 55%, var(--vc, 0)); }
.ti-pat-grid tr.tpb td { background-image: linear-gradient(rgba(127,127,127,0.10), rgba(127,127,127,0.10)); }
.ti-pat-grid tr.tph td { background-image: linear-gradient(var(--accent-glow), var(--accent-glow)); }
.ti-pat-grid tr.tph td.tprn { color: var(--accent); }
/* Header VU dot — brightens with the channel's live level. */
.tpdot {
  display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  margin-left: 5px; vertical-align: 1px;
  background: hsl(var(--h, 20), 70%, 55%);
  opacity: calc(0.25 + var(--vc, 0) * 4);
}
/* Cell segments: note / instrument / volume / effect. */
.tpn { color: var(--text0); }
.tpi { color: var(--text1); }
.tpv { color: var(--text2); }
.tpf { color: var(--accent-dim); }
.tpe { color: var(--text2); opacity: 0.55; }
.ti-pat-note { font-size: 11px; color: var(--text2); margin-top: 6px; }
.ti-pat-orderrow { display: flex; align-items: center; gap: 8px; }
.ti-pat-orderrow .ti-pat-order { flex: 1; min-width: 0; }
.ti-pat-pos {
  flex-shrink: 0; font-family: "SF Mono", "Menlo", "Fira Code", monospace;
  font-size: 10px; color: var(--text2); white-space: nowrap;
}
.ti-pat-caption { font-size: 10.5px; color: var(--text2); margin-top: 6px; opacity: 0.85; }

/* Demoscene scroller — one-line horizontal marquee of the song message. */
.ti-msg-marquee {
  overflow: hidden; white-space: nowrap; border: 1px solid var(--border);
  border-radius: 6px; background: var(--bg0); padding: 7px 0; margin-bottom: 8px;
}
.ti-msg-marquee span {
  display: inline-block; padding-left: 100%;
  font-family: "SF Mono", "Menlo", "Fira Code", monospace;
  font-size: 13px; color: var(--accent);
  animation: ti-scroll 22s linear infinite;
}
@keyframes ti-scroll { from { transform: translateX(0); } to { transform: translateX(-100%); } }
@media (prefers-reduced-motion: reduce) {
  .ti-msg-marquee span { animation: none; padding-left: 0; white-space: normal; }
}

/* ── Song message (tracker modules) ──────────────────────────────────────── */
.ti-msg-wrap {
  border: 1px solid var(--border); border-radius: 6px;
  background: var(--bg1); padding: 8px 12px; overflow-x: auto;
  max-height: 300px; overflow-y: auto;
}
#ti-message {
  margin: 0; font-family: "SF Mono", "Menlo", "Fira Code", monospace;
  font-size: 11px; line-height: 1.55; color: var(--text0);
  white-space: pre; min-width: max-content;
}

/* ── Queue panel ─────────────────────────────────────────────────────────── */
#queue-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: var(--player-h);
  width: 320px;
  z-index: 600;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-left: 1px solid var(--glass-border);
  box-shadow: -8px 0 32px rgba(0,0,0,0.45);
  display: flex;
  flex-direction: column;
  animation: queue-slide-in 0.22s ease;
}
@keyframes queue-slide-in {
  from { transform: translateX(100%); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
#queue-panel.hidden { display: none; }

.queue-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 14px 16px 12px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.queue-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text0);
  flex: 1;
}
#queue-count {
  font-size: 11px;
  font-weight: 400;
  color: var(--text2);
}
#btn-queue-clear {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 6px;
  color: var(--text2);
  font-size: 11px;
  padding: 3px 9px;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
#btn-queue-clear:hover { background: rgba(255,255,255,0.13); color: var(--text0); }
#btn-queue-close {
  background: none;
  border: none;
  color: var(--text2);
  font-size: 18px;
  cursor: pointer;
  padding: 2px 6px;
  line-height: 1;
  border-radius: 5px;
  transition: color 0.12s, background 0.12s;
}
#btn-queue-close:hover { color: var(--text0); background: rgba(255,255,255,0.09); }

.queue-list[hidden] { display: none; }
.queue-list {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}
.queue-empty {
  padding: 24px 16px;
  color: var(--text2);
  font-size: 12px;
  text-align: center;
}

.queue-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px 7px 6px;
  border-bottom: 1px solid var(--border);
  /* Reserve 2px on the leading/top edges so .playing / .dragging-over
     don't shift content sideways or downward — Visual-Test #1 caught the
     2px layout jump on every track advance. */
  border-left: 2px solid transparent;
  border-top: 2px solid transparent;
  cursor: pointer;
  user-select: none;
  transition: background 0.1s;
}
.queue-row:hover { background: var(--row-hover); }
.queue-row.playing {
  background: var(--row-playing);
  border-left-color: var(--accent);
}
.queue-row.dragging-over {
  background: rgba(240,114,42,0.12);
  border-top-color: var(--accent);
}
/* Playing always wins over selected — the previously-declared order put
   .selected after .playing so a row that was both showed the blue
   selection border instead of the accent-orange now-playing marker. */
.queue-row.selected.playing {
  border-left-color: var(--accent);
}

.queue-drag-handle {
  color: var(--text2);
  font-size: 14px;
  cursor: grab;
  padding: 0 2px;
  flex-shrink: 0;
  opacity: 0.5;
  transition: opacity 0.1s;
}
.queue-row:hover .queue-drag-handle { opacity: 1; }

/* Tiny album art thumbnail in queue / playlist rows */
.queue-row-art {
  width: 30px;
  height: 30px;
  border-radius: 4px;
  overflow: hidden;
  background: var(--bg3);
  flex-shrink: 0;
  position: relative;
}
.queue-row-art img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  /* Match the library-row pattern: image stays transparent until the
     bitmap actually decodes, so the format placeholder underneath shows
     through whenever ``/api/art/{id}`` is unavailable, 404s, or hasn't
     been extracted yet.  No track in the playlist/queue ever shows a
     broken-image glyph. */
  opacity: 0;
  transition: opacity 120ms ease;
}
.queue-row-art img.loaded { opacity: 1; }
.queue-row-art .qr-art-ph {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  line-height: 1;
  color: rgba(107,200,240,0.82);
  filter: drop-shadow(0 0 4px  rgba(107,200,240,0.85))
          drop-shadow(0 0 8px  rgba(107,200,240,0.45));
  pointer-events: none;
  user-select: none;
}

.queue-playing-icon {
  color: var(--accent);
  font-size: 10px;
  flex-shrink: 0;
  width: 12px;
  text-align: center;
}

.queue-track-info {
  flex: 1;
  overflow: hidden;
  min-width: 0;
}
.queue-track-title {
  display: block;
  font-size: 12px;
  font-weight: 500;
  color: var(--text0);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.queue-track-artist {
  display: block;
  font-size: 11px;
  color: var(--text2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.queue-track-dur {
  font-size: 11px;
  color: var(--text2);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}

.queue-remove-btn {
  background: none;
  border: none;
  color: var(--text2);
  cursor: pointer;
  font-size: 14px;
  /* WCAG 2.5.5 AAA target is 44×44; we can't go that big without
     disrupting row density, but bump from 18×22 to ~30×30 so touch and
     mouse users can both hit the affordance reliably. */
  min-width: 30px;
  min-height: 30px;
  padding: 6px 8px;
  line-height: 1;
  border-radius: 4px;
  opacity: 0;
  flex-shrink: 0;
  transition: opacity 0.12s, color 0.12s;
}
.queue-row:hover .queue-remove-btn { opacity: 1; }
.queue-remove-btn:hover { color: var(--danger); }

.queue-drop-zone {
  padding: 14px 16px;
  border-top: 1px dashed var(--border);
  color: var(--text2);
  font-size: 11px;
  text-align: center;
  flex-shrink: 0;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.queue-drop-zone.drag-active {
  background: rgba(240,114,42,0.1);
  color: var(--accent);
  border-color: var(--accent);
}

/* Library row drag state */
tr.dragging { opacity: 0.4; }

/* ── Multi-select row highlight ───────────────────────────────────────────── */
#track-table tbody tr.multi-selected {
  background: rgba(107,200,240,0.13);
}
#track-table tbody tr.multi-selected td {
  color: rgba(255,255,255,0.92);
}

/* ── Selection bar ─────────────────────────────────────────────────────────── */
#selection-bar {
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  background: rgba(240,114,42,0.10);
  border-bottom: 1px solid rgba(240,114,42,0.18);
  flex-shrink: 0;
  animation: sel-bar-in 0.15s ease;
}
#selection-bar:not([hidden]) {
  display: flex;
}
@keyframes sel-bar-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
#sel-count {
  font-size: 12px;
  font-weight: 600;
  color: var(--accent);
  flex: 1;
}
#selection-bar button {
  background: rgba(255,255,255,0.08);
  border: 1px solid var(--glass-border);
  border-radius: 6px;
  color: var(--text1);
  font-size: 11px;
  padding: 4px 10px;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
  white-space: nowrap;
}
#selection-bar button:hover { background: rgba(255,255,255,0.15); color: var(--text0); }

/* ── Browse toolbar (grid toggle) ──────────────────────────────────────────── */
#browse-toolbar {
  display: flex;
  justify-content: flex-end;
  padding: 4px 10px 0;
  flex-shrink: 0;
  min-height: 0;
}
#browse-toolbar:empty,
#browse-toolbar:has(> :only-child[hidden]) {
  display: none;
}
.btn-grid-toggle {
  background: rgba(255,255,255,0.07);
  border: 1px solid var(--glass-border);
  border-radius: 6px;
  color: var(--text2);
  font-size: 14px;
  padding: 3px 8px;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
.btn-grid-toggle:hover { background: rgba(255,255,255,0.13); color: var(--text0); }
.btn-grid-toggle.active { color: var(--accent); border-color: rgba(240,114,42,0.3); }

/* ── Album grid ──────────────────────────────────────────────────────────────── */
#album-grid[hidden] { display: none; }
#album-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 16px;
  padding: 16px;
  overflow-y: auto;
  flex: 1;
}

.album-card {
  cursor: pointer;
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg3);
  border: 1px solid var(--glass-border);
  transition: transform 0.12s, box-shadow 0.12s, border-color 0.12s;
}
.album-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 20px rgba(0,0,0,0.4);
  border-color: rgba(255,255,255,0.15);
}

.album-card-art {
  width: 100%;
  aspect-ratio: 1;
  background: var(--bg2);
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
.album-card-initials {
  font-size: 28px;
  font-weight: 700;
  /* Match the placeholder language used by the bottom player and row
     covers (glowing-blue) so a user scanning the album grid recognises
     the "no art yet" state with the same visual cue.  Initials still
     communicate "which album" at a glance — emoji would lose that. */
  color: var(--art-glow);
  filter: drop-shadow(0 0 6px var(--art-glow-soft))
          drop-shadow(0 0 14px var(--art-glow-faint));
  letter-spacing: 2px;
  user-select: none;
}

.album-card-art-loaded .album-card-art {
  background-size: cover;
  background-position: center;
}

.album-card-info {
  padding: 8px 10px 10px;
}
.album-card-title {
  font-size: 12px;
  font-weight: 600;
  color: var(--text0);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.album-card-sub {
  font-size: 11px;
  color: var(--text2);
  margin-top: 2px;
}

/* ── Waveform canvas ─────────────────────────────────────────────────────────── */
#waveform-canvas {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  z-index: 0;
  border-radius: 2px;
  /* Sized and positioned by JS to match the seek-bar element */
}

/* The seek bar sits above the waveform */
.player-progress {
  position: relative;
}
#seek-bar {
  position: relative;
  z-index: 1;
}
/* When waveform is active, make seek bar track transparent so waveform shows */
.player-progress.has-waveform #seek-bar {
  background: transparent !important;
}

/* ── Radio mode ──────────────────────────────────────────────────────────────
   A live stream has no seekable position and no next/prev *track*, so the
   player bar (a) replaces the seek row with a "● LIVE" badge + now-playing
   ticker, and (b) repurposes ◄◄ / ►► to surf the current station list, with
   the target station name shown inline beside each button.  Everything here
   is gated on #player-bar.radio-mode, set by the renderer when a station is
   the now-playing item — the normal track layout is untouched otherwise. */

/* Hide the seek widgets; the waveform canvas stays behind the ticker. */
#player-bar.radio-mode .player-progress > #time-cur,
#player-bar.radio-mode .player-progress > #time-dur,
#player-bar.radio-mode .player-progress > #seek-bar { visibility: hidden; }

.radio-nowplaying { display: none; }
#player-bar.radio-mode .radio-nowplaying {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;  /* centre the LIVE + ticker group under the transport */
  gap: 10px;
  z-index: 2;            /* above the waveform canvas + hidden seek bar */
  pointer-events: none;  /* nothing here is clickable — let clicks fall through */
}
.radio-live {
  flex-shrink: 0;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: #ff5252;
  text-transform: uppercase;
  animation: radio-live-pulse 2s ease-in-out infinite;
}
@keyframes radio-live-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }

/* flex:0 1 auto → the wrapper sizes to the ticker so a short now-playing line
   centres; a long one shrinks to the available width and marquees (clipped). */
.radio-ticker-wrap { flex: 0 1 auto; min-width: 0; overflow: hidden; }
.radio-ticker {
  display: inline-block;
  white-space: nowrap;
  font-size: 12px;
  color: var(--text1);
  text-shadow: 0 1px 3px rgba(0,0,0,0.55);   /* readable over the waveform */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* JS adds .scroll (and sets --ticker-shift / --ticker-dur) only when the text
   actually overflows its wrapper — a measured marquee, no magic numbers. */
.radio-ticker.scroll {
  max-width: none;
  text-overflow: clip;
  animation: radio-ticker-scroll var(--ticker-dur, 12s) linear infinite;
}
@keyframes radio-ticker-scroll {
  0%, 12%   { transform: translateX(0); }
  88%, 100% { transform: translateX(var(--ticker-shift, 0)); }
}

/* Surf-target station names, inline beside ◄◄ / ►►.  Both sides are a FIXED
   equal width (flex:0 0) so unequal station-name lengths can't shift the play
   button off-centre.  An empty target (a list end) keeps its width as blank
   space — invisible (no box) but symmetric — so the cluster stays centred. */
.radio-target { display: none; }
#player-bar.radio-mode .radio-target {
  display: inline-block;
  /* Hard-clamp to exactly 120px.  flex:0 0 120px alone is honoured by Chromium
     but NOT by WebKit/Gecko during the pill's intrinsic-size pass — there the
     LAST target collapsed to ~16px (flex-shrink:0 ignored), making the pill
     left-heavy and pushing the play button ~53px off-centre.  min/max-width are
     fundamental box constraints every engine obeys, so both flanks stay 120px
     and the transport is centred cross-browser. */
  flex: 0 0 120px;
  min-width: 120px;
  max-width: 120px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 10px;
  line-height: 1.15;
  color: var(--text2);
  opacity: 0.85;
}
#player-bar.radio-mode .radio-target-prev { text-align: right; }
#player-bar.radio-mode .radio-target-next { text-align: left; }

/* ── Playlist panel ──────────────────────────────────────────────────────────── */
#playlist-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: var(--player-h);
  width: 320px;
  z-index: 600;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-left: 1px solid var(--glass-border);
  box-shadow: -8px 0 32px rgba(0,0,0,0.45);
  display: flex;
  flex-direction: column;
  animation: queue-slide-in 0.22s ease;
}
#playlist-panel.hidden { display: none; }

.playlist-header[hidden] { display: none; }
.playlist-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 14px 16px 12px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.playlist-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text0);
  flex: 1;
}
#btn-playlist-new {
  background: rgba(240,114,42,0.12);
  border: 1px solid rgba(240,114,42,0.22);
  border-radius: 6px;
  color: var(--accent);
  font-size: 11px;
  padding: 3px 9px;
  cursor: pointer;
  transition: background 0.12s;
}
#btn-playlist-new:hover { background: rgba(240,114,42,0.22); }
#btn-playlist-close {
  background: none;
  border: none;
  color: var(--text2);
  font-size: 18px;
  cursor: pointer;
  padding: 2px 6px;
  line-height: 1;
  border-radius: 5px;
  transition: color 0.12s, background 0.12s;
}
#btn-playlist-close:hover { color: var(--text0); background: rgba(255,255,255,0.09); }

/* Shared close-btn style for dynamically created btn-playlist-close2 */
.playlist-close-btn {
  background: none;
  border: none;
  color: var(--text2);
  font-size: 18px;
  cursor: pointer;
  padding: 2px 6px;
  line-height: 1;
  border-radius: 5px;
  transition: color 0.12s, background 0.12s;
}
.playlist-close-btn:hover { color: var(--text0); background: rgba(255,255,255,0.09); }

/* Back button in playlist track view */
.pl-back-btn {
  background: none;
  border: none;
  color: var(--accent);
  cursor: pointer;
  font-size: 15px;
  padding: 2px 6px;
  border-radius: 4px;
  line-height: 1;
  flex-shrink: 0;
  transition: background 0.12s;
}
.pl-back-btn:hover { background: rgba(240,114,42,0.12); }
.pl-track-count {
  font-size: 10px;
  color: var(--text2);
  flex-shrink: 0;
}

/* Open arrow button on playlist list rows */
.pl-open-btn {
  background: none;
  border: none;
  color: var(--text2);
  cursor: pointer;
  font-size: 11px;
  padding: 2px 6px;
  border-radius: 4px;
  opacity: 0;
  flex-shrink: 0;
  transition: opacity 0.12s, color 0.12s;
}
.playlist-row:hover .pl-open-btn { opacity: 1; }
.pl-open-btn:hover { color: var(--accent); }

/* Disabled drop zone */
.playlist-drop-zone.drop-zone-disabled {
  opacity: 0.4;
  pointer-events: none;
}

/* "Create new playlist" item in add-dropdown */
.playlist-dd-create {
  border-top: 1px solid var(--border);
  color: var(--accent) !important;
  font-style: italic;
}

.playlist-list[hidden] { display: none; }
.playlist-list {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}
.playlist-empty {
  padding: 24px 16px;
  color: var(--text2);
  font-size: 12px;
  text-align: center;
}

.playlist-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  user-select: none;
  transition: background 0.1s;
}
.playlist-row:hover { background: var(--row-hover); }

.playlist-row-info {
  flex: 1;
  overflow: hidden;
  min-width: 0;
}
.playlist-row-name {
  display: block;
  font-size: 12px;
  font-weight: 500;
  color: var(--text0);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.playlist-row-count {
  display: block;
  font-size: 11px;
  color: var(--text2);
}

.playlist-delete-btn {
  background: none;
  border: none;
  color: var(--text2);
  cursor: pointer;
  font-size: 14px;
  padding: 2px 4px;
  line-height: 1;
  border-radius: 4px;
  opacity: 0;
  flex-shrink: 0;
  transition: opacity 0.12s, color 0.12s;
}
.playlist-row:hover .playlist-delete-btn { opacity: 1; }
.playlist-delete-btn:hover { color: #e55; }

.playlist-drop-zone {
  padding: 14px 16px;
  border-top: 1px dashed var(--border);
  color: var(--text2);
  font-size: 11px;
  text-align: center;
  flex-shrink: 0;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.playlist-drop-zone.drag-active {
  background: rgba(240,114,42,0.1);
  color: var(--accent);
  border-color: var(--accent);
}

/* ── Playlist add dropdown ────────────────────────────────────────────────── */
.playlist-add-dropdown {
  background: var(--bg3);
  border: 1px solid var(--border-bright);
  border-radius: 8px;
  padding: 4px 0;
  min-width: 180px;
  max-height: 240px;
  overflow-y: auto;
  box-shadow: var(--glass-shadow);
  animation: dialog-in 0.12s ease;
}
.playlist-dd-item {
  padding: 8px 14px;
  font-size: 12px;
  color: var(--text1);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: background 0.08s, color 0.08s;
}
.playlist-dd-item:hover { background: rgba(255,255,255,0.08); color: var(--text0); }
.playlist-dd-empty {
  padding: 12px 14px;
  font-size: 11px;
  color: var(--text2);
  text-align: center;
}

/* ── Playlist multi-select ───────────────────────────────────────────────── */

/* Selected row: subtle blue tint, left accent line */
.queue-row.selected {
  background: rgba(107,200,240,0.10);
  border-left: 2px solid rgba(107,200,240,0.55);
}
.queue-row.selected:hover {
  background: rgba(107,200,240,0.16);
}
/* Dragged rows: ghost while in flight */
.queue-row.dragging {
  opacity: 0.35;
}

/* ── Title row inside playlist tracks (flex to host badge) ───────────────── */
#playlist-tracks .queue-track-titlerow {
  display: flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
}
#playlist-tracks .queue-track-title {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Duplicate badges ────────────────────────────────────────────────────── */
.dup-badge {
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 1px 5px;
  border-radius: 10px;
  line-height: 16px;
  cursor: default;
  white-space: nowrap;
}
/* "2×" — same track ID added twice: amber warning */
.dup-id-badge {
  background: rgba(240,180,42,0.18);
  color: #f0b42a;
  border: 1px solid rgba(240,180,42,0.30);
}
/* Row tint for ID-duplicate rows */
.queue-row.dup-id {
  border-left: 2px solid rgba(240,180,42,0.5);
}
/* "⧉" — same song, different path: muted blue info */
.dup-loc-badge {
  background: rgba(107,200,240,0.12);
  color: rgba(107,200,240,0.7);
  border: 1px solid rgba(107,200,240,0.20);
  font-size: 11px;
  padding: 0px 4px;
}

/* ── Selection bar ───────────────────────────────────────────────────────── */
#pl-sel-bar[hidden] { display: none; }
#pl-sel-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  background: var(--bg3);
  border-top: 1px solid var(--border-bright);
  flex-shrink: 0;
  animation: pl-selbar-in 0.12s ease;
}
@keyframes pl-selbar-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
#pl-sel-count {
  flex: 1;
  font-size: 11px;
  color: var(--text1);
}
#pl-sel-remove {
  font-size: 11px;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: 5px;
  border: 1px solid rgba(220,70,70,0.35);
  background: rgba(220,70,70,0.12);
  color: #e06060;
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
}
#pl-sel-remove:hover {
  background: rgba(220,70,70,0.25);
  color: #ff8080;
}
#pl-sel-deselect {
  font-size: 11px;
  padding: 4px 8px;
  border-radius: 5px;
  border: 1px solid var(--border-bright);
  background: transparent;
  color: var(--text2);
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
}
#pl-sel-deselect:hover {
  background: var(--bg4);
  color: var(--text1);
}

/* ── Playlist name modal overlay ─────────────────────────────────────────── */
.pl-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 10000;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  animation: pl-modal-fade-in 0.1s ease;
}
.pl-modal-backdrop.pl-modal-out {
  animation: pl-modal-fade-out 0.12s ease forwards;
}
@keyframes pl-modal-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes pl-modal-fade-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}
.pl-modal-dialog {
  background: var(--bg2);
  border: 1px solid var(--border-bright);
  border-radius: 12px;
  padding: 22px 22px 18px;
  width: 300px;
  box-shadow: 0 16px 48px rgba(0,0,0,0.7);
}
.pl-modal-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text0);
  margin-bottom: 12px;
}
.pl-modal-input {
  width: 100%;
  background: var(--bg3);
  border: 1px solid var(--border-bright);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 13px;
  color: var(--text0);
  outline: none;
  transition: border-color 0.15s;
}
.pl-modal-input:focus {
  border-color: rgba(107,200,240,0.5);
  box-shadow: 0 0 0 2px rgba(107,200,240,0.12);
}
.pl-modal-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  margin-top: 14px;
}
.pl-modal-btn {
  padding: 6px 16px;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  border: 1px solid var(--border-bright);
  transition: background 0.1s, color 0.1s;
}
.pl-modal-cancel {
  background: transparent;
  color: var(--text1);
}
.pl-modal-cancel:hover { background: var(--bg3); color: var(--text0); }
.pl-modal-ok {
  background: var(--accent);
  color: #fff;
  border-color: transparent;
}
.pl-modal-ok:hover { background: #e05e18; }

/* ── VU Meters (tracker/module per-channel bars in player bar) ────────────── */
.vu-meters {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 100%;
  display: flex;
  align-items: flex-end;
  gap: 2px;
  padding: 0 8px;
  pointer-events: none;
  z-index: 0;
  /* Was 0.3 — barely visible even on dark.  0.65 still keeps the meters
     subordinate to the player controls in the foreground but actually
     readable. */
  opacity: 0.65;
}
.vu-meters[hidden] { display: none; }

/* When the track-info modal is open, the same ``#vu-meters`` element
   is reparented onto the 260×260 cover-art box (``#ti-art``).  The
   base rule's ``position: absolute; inset: 0`` already fills the
   parent; here we only bump opacity for legibility over cover art
   and raise z-index so the bars sit above the art image but below
   the loading shimmer overlay. */
.vu-meters.in-modal {
  opacity: 0.75;
  z-index: 5;
  /* Slight inset so the bars don't touch the cover-art edge — the
     cover art has a 12px border-radius which would clip the
     bar bottoms otherwise. */
  padding: 0 10px 8px;
}

/* ``.vu-col`` is the new per-channel container — wraps the bar with
   its label and pan indicator stack.  Each column grows to share the
   container width equally; vertically, the column reserves a fixed
   strip at the bottom for the label + pan glyph, and the bar fills
   the remaining height. */
.vu-col {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-end;
  min-width: 0;
  /* Column itself stretches to the container's full height so the
     percentage in the .vu-bar height calc resolves against a real
     pixel value.  Without this, ``height: calc(var(--vu-level) * 100%)``
     was anchored to an undefined parent height and the bar collapsed
     to its ``min-height`` floor (6px). */
  height: 100%;
}

.vu-bar {
  /* Do NOT flex-grow — the explicit ``height`` below drives the bar
     size based on the per-channel VU level.  ``flex: 1`` would
     override the height and stretch the bar to fill all remaining
     space in the column, making every bar identical-height
     regardless of input level (the bug the user observed in
     v1.3.0 first cut: 30 channels at 0 px of motion despite valid
     --vu-level values in the 0.0–0.77 range). */
  flex: 0 0 auto;
  background: linear-gradient(
    to top,
    hsla(var(--vu-hue, 30), 80%, 55%, 0.85),
    hsla(var(--vu-hue, 30), 80%, 70%, 0.35)
  );
  /* ``--vu-level`` is the 0..1 quantised level set per frame by
     _drawVU (VUMR mode) or the FFT spectrum slicer (fallback mode).
     Percentage anchors to ``.vu-col``'s explicit height above. */
  height: calc(var(--vu-level, 0) * 100%);
  /* A 6 px floor so the bars are visible at idle / silent passages
     too — gives the user feedback that the meter is alive, not just
     dead pixels. */
  min-height: 6px;
  border-radius: 2px 2px 0 0;
  /* No transition — bars snap to each frame's value with no smoothing.
     The 66 ms draw cadence is the only motion-step floor; releases
     read as instant from the user's POV. */
  transition: none;
}

.vu-col-label {
  font-size: 9px;
  text-align: center;
  color: var(--text2, #aaa);
  line-height: 1.1;
  margin-top: 1px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: clip;
}
.vu-meters[data-preset="roomy"] .vu-col-label { font-size: 10px; }

.vu-col-pan {
  font-size: 8px;
  text-align: center;
  color: var(--text2, #888);
  letter-spacing: 1px;
  line-height: 1;
  opacity: 0.6;
}
.vu-meters[data-preset="roomy"] .vu-col-pan { font-size: 10px; }

/* The ``mini`` preset hides labels entirely — _initVU never adds the
   label/pan divs in that mode, but if the preset switches at runtime
   the rule below provides a defensive fallback. */
.vu-meters[data-preset="mini"] .vu-col-label,
.vu-meters[data-preset="mini"] .vu-col-pan { display: none; }

/* The ``numeric`` preset is for >8 channel layouts (16-ch FastTracker /
   ScreamTracker in the modal box).  Number labels stay visible but
   pan circles are dropped — at the column widths this tier hits the
   ●○○/○●○/○○● glyph triplet becomes unreadable and just adds noise.
   _initVU stops emitting the .vu-col-pan node in this mode; the rule
   below is a defensive fallback if a stale preset is rendered. */
.vu-meters[data-preset="numeric"] .vu-col-pan { display: none; }
.vu-meters[data-preset="numeric"] .vu-col-label { font-size: 9px; }

/* FFT fallback mode — the bars are a 32-bin spectrum analyzer, NOT
   per-channel meters.  Per-voice labels (CH:N) and pan circles are
   meaningless here and would actively mislead, so hide them. */
.vu-meters[data-source="fft"] .vu-col-label,
.vu-meters[data-source="fft"] .vu-col-pan { display: none; }

/* Fallback animation when AudioContext is not available */
.vu-bar.vu-animated {
  animation: vu-pulse 0.5s ease-in-out infinite alternate;
  animation-delay: calc(var(--vu-hue) * 2ms);
}
@keyframes vu-pulse {
  from { height: 10%; }
  to   { height: 60%; }
}

/* ── VU "circuit board" skin (opt-in, tracker/VUMR only) ──────────────────
   Retro PCB rendering of the SAME per-channel ``--vu-level`` data as the
   bars (no new data path — app.js ``_readVuStyle`` just flips
   ``data-style="circuit"``).  Each column is a copper trace rising to a
   round solder pad that glows green with the channel's amplitude.  The
   glow/blur are driven by ``--vu-level`` via ``calc()`` so the existing
   15 Hz ``--vu-level`` writes animate it with zero extra JS.  Custom
   props inherit DOWN, so the trace must hang off ``.vu-bar::before``
   (which carries ``--vu-level``), not ``.vu-col``. */
.vu-meters[data-style="circuit"] {
  background:
    repeating-linear-gradient(0deg, rgba(63,224,137,0.045) 0 1px, transparent 1px 13px),
    radial-gradient(130% 100% at 50% 100%, #0a2417, #06120b);
  border-radius: 8px;
  padding: 8px 4px 4px;
}
.vu-meters[data-style="circuit"] .vu-col { justify-content: flex-end; }
.vu-meters[data-style="circuit"] .vu-bar {
  position: relative;
  height: 11px !important;
  width: 11px;
  align-self: center;
  margin-top: auto;            /* pad sits at the bottom of the column */
  border-radius: 50%;
  background: radial-gradient(circle,
    hsla(145, 90%, 66%, calc(0.40 + var(--vu-level, 0) * 0.60)),
    hsla(145, 70%, 28%, 0.55));
  box-shadow:
    0 0 calc(2px + var(--vu-level, 0) * 13px) hsla(145, 90%, 60%, calc(0.30 + var(--vu-level, 0) * 0.70)),
    inset 0 0 2px rgba(0, 0, 0, 0.55);
  border: 1px solid hsla(145, 50%, 42%, 0.6);
  transition: box-shadow 60ms linear, background 60ms linear;
}
/* The dim copper "track" — the full-height PCB trace the signal rides up. */
.vu-meters[data-style="circuit"] .vu-bar::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;                /* trace runs UP from the pad */
  transform: translateX(-50%);
  width: 2px;
  height: 42px;
  background: linear-gradient(to top,
    hsla(145, 60%, 40%, 0.32),
    hsla(145, 60%, 30%, 0.08));
}
/* The bright "signal" — fills the trace up to the current level so amplitude
   reads as HEIGHT (a position cue), not glow alone.  Quiet channels are then
   legible at a glance instead of just dim. */
.vu-meters[data-style="circuit"] .vu-bar::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 100%;
  transform: translateX(-50%);
  width: 2px;
  height: calc(var(--vu-level, 0) * 42px);
  background: linear-gradient(to top,
    hsla(145, 88%, 62%, 0.95),
    hsla(145, 82%, 50%, 0.45));
  box-shadow: 0 0 calc(2px + var(--vu-level, 0) * 6px)
              hsla(145, 88%, 58%, calc(0.40 + var(--vu-level, 0) * 0.60));
  transition: height 60ms linear, box-shadow 60ms linear;
}
.vu-meters[data-style="circuit"] .vu-col-label {
  color: hsla(145, 45%, 62%, 0.78);
  font-family: ui-monospace, monospace;
}
/* Reduced-motion: freeze pad glow + trace fill (no easing). */
@media (prefers-reduced-motion: reduce) {
  .vu-meters[data-style="circuit"] .vu-bar,
  .vu-meters[data-style="circuit"] .vu-bar::before,
  .vu-meters[data-style="circuit"] .vu-bar::after { transition: none; }
}

/* ── Embedded visualization shared chrome ─────────────────────────────────── */
.viz-host {
  position: relative;
  border-radius: 10px;
  background:
    radial-gradient(120% 120% at 50% 0%, rgba(255,255,255,0.02), transparent 70%),
    #070a0f;
  border: 1px solid #1b2433;
  overflow: hidden;
}
.viz-host svg, .viz-host canvas { display: block; width: 100%; height: 100%; }
/* SR-only mirror so canvas/SVG viz expose meaning to assistive tech. */
/* Visually-hidden, screen-reader-only.  ``.sr-only`` is the conventional name
   used in the markup (e.g. the skeleton's "Loading tracks…" label); it was
   never defined, so that text painted on screen.  Share one rule with the
   visualizer's ``.viz-sr-only``. */
.sr-only,
.viz-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* Signal chain (Track Info modal) */
.viz-signal-chain { width: 100%; height: 96px; }
.viz-signal-chain .viz-node { fill: var(--bg3); stroke: var(--line, #243043); }
.viz-signal-chain .viz-node.active {
  stroke: var(--viz-req);
  filter: drop-shadow(0 0 7px var(--viz-req));
}
.viz-signal-chain .viz-node-label { fill: var(--text, #e6edf3); font: 600 10px sans-serif; }
:root[data-theme="light"] .viz-host { background: #eef1f5; border-color: #d4d9e0; }
/* The four canvas visualizations paint a dark scene (dark-on-dark by
   design); on a light host they'd read as a black slab.  Pin their hosts to
   a dark "instrument screen" in BOTH themes so the panel frames the canvas
   deliberately instead of clashing.  ID specificity (1,0,0) outranks the
   light .viz-host rule above.  The SVG signal-chain host is theme-adaptive
   and is intentionally NOT included here. */
#viz-scan-flow, #viz-cache-cascade, #viz-ftp-lanes, #galaxy-view {
  background:
    radial-gradient(120% 120% at 50% 0%, rgba(255,255,255,0.02), transparent 70%),
    #070a0f;
  border-color: #1b2433;
}
:root[data-theme="light"] .viz-signal-chain .viz-node { fill: #fff; stroke: #c4ccd6; }
:root[data-theme="light"] .viz-signal-chain .viz-node-label { fill: #1a1d22; }

/* Library Galaxy view (#6) — fills the content pane; legend chips float
   over the canvas at the bottom and double as format filters. */
#galaxy-view {
  position: relative;
  width: 100%;
  height: 100%;
  min-height: 420px;
  border: none;
  border-radius: 0;
  background: #070a0f;
}
#galaxy-view.galaxy-flex { display: flex; flex-direction: column; }
/* body holds the three co-located surfaces (canvas / legend / list), filling
   the column below the toolbar — no magic pixel offset to keep in sync. */
.galaxy-body { position: relative; flex: 1 1 auto; min-height: 0; }
#galaxy-view canvas { position: absolute; inset: 0; }
.galaxy-legend {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex; gap: 8px; flex-wrap: wrap;
  padding: 12px 16px;
  max-height: 33%;
  overflow-y: auto;
  background: linear-gradient(0deg, rgba(7,10,15,0.92), rgba(7,10,15,0.6) 60%, transparent);
}
.galaxy-empty { color: var(--text2, #93a1b3); font: 500 13px/1.4 system-ui, sans-serif; }
.galaxy-chip {
  font: 600 12px/1 system-ui, sans-serif;
  color: hsl(var(--gx-hue, 200), 70%, 78%);
  background: hsla(var(--gx-hue, 200), 60%, 22%, 0.55);
  border: 1px solid hsla(var(--gx-hue, 200), 60%, 50%, 0.5);
  border-radius: 999px;
  padding: 5px 12px;
  cursor: pointer;
  transition: transform .12s, box-shadow .12s;
}
.galaxy-chip:hover, .galaxy-chip:focus-visible {
  transform: translateY(-1px);
  box-shadow: 0 0 14px hsla(var(--gx-hue, 200), 70%, 55%, 0.5);
  outline: none;
}

/* ── Galaxy / List toggle + sortable format list ───────────────────────────
   The galaxy is a proportional-area chart of formats; the List is its
   sortable, searchable, accessible equivalent.  Both sit inside #galaxy-view
   (always a dark scene), switched by the .mode-galaxy / .mode-list classes. */
.galaxy-toolbar {
  flex: 0 0 auto;
  z-index: 3;
  display: flex; align-items: center; gap: 12px;
  padding: 8px 14px;
  background: rgba(7,10,15,0.92);
  border-bottom: 1px solid #141c28;
}
.galaxy-mode-toggle {
  display: inline-flex;
  border: 1px solid #2a3546;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(10,14,20,0.7);
}
.galaxy-mode-btn {
  font: 600 12px/1 system-ui, sans-serif;
  color: #93a1b3;
  background: transparent;
  border: none;
  padding: 6px 15px;
  cursor: pointer;
  transition: background .12s, color .12s;
}
.galaxy-mode-btn + .galaxy-mode-btn { border-left: 1px solid #2a3546; }
.galaxy-mode-btn:hover:not(.is-disabled) { color: #e6edf5; }
.galaxy-mode-btn.active { color: #071019; background: #6fd3ff; }
/* Galaxy unavailable: aria-disabled (stays focusable + describable) rather than
   the `disabled` attribute, which drops it from the tab order and hides its reason. */
.galaxy-mode-btn.is-disabled { opacity: 0.4; cursor: not-allowed; }
.galaxy-mode-btn:focus-visible { outline: 2px solid #6fd3ff; outline-offset: -2px; }
.galaxy-search {
  flex: 0 1 240px;
  font: 500 13px/1 system-ui, sans-serif;
  color: #e6edf5;
  background: rgba(10,14,20,0.7);
  border: 1px solid #2a3546;
  border-radius: 8px;
  padding: 6px 10px;
}
.galaxy-search::placeholder { color: #6c7a8c; }
.galaxy-search:focus-visible { outline: none; border-color: #6fd3ff; }
.galaxy-count { margin-left: auto; font: 500 12px/1 system-ui, sans-serif; color: #7f8da0; }
.galaxy-reason {
  flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 500 12px/1.3 system-ui, sans-serif; color: #c9a94a;
}

/* ── Family filter (second toolbar row; filters both surfaces) ─────────────── */
.galaxy-family-bar {
  flex: 0 0 auto;
  z-index: 3;
  display: flex; align-items: center; gap: 8px;
  padding: 8px 14px;
  overflow-x: auto; scrollbar-width: thin;
  background: rgba(7,10,15,0.85);
  border-bottom: 1px solid #141c28;
}
.galaxy-fam-chip {
  flex: 0 0 auto;
  display: inline-flex; align-items: baseline; gap: 6px;
  font: 600 12px/1 system-ui, sans-serif;
  color: hsl(var(--gx-hue, 210), 30%, 72%);
  background: hsla(var(--gx-hue, 210), 45%, 22%, 0.35);
  border: 1px solid hsla(var(--gx-hue, 210), 45%, 50%, 0.4);
  border-radius: 999px;
  padding: 5px 12px;
  cursor: pointer;
  white-space: nowrap;
  transition: background .12s, color .12s, border-color .12s;
}
/* the "All" chip has no hue — neutral steel */
.galaxy-fam-chip[data-family="all"] {
  color: #aeb9c8;
  background: rgba(120,140,165,0.14);
  border-color: #2a3546;
}
.galaxy-fam-chip:hover { color: #e6edf5; }
.galaxy-fam-chip.active {
  color: #071019;
  background: hsl(var(--gx-hue, 210), 65%, 62%);
  border-color: hsl(var(--gx-hue, 210), 65%, 62%);
}
.galaxy-fam-chip[data-family="all"].active {
  color: #071019; background: #9fb0c4; border-color: #9fb0c4;
}
.galaxy-fam-chip:focus-visible { outline: 2px solid #6fd3ff; outline-offset: 2px; }
.galaxy-fam-n { font-weight: 500; font-variant-numeric: tabular-nums; opacity: 0.85; font-size: 11px; }

/* mode visibility */
#galaxy-view.mode-list canvas,
#galaxy-view.mode-list .galaxy-legend { display: none; }
#galaxy-view.mode-galaxy .galaxy-list { display: none; }

/* the list surface fills below the toolbar and scrolls */
.galaxy-list {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  padding: 6px 14px 18px;
}
.galaxy-table {
  width: 100%;
  border-collapse: collapse;
  font: 500 13px/1.3 system-ui, sans-serif;
  color: #cdd6e2;
}
.galaxy-table thead th {
  position: sticky; top: 0; z-index: 1;
  text-align: left;
  padding: 0;
  background: #0b0f16;
  border-bottom: 1px solid #1f2937;
  font-size: 12px; color: #8b98ab;
}
.galaxy-table thead th:nth-child(2) { text-align: right; }
.galaxy-th-btn {
  width: 100%;
  display: flex; align-items: center; gap: 4px;
  font: 600 12px/1 system-ui, sans-serif;
  color: #8b98ab;
  background: transparent; border: none;
  padding: 10px 10px;
  cursor: pointer;
}
.galaxy-table thead th:nth-child(2) .galaxy-th-btn { justify-content: flex-end; }
.galaxy-th-btn:hover { color: #e6edf5; }
.galaxy-th-btn:focus-visible { outline: 2px solid #6fd3ff; outline-offset: -2px; }
.galaxy-sort-caret { font-size: 9px; color: #6fd3ff; }
.galaxy-table tbody tr { border-bottom: 1px solid rgba(31,41,55,0.6); }
.galaxy-table tbody tr:hover { background: rgba(111,211,255,0.06); }
.galaxy-table tbody tr[aria-current="true"] { background: rgba(111,211,255,0.12); }
.galaxy-table td { padding: 0; }
/* Format cell is a row-header <th scope=row> so Tracks/Share announce with it. */
.galaxy-table tbody th { padding: 0; font-weight: 400; text-align: left; }
.galaxy-row-btn {
  width: 100%;
  display: flex; align-items: center; gap: 9px;
  font: 600 13px/1 system-ui, sans-serif;
  color: hsl(var(--gx-hue, 200), 65%, 80%);
  background: transparent; border: none;
  padding: 9px 10px;
  cursor: pointer;
  text-align: left;
}
.galaxy-row-btn:hover { color: #fff; }
.galaxy-row-btn:focus-visible { outline: 2px solid #6fd3ff; outline-offset: -2px; }
.galaxy-dot {
  flex: 0 0 auto;
  width: 10px; height: 10px; border-radius: 50%;
  background: hsl(var(--gx-hue, 200), 75%, 60%);
  box-shadow: 0 0 8px hsla(var(--gx-hue, 200), 80%, 55%, 0.6);
}
.galaxy-num { text-align: right; padding-right: 10px !important; color: #e6edf5; font-variant-numeric: tabular-nums; white-space: nowrap; }
.galaxy-share { padding-right: 10px !important; min-width: 120px; }
.galaxy-share .galaxy-bar {
  display: inline-block; vertical-align: middle;
  width: var(--w, 0%); max-width: calc(100% - 46px); min-width: 3px; height: 8px;
  border-radius: 4px;
  /* fixed mid-high lightness + inset light border keeps every hue (incl. deep
     blues ~220-255) ≥3:1 against the #070a0f scene — WCAG 1.4.11 non-text contrast. */
  background: hsl(var(--gx-hue, 200), 72%, 62%);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.30);
  margin-right: 8px;
}
.galaxy-share .galaxy-pct { color: #8b98ab; font-variant-numeric: tabular-nums; font-size: 12px; }
.galaxy-retry {
  margin-left: 6px;
  font: 600 12px/1 system-ui, sans-serif; color: #6fd3ff;
  background: transparent; border: 1px solid #2a3546; border-radius: 6px;
  padding: 4px 10px; cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
  .galaxy-mode-btn, .galaxy-chip { transition: none; }
}

/* ── Keyboard shortcuts overlay ────────────────────────────────────────── */
/* ── Welcome / onboarding overlay ───────────────────────────────────────────
   First-run guide (and re-openable via the header "?").  Mirrors the
   shortcuts / links overlay shell. */
#welcome-overlay {
  position: fixed;
  inset: 0;
  z-index: 20000;            /* same layer as shortcuts/links; above admin */
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
#welcome-overlay.hidden { display: none; }
#welcome-panel {
  position: relative;
  background: var(--bg2);
  border: 1px solid var(--border-bright);
  border-radius: 16px;
  box-shadow: var(--glass-shadow);
  max-width: 460px;
  width: 100%;
  max-height: 86vh;
  overflow-y: auto;
  padding: 28px 26px 22px;
}
.welcome-close {
  position: absolute;
  top: 12px;
  right: 14px;
  background: none;
  border: none;
  color: var(--text2);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 4px;
}
.welcome-close:hover { color: var(--text0); background: rgba(255,255,255,0.08); }
.welcome-hero { text-align: center; margin-bottom: 18px; }
.welcome-logo { font-size: 40px; line-height: 1; display: block; margin-bottom: 6px; }
.welcome-hero h2 { margin: 0 0 6px; font-size: 20px; font-weight: 700; color: var(--text0); }
.welcome-sub { margin: 0; font-size: 13px; color: var(--text2); line-height: 1.5; }
.welcome-features {
  list-style: none;
  margin: 0 0 22px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.welcome-features li { display: flex; gap: 12px; align-items: flex-start; }
.welcome-features .wf-icon { font-size: 20px; line-height: 1.3; flex: 0 0 26px; text-align: center; }
.welcome-features div { font-size: 13px; color: var(--text1); line-height: 1.45; }
.welcome-features strong { color: var(--text0); font-weight: 600; }
.welcome-features code {
  font-family: var(--mono, ui-monospace, SFMono-Regular, monospace);
  font-size: 11.5px;
  background: var(--bg3);
  color: var(--text1);
  padding: 1px 5px;
  border-radius: 4px;
}
.welcome-beta {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--accent);
  border: 1px solid var(--accent-dim);
  border-radius: 999px;
  padding: 0 6px;
  margin-left: 4px;
  vertical-align: middle;
}
.welcome-actions { display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap; }
.welcome-foot {
  margin: 16px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text2);
  text-align: center;
}
.welcome-foot kbd {
  font-family: var(--mono, ui-monospace, SFMono-Regular, monospace);
  font-size: 11px;
  background: var(--bg3);
  border: 1px solid var(--border-bright);
  border-radius: 4px;
  padding: 0 5px;
}
.welcome-link {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  cursor: pointer;
  font: inherit;
  text-decoration: underline;
}
.welcome-link:hover { color: var(--accent-dim); }
@media (max-width: 520px) {
  .welcome-actions { justify-content: stretch; }
  .welcome-actions button { flex: 1 1 auto; }
}

#shortcuts-overlay {
  position: fixed;
  inset: 0;
  /* Sit above ``.pl-modal-backdrop`` (10000) and ``#restart-overlay``
     (10000) — Visual-Test #1 caught the inversion where `?` did nothing
     while a playlist-name modal was open. */
  z-index: 20000;
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
#shortcuts-overlay.hidden { display: none; }

#shortcuts-panel {
  background: var(--bg2);
  border: 1px solid var(--border-bright);
  border-radius: 16px;
  box-shadow: var(--glass-shadow);
  max-width: 680px;
  width: 90vw;
  max-height: 80vh;
  overflow-y: auto;
}

.shortcuts-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px 12px;
  border-bottom: 1px solid var(--border);
}
.shortcuts-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text0);
}
.shortcuts-header button {
  background: none;
  border: none;
  color: var(--text2);
  font-size: 20px;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 4px;
}
.shortcuts-header button:hover { color: var(--text0); background: rgba(255,255,255,0.08); }

/* ── Find-music (archives & demoscene) overlay ──────────────────────────
   Reuses the shortcuts-overlay shell; wider panel for the archive cards. */
#links-overlay {
  position: fixed;
  inset: 0;
  z-index: 20000;
  background: rgba(0,0,0,0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
#links-overlay.hidden { display: none; }
#links-panel {
  background: var(--bg2);
  border: 1px solid var(--border-bright);
  border-radius: 16px;
  box-shadow: var(--glass-shadow);
  max-width: 780px;
  width: 92vw;
  max-height: 84vh;
  overflow-y: auto;
}
.links-body { padding: 4px 18px 16px; }
/* The moved settings sections shouldn't carry tab-specific spacing in the modal */
.links-body .admin-section { margin: 0; padding: 14px 0 4px; border: none; background: none; }
.links-body .admin-section + .admin-section { border-top: 1px solid var(--border); }

.shortcuts-body {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  padding: 20px 24px 24px;
}

.shortcuts-col h4 {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--accent);
  margin-bottom: 10px;
}

.sc-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 5px 0;
  font-size: 12.5px;
  color: var(--text1);
}

.sc-row kbd {
  display: inline-block;
  background: var(--bg4);
  border: 1px solid var(--border-bright);
  border-radius: 5px;
  padding: 2px 8px;
  font-family: inherit;
  font-size: 11px;
  font-weight: 500;
  color: var(--text0);
  min-width: 28px;
  text-align: center;
  white-space: nowrap;
  box-shadow: 0 1px 2px rgba(0,0,0,0.3);
}

.sc-row span {
  flex: 1;
  text-align: right;
  color: var(--text2);
  font-size: 12px;
}

/* ── Admin tabs ──────────────────────────────────────────────────────────── */
.admin-tabs {
  display: flex;
  gap: 0;
  border-bottom: 1px solid var(--border);
  padding: 0 16px;
  flex-shrink: 0;
  /* Tabs (Library / Renderers / Backup / System / Users / Log) overflow
     the admin panel width on phones.  Allow horizontal scroll so every
     tab stays reachable.  ``-webkit-overflow-scrolling: touch`` keeps
     iOS momentum scroll active.  Hide the native scrollbar on macOS
     desktop where the trackpad swipe already works. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
.admin-tabs::-webkit-scrollbar { height: 3px; }
.admin-tabs::-webkit-scrollbar-thumb { background: var(--scrollbar); }
.admin-tab {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--text2);
  padding: 10px 16px 8px;
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
  white-space: nowrap;
}
.admin-tab:hover {
  color: var(--text1);
}
.admin-tab.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
.admin-tab {
  flex: 1;                      /* equal-width tabs */
  text-align: center;
}
.admin-tab-pane {
  display: none;
}
.admin-tab-pane.active {
  display: block;
  animation: pane-glow-in 0.35s ease;
}
@keyframes pane-glow-in {
  from { box-shadow: inset 0 0 40px rgba(180,50,50,0.10); }
  to   { box-shadow: inset 0 0 60px rgba(180,50,50,0.04); }
}
.admin-panel-body {
  box-shadow: inset 0 0 50px rgba(160,40,40,0.04);
}

/* ── Rating stars ──────────────────────────────────────────────────────────── */
.col-rating { width: 80px; text-align: center; white-space: nowrap; }
.col-rating .star {
  cursor: pointer;
  font-size: 14px;
  /* Bigger click target — the 12px/1px glyph was ~14px, below the WCAG
     2.5.8 minimum.  The vertical padding fills the ~28px row (≥24px tall)
     and the horizontal padding widens each star's hit area; the negative
     margin keeps the five-star group from growing the column width. */
  padding: 7px 4px;
  margin: 0 -1px;
  transition: color 0.1s;
}
.col-rating .star-filled { color: var(--accent); }
.col-rating .star-empty { color: var(--bg4); }
.col-rating .star:hover,
.col-rating .star:hover ~ .star { color: var(--accent-dim); }
/* Reverse hover for stars: highlight up to hovered star */
.col-rating:hover .star { color: var(--accent-dim); }
.col-rating:hover .star:hover ~ .star { color: var(--bg4); }
th.col-rating { font-size: 14px; color: var(--accent); cursor: default; }

/* ── Track context menu (right-click) ─────────────────────────────────────── */
.ctx-menu {
  min-width: 180px; background: var(--bg2); border: 1px solid var(--border-bright);
  border-radius: 8px; padding: 4px; z-index: 10000; font-size: 13px;
  box-shadow: 0 8px 28px rgba(0,0,0,0.45);
}
.ctx-item {
  display: block; width: 100%; text-align: left; background: none; border: none;
  color: var(--text0); padding: 7px 12px; border-radius: 5px; cursor: pointer;
  font-size: 13px; font-family: inherit;
}
.ctx-item:hover, .ctx-item:focus { background: var(--accent); color: #fff; outline: none; }
.ctx-sep { height: 1px; background: var(--border); margin: 4px 6px; }
.ctx-rate { display: flex; gap: 3px; padding: 5px 12px; }
.ctx-star { cursor: pointer; font-size: 16px; line-height: 1; color: var(--bg4); }
.ctx-star.on { color: var(--accent); }
.ctx-rate:hover .ctx-star { color: var(--accent-dim); }
.ctx-star:hover ~ .ctx-star { color: var(--bg4); }

/* ── Album-card "play all" hover button ───────────────────────────────────── */
.album-card-art { position: relative; }
.album-card-play {
  position: absolute; right: 8px; bottom: 8px; width: 34px; height: 34px;
  border-radius: 50%; border: none; background: var(--accent); color: #fff;
  font-size: 13px; cursor: pointer; display: flex; align-items: center;
  justify-content: center; opacity: 0; transform: translateY(4px);
  transition: opacity 0.12s, transform 0.12s; box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
.album-card:hover .album-card-play,
.album-card:focus-within .album-card-play { opacity: 1; transform: translateY(0); }
.album-card-play:hover { background: var(--accent-dim); }
@media (prefers-reduced-motion: reduce) {
  .album-card-play { transition: none; transform: none; }
}

/* ── Search preview dropdown ───────────────────────────────────────────── */
.search-preview {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 1000;
  background: var(--bg1);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 10px;
  box-shadow: 0 12px 48px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05);
  backdrop-filter: blur(20px) saturate(150%);
  -webkit-backdrop-filter: blur(20px) saturate(150%);
  margin-top: 4px;
  overflow: hidden;
  display: none;
}
.search-preview.visible { display: block; }

.sp-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  cursor: pointer;
  transition: background 0.08s;
}
.sp-row:hover, .sp-row.sp-highlighted {
  background: rgba(255,255,255,0.06);
}
/* Passive "top result" anchor — distinctly lighter than the active
   hover/keyboard selection above, so it reads as "this is the top hit" rather
   than "selected, press Enter" (Enter opens full results until the user
   arrows down). A subtle accent stripe marks it without implying activation. */
.sp-row.sp-anchored:not(.sp-highlighted):not(:hover) {
  background: rgba(255,255,255,0.025);
  box-shadow: inset 2px 0 0 var(--accent-dim);
}
.sp-art {
  width: 32px;
  height: 32px;
  border-radius: 4px;
  flex-shrink: 0;
  background: var(--bg3);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  position: relative;
}
.sp-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.sp-art .sp-art-ph {
  font-size: 17px;
  line-height: 1;
  color: rgba(107,200,240,0.82);
  filter:
    drop-shadow(0 0 3px rgba(107,200,240,0.7))
    drop-shadow(0 0 7px rgba(107,200,240,0.45))
    drop-shadow(0 0 14px rgba(107,200,240,0.25));
}
.sp-info {
  flex: 1;
  min-width: 0;
  overflow: hidden;
}
.sp-title {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text0);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sp-sub {
  font-size: 11px;
  color: var(--text2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sp-dur {
  font-size: 11px;
  color: var(--text2);
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}
.sp-empty {
  padding: 14px;
  text-align: center;
  color: var(--text2);
  font-size: 12px;
}
.sp-more {
  padding: 6px 12px;
  text-align: center;
  color: var(--text2);
  font-size: 11px;
  border-top: 1px solid var(--border);
  background: rgba(255,255,255,0.02);
}
/* Hint footer when < 8 results: "Press ↵ for full results" */
.sp-hint {
  padding: 5px 12px;
  text-align: center;
  color: var(--text2);
  font-size: 10px;
  border-top: 1px solid var(--border);
  opacity: 0.6;
  letter-spacing: 0.02em;
}
/* Syntax helper dropdown — shown on first focus to expose advanced
   field-operator syntax (artist:foo year:>2020 format:FLAC). */
.sp-syntax-hint {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  background: rgba(255,255,255,0.025);
}
.sp-hint-title {
  display: block;
  font-size: 11px;
  font-weight: 600;
  color: var(--text2);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-bottom: 4px;
}
.sp-hint-ops {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-size: 12px;
  color: var(--text);
  line-height: 1.6;
}
.sp-hint-ops code {
  display: inline-block;
  padding: 1px 6px;
  margin: 0 4px 2px 0;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--border);
  border-radius: 4px;
}
/* Red flag bar when the user typed an unparseable field operator. */
.sp-bad-ops {
  padding: 6px 12px;
  border-bottom: 1px solid rgba(220, 80, 80, 0.35);
  background: rgba(220, 80, 80, 0.08);
  color: #e67878;
  font-size: 12px;
}
.sp-bad-op {
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-weight: 600;
}

/* ── Admin settings grid ──────────────────────────────────────────────── */
.admin-settings-grid {
  display: grid;
  /* minmax(0, 1fr) lets columns actually shrink past their min-content,
     so a long label or an absolutely-positioned tooltip can't force the
     panel into horizontal scroll. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px 14px;
}
.admin-setting {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;          /* allow flex children to shrink inside the grid cell */
  min-height: 30px;      /* even vertical rhythm across checkbox/select/number
                            rows — checkbox rows used to collapse to ~15px while
                            number rows were ~24px, reading as cramped/ragged */
}
.admin-setting label {
  font-size: 12px;
  color: var(--text1);
  flex: 1 1 auto;
  min-width: 0;          /* let the label text wrap rather than push the cell wider */
  line-height: 1.25;
  word-break: normal;
  overflow-wrap: anywhere;
}
/* All right-rail box controls share one styled appearance so the column
   reads as a single aligned unit (the bare OS <select> was the odd one out). */
.admin-setting input[type="number"],
.admin-setting select {
  flex: 0 0 auto;
  width: 72px;
  box-sizing: border-box;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--glass-border);
  border-radius: 6px;
  padding: 4px 8px;
  color: var(--text0);
  font-size: 12px;
}
.admin-setting select {
  width: auto;           /* selects size to their options unless a section pins a width */
  min-width: 120px;
  cursor: pointer;
}
/* Custom checkbox — fully CSS-drawn (appearance:none) so its box size and
   position are identical across Chromium/WebKit/Gecko/Edge.  The native
   ``accent-color`` checkbox measured 16px in Chromium but rendered with a
   browser-specific intrinsic box + margin in Edge, leaving it visibly inset
   from the select/number right edge in the SID panel.  A drawn box removes
   that variance: exactly 16px, zero margin, right edge flush with the other
   controls' right edge in every engine. */
.admin-setting input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin: 0;
  box-sizing: border-box;
  border: 1px solid var(--glass-border);
  border-radius: 4px;
  background: rgba(255,255,255,0.06);
  cursor: pointer;
  position: relative;
  transition: background 0.12s ease, border-color 0.12s ease;
}
.admin-setting input[type="checkbox"]:hover {
  border-color: var(--accent);
}
.admin-setting input[type="checkbox"]:checked {
  background: var(--accent);
  border-color: var(--accent);
}
.admin-setting input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 1px;
  width: 4px;
  height: 8px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.admin-setting input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* SID fidelity: pin the select + number to one width so their left AND right
   edges line up into a clean column (checkboxes stay right-aligned to the same
   right edge). */
.admin-sid-fidelity .admin-setting select,
.admin-sid-fidelity .admin-setting input[type="number"] {
  width: 150px;
  min-width: 150px;
}
.setting-info {
  position: relative;
  cursor: help;
  font-size: 11px;
  color: var(--text2);
  opacity: 0.5;
  margin-left: 2px;
  line-height: 1;
}
.setting-info:hover,
.setting-info:focus,
.setting-info:focus-within { opacity: 0.9; }
.setting-info::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  /* Wrap long tooltips and cap width so the absolute box can't bloat the
     parent's scrollWidth and force the admin panel into horizontal scroll. */
  white-space: normal;
  width: max-content;
  max-width: 220px;
  text-align: center;
  line-height: 1.35;
  font-size: 11px;
  color: var(--text0);
  background: var(--bg2);
  border: 1px solid var(--glass-border);
  border-radius: 6px;
  padding: 5px 10px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
  z-index: 20;
  box-shadow: 0 6px 18px rgba(0,0,0,0.35);
}
/* Keyboard parity — Tab + Enter onto a setting-info icon now reveals its
   tooltip; previously this only showed on :hover and was invisible to
   keyboard users. */
.setting-info:hover::after,
.setting-info:focus::after,
.setting-info:focus-within::after { opacity: 1; }

/* ── About section (System tab) ───────────────────────────────────────── */
.about-section { }
.about-grid {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 10px;
  padding: 8px 4px;
}
.about-logo {
  position: relative;
  flex: none;
  width: 56px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.about-logo-glow,
.about-logo-top {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 44px;
  line-height: 1;
}
.about-logo-glow {
  filter: blur(8px);
  opacity: 0.7;
}
.about-info { display: flex; flex-direction: column; gap: 2px; }
.about-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text0);
  letter-spacing: 0.3px;
}
.about-byline {
  font-size: 11px;
  color: var(--text2);
  letter-spacing: 0.4px;
  margin-bottom: 4px;
}
.about-version {
  font-size: 12px;
  color: var(--text1);
}
.about-version #about-version {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--accent);
  font-weight: 600;
}
.about-links {
  margin-top: 4px;
  font-size: 12px;
}
.about-links a {
  color: var(--accent);
  text-decoration: none;
  transition: opacity 0.15s;
}
.about-links a:hover { opacity: 0.78; text-decoration: underline; }
.about-license {
  margin-top: 6px;
  font-size: 10.5px;
  color: var(--text2);
  letter-spacing: 0.2px;
}

/* ── Admin log viewer ─────────────────────────────────────────────────── */
.admin-log-toolbar {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 8px;
}
.admin-log-toolbar label {
  font-size: 12px;
  color: var(--text2);
  display: flex;
  align-items: center;
  gap: 4px;
}
.admin-log-toolbar select {
  background: var(--bg0);
  color: var(--text1);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 4px;
  font-size: 12px;
}
.admin-log-viewer {
  background: var(--bg0);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  font-family: "SF Mono", "Menlo", monospace;
  font-size: 11px;
  color: var(--text2);
  max-height: calc(100vh - 300px);
  min-height: 200px;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-all;
  line-height: 1.5;
}

/* ── Startup intro overlay ───────────────────────────────────────────────── */
#sb-intro-overlay {
  position: fixed;
  inset: 0;
  z-index: 99999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  pointer-events: none;
}
#sb-intro-logo {
  display: flex;
  align-items: center;
  gap: 20px;
  opacity: 0;
  will-change: transform, opacity;
}
#sb-intro-logo .sb-intro-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  font-size: 80px;
  line-height: 1;
}
#sb-intro-logo .sb-intro-icon-glow {
  display: inline-block;
  color: rgba(107,200,240,0.84);
  filter:
    drop-shadow(0 0 8px rgba(107,200,240,0.85))
    drop-shadow(0 0 20px rgba(107,200,240,0.6))
    drop-shadow(0 0 38px rgba(107,200,240,0.35));
}
#sb-intro-logo .sb-intro-icon-top {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
#sb-intro-logo .sb-intro-title-wrap {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0;
  position: relative;       /* anchor for electric canvas */
}
#sb-intro-logo .sb-intro-text {
  font-size: 76px;
  font-weight: 800;
  color: #f0f2f5;
  letter-spacing: -3px;
  line-height: 1;
  text-shadow:
    0 0 10px rgba(107,200,240,0.7),
    0 0 30px rgba(107,200,240,0.45),
    0 0 60px rgba(107,200,240,0.2);
}
#sb-intro-logo .sb-intro-byline {
  font-size: 38px;
  font-weight: 400;
  color: rgba(180,200,220,0.6);
  letter-spacing: 1px;
  line-height: 1;
  margin-top: 2px;
  text-shadow: 0 0 8px rgba(107,200,240,0.3);
}
#sb-intro-logo .sb-intro-electric {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
}

/* ══════════════════════════════════════════════════════════════════════════════
   SMART NAV — sidebar Smart section styling
   ══════════════════════════════════════════════════════════════════════════════ */

#nav-smart li {
  display: flex;
  align-items: center;
  padding: 6px 14px;
  cursor: pointer;
  color: var(--text1);
  border-radius: 5px;
  margin: 1px 6px;
  transition: background 0.1s, color 0.1s;
  font-size: 12.5px;
}
#nav-smart li:hover {
  background: rgba(255,255,255,0.06);
  color: var(--text0);
}
#nav-smart li.active {
  /* Use the brand accent — Library and Smart were using two competing
     activation colours (UX/UI #2), breaking the system. */
  background: var(--accent-glow);
  color: var(--accent);
  font-weight: 500;
  border: 1px solid rgba(240,114,42,0.22);
}

/* ══════════════════════════════════════════════════════════════════════════════
   FORMAT BADGES — colour-coded capsules in the Type column
   ══════════════════════════════════════════════════════════════════════════════ */

.fmt-badge {
  display: inline-block;
  /* Was 10.5px — bumped to 11px to clear the small-text floor; matches
     --text-xs.  Letter-spacing dropped slightly to compensate for the
     extra pixel. */
  padding: 1px 7px;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.25px;
  line-height: 1.6;
  white-space: nowrap;
  text-transform: uppercase;
}

/* Surround-sound marker — a compact channel-layout pill (5.1 / 7.1 / 4.0) shown
   next to the format badge in the list and next to the format name in the info
   panel.  Rendered ONLY for real multichannel PCM audio; module/chip "channels"
   are synth voices, not speakers, so they never get it (see surroundLabel).  The
   text ("5.1") carries the meaning on its own.  Sits inline so it never changes
   a virtual-scroll row's height. */
.surround-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  border-radius: 4px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.3px;
  line-height: 1.5;
  vertical-align: middle;
  white-space: nowrap;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 13%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  cursor: help;
}
/* Explicit: the display:inline-block above would otherwise beat the UA [hidden]
   rule and leave an empty pill on every mono/stereo row. */
.surround-badge[hidden] { display: none; }

/* Track-health badge — a known playback defect flagged at scan time.  A
   leading glyph + the text label carry the meaning, so it is never conveyed
   by colour alone (WCAG 1.4.1): ◐ = partial (plays, some content substituted),
   ⚠ = corrupt (undecodable).  Amber for partial, danger-red for corrupt. */
.track-defect-badge {
  display: inline-block;
  margin-left: 7px;
  padding: 0 6px;
  border-radius: 4px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.3px;
  line-height: 1.5;
  vertical-align: middle;
  white-space: nowrap;
  text-transform: uppercase;
  border: 1px solid transparent;
  cursor: help;
}
.track-defect-badge::before { margin-right: 3px; opacity: 0.9; }
.track-defect-partial {
  color: #e0a13a;
  background: rgba(224,161,58,0.14);
  border-color: rgba(224,161,58,0.35);
}
.track-defect-partial::before { content: '◐'; }
.track-defect-corrupt {
  color: var(--danger, #ee5555);
  background: rgba(238,85,85,0.14);
  border-color: rgba(238,85,85,0.38);
}
.track-defect-corrupt::before { content: '⚠'; }

/* Shape-based codability so format isn't conveyed by colour alone (WCAG
   1.4.1).  Filled dot = lossless, hollow ring = lossy.  Tracker/SID/MIDI
   keep no glyph — they're already shape-unique formats. */
.fmt-badge.lossless::before,
.fmt-lossless::before { content: '● '; opacity: 0.85; }
.fmt-badge.lossy::before,
.fmt-mp3::before,
.fmt-ogg::before,
.fmt-aac::before { content: '○ '; opacity: 0.7; }

/* Lossless (FLAC, ALAC, WAV, AIFF) — green tint */
.fmt-lossless {
  background: rgba(80,200,120,0.14);
  color: #6fcf8c;
  border: 1px solid rgba(80,200,120,0.22);
}

/* MP3 — blue tint.  Text colour bumped ~15% toward 100% luminance for
   AA contrast on darker --bg surfaces (#7db5f0 → #9fc8f6). */
.fmt-mp3 {
  background: rgba(100,160,240,0.12);
  color: #9fc8f6;
  border: 1px solid rgba(100,160,240,0.20);
}

/* Ogg/Opus — purple tint (luminance-bumped from #b39de0 → #c8b4ec). */
.fmt-ogg {
  background: rgba(160,120,220,0.12);
  color: #c8b4ec;
  border: 1px solid rgba(160,120,220,0.18);
}

/* AAC/M4A — amber tint (luminance-bumped from #d4b44a → #e6cf5d). */
.fmt-aac {
  background: rgba(220,180,60,0.12);
  color: #e6cf5d;
  border: 1px solid rgba(220,180,60,0.18);
}

/* SID — retro cyan */
.fmt-sid {
  background: rgba(0,220,220,0.10);
  color: #40d0d0;
  border: 1px solid rgba(0,220,220,0.18);
}

/* MIDI — pink tint (luminance-bumped from #dc6ab0 → #ec8bc6). */
.fmt-midi {
  background: rgba(220,80,160,0.12);
  color: #ec8bc6;
  border: 1px solid rgba(220,80,160,0.18);
}

/* Tracker (MOD, S3M, XM, IT, etc.) — uses the brand accent so retro
   formats keep brand consistency across the type column (UX/UI #2 flagged
   the previous ``#e8a040`` as one shade off the brand orange #f0722a). */
.fmt-tracker {
  background: var(--accent-glow);
  color: var(--accent);
  border: 1px solid rgba(240,114,42,0.22);
}

/* Fallback — grey */
.fmt-other, .fmt-unknown {
  background: rgba(255,255,255,0.06);
  color: var(--text2);
  border: 1px solid rgba(255,255,255,0.08);
}

/* ══════════════════════════════════════════════════════════════════════════════
   KEYBOARD FOCUS — J/K row navigation highlight
   ══════════════════════════════════════════════════════════════════════════════ */

#track-table tbody tr.kb-focused {
  outline: 2px solid rgba(107,200,240,0.5);
  outline-offset: -2px;
  background: rgba(107,200,240,0.06);
}
#track-table tbody tr.kb-focused td {
  color: var(--text0);
}

/* ══════════════════════════════════════════════════════════════════════════════
   DUPLICATE GROUP ROWS — visual grouping in duplicates view
   ══════════════════════════════════════════════════════════════════════════════ */

/* First track in a duplicate group gets a top border separator */
#track-table tbody tr.dup-group-first {
  border-top: 2px solid rgba(240,114,42,0.25);
}
#track-table tbody tr.dup-group-first td:first-child::before {
  content: '▸';
  color: var(--accent);
  font-size: 10px;
  margin-right: 2px;
}

/* Non-primary tracks are slightly dimmed to emphasize the primary */
#track-table tbody tr.dup-variant {
  opacity: 0.65;
}
#track-table tbody tr.dup-variant:hover {
  opacity: 1;
}

/* Primary badge — small star indicator next to format */
#track-table tbody tr.dup-primary .col-format .fmt-badge::after {
  content: ' ★';
  font-size: 9px;
  opacity: 0.7;
}

/* ──────────────────────────────────────────────────────────────────────────
   Restart overlay — shown while the server process is relaunching.
   ────────────────────────────────────────────────────────────────────────── */
#restart-overlay {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(8, 10, 14, 0.78);
  backdrop-filter: blur(10px) saturate(120%);
  -webkit-backdrop-filter: blur(10px) saturate(120%);
  animation: restart-fade-in 200ms ease-out;
}
#restart-overlay.visible { display: flex; }

#restart-overlay .restart-card {
  min-width: 280px;
  max-width: 420px;
  padding: 28px 32px;
  border-radius: 16px;
  background: rgba(22, 26, 32, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 30px 60px rgba(0, 0, 0, 0.5);
  text-align: center;
  color: #eaeef3;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

#restart-overlay .restart-spinner {
  width: 44px;
  height: 44px;
  margin: 0 auto 18px;
  border: 3px solid rgba(255, 255, 255, 0.12);
  border-top-color: #4aa3ff;
  border-radius: 50%;
  animation: restart-spin 900ms linear infinite;
}

#restart-overlay .restart-title {
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 0.2px;
  margin-bottom: 8px;
}

#restart-overlay .restart-subtitle {
  font-size: 13px;
  color: rgba(234, 238, 243, 0.72);
  line-height: 1.45;
}

@keyframes restart-spin  { to { transform: rotate(360deg); } }
@keyframes restart-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Hint text under admin action buttons */
.admin-hint {
  margin-top: 10px;
  font-size: 12px;
  color: rgba(234, 238, 243, 0.6);
  line-height: 1.45;
  max-width: 480px;
}

/* ── Cross-platform shims ──────────────────────────────────────────────── */

/* Safe-area padding: respects iPad Safari + MacBook notch insets when the
   app is added to the home screen or running fullscreen.  ``env(...)``
   returns 0 outside notched contexts so non-notch browsers are unaffected. */
#header {
  padding-left:  max(var(--space-4, 16px), env(safe-area-inset-left));
  padding-right: max(var(--space-4, 16px), env(safe-area-inset-right));
}
#player-bar {
  /* Grow the bar to accommodate the iOS home indicator instead of
     stealing height from the content.  With ``box-sizing: border-box``
     and a fixed ``height: var(--player-h)``, a bare ``padding-bottom``
     used to shrink the inner content area and clip the 44 px-tall
     waveform canvas at the bottom (REG-4 regression). */
  height: calc(var(--player-h) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
}

/* RTL hook — `dir="rtl"` flips the sidebar/main split.  Full RTL pass
   would need each margin-left/padding-right rewritten with logical
   properties; this baseline at least mirrors the top-level layout. */
[dir="rtl"] #main-layout {
  flex-direction: row-reverse;
}
[dir="rtl"] .player-controls,
[dir="rtl"] .player-extra-controls {
  flex-direction: row-reverse;
}

/* Print stylesheet — dumping the dark glass UI to ink was a 4394-line
   `@media print`-free CSS.  Strip backgrounds, simplify, hide chrome. */
@media print {
  body, #app { background: white !important; color: black !important; }
  body::before { display: none !important; }
  #header, #sidebar, #player-bar, .player-bar, #shortcuts-overlay,
  #admin-overlay, #queue-panel, #playlist-panel, #ti-overlay,
  #eq-overlay, .skip-to-main, .toast-host,
  /* New overlays added by the multi-user / password flows: must also
     stay out of printed pages so a "Print" while logged-out doesn't
     dump a full-page login modal. */
  .auth-overlay, #password-dialog, #restart-overlay, .user-chip {
    display: none !important;
  }
  #content, #track-list-wrap { overflow: visible !important; }
  table, tr, td, th {
    color: black !important;
    background: white !important;
    border-color: #ccc !important;
  }
  a { color: black; text-decoration: underline; }
}

/* ── Soundfont marketplace category header (E-16) ─────────────────────── */
.sf-known-cat-hdr {
  margin-top: 14px;
  margin-bottom: 6px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text2);
}
.sf-known-cat-hdr:first-child { margin-top: 4px; }

/* ── Track Info → Chapters (E-18) ────────────────────────────────────────── */
.ti-chapters {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.ti-chapter-header {
  font-size: 11px;
  color: var(--text2);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  margin-bottom: 6px;
}
.ti-chapter-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.ti-chapter-row {
  display: grid;
  grid-template-columns: 64px 1fr;
  gap: 10px;
  align-items: center;
  background: none;
  border: none;
  text-align: left;
  padding: 8px 10px;
  border-radius: var(--radius-sm, 4px);
  color: var(--text0);
  cursor: pointer;
  font: inherit;
  transition: background 0.12s;
}
.ti-chapter-row:hover  { background: var(--row-hover); }
.ti-chapter-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.ti-chapter-time {
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--text2);
}
.ti-chapter-title {
  font-size: 13px;
  color: var(--text0);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Auth overlay (login / register) ─────────────────────────────────────── */
.auth-overlay {
  position: fixed;
  inset: 0;
  z-index: 20000;            /* above admin, toast, everything */
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg0);
  /* Slight gradient + accent glow so the bare login card doesn't feel like
     a system modal hovering over emptiness. */
  background:
    radial-gradient(circle at 35% 25%, rgba(240,114,42,0.12) 0%, transparent 60%),
    radial-gradient(circle at 70% 80%, rgba(107,200,240,0.10) 0%, transparent 55%),
    var(--bg0);
  /* Respect notch / home-indicator on iPhone X+ — the inset is 0 on a
     regular Mac desktop browser so this is a no-op there. */
  padding:
    max(var(--space-4, 16px), env(safe-area-inset-top))
    max(var(--space-4, 16px), env(safe-area-inset-right))
    max(var(--space-4, 16px), env(safe-area-inset-bottom))
    max(var(--space-4, 16px), env(safe-area-inset-left));
  /* On narrow phones in landscape, the auth-card + keyboard combined can
     exceed viewport height — let the overlay scroll instead of trapping
     the user. */
  overflow-y: auto;
}
:root[data-theme="light"] .auth-overlay {
  /* Wash the accent gradients lighter so they don't crash on near-white
     surfaces.  Without this the radial paint reads as a dirty grey haze. */
  background:
    radial-gradient(circle at 35% 25%, rgba(240,114,42,0.05) 0%, transparent 60%),
    radial-gradient(circle at 70% 80%, rgba(107,200,240,0.05) 0%, transparent 55%),
    var(--bg0);
}
.auth-overlay.hidden { display: none; }
.auth-card {
  width: 100%;
  max-width: 380px;
  background: var(--panel-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg, 12px);
  padding: 28px 28px 22px;
  box-shadow: var(--glass-shadow);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
}
.auth-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 18px;
}
.auth-brand-icon {
  font-size: 28px;
  filter: drop-shadow(0 0 8px rgba(107,200,240,0.45));
}
.auth-brand-name {
  font-size: 18px;
  font-weight: 600;
  color: var(--text0);
  letter-spacing: 0.5px;
}
.auth-title {
  font-size: 20px;
  color: var(--text0);
  margin: 0 0 16px;
  font-weight: 600;
}
.auth-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.auth-field {
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.auth-field > span {
  font-size: 12px;
  color: var(--text2);
}
.auth-field > span em {
  font-style: italic;
  color: var(--text2);
  opacity: 0.7;
}
.auth-field input {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 8px);
  color: var(--text0);
  padding: 9px 12px;
  font-size: 14px;
  outline: none;
  transition: border-color 0.15s, background 0.15s;
}
.auth-field input:focus {
  border-color: var(--accent);
  background: var(--bg1);
}
.auth-submit {
  margin-top: 6px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: var(--radius-md, 8px);
  padding: 11px 16px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s, transform 0.05s;
}
.auth-submit:hover  { background: var(--accent-dim); }
.auth-submit:active { transform: translateY(1px); }
.auth-submit:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.auth-error {
  margin-top: 4px;
  padding: 9px 12px;
  background: rgba(220, 50, 50, 0.16);
  color: #ffb0b0;
  border: 1px solid rgba(220, 50, 50, 0.32);
  border-radius: var(--radius-md, 8px);
  font-size: 12px;
}
:root[data-theme="light"] .auth-error {
  background: rgba(220, 50, 50, 0.08);
  color: #8a1f1f;
  border-color: rgba(220, 50, 50, 0.30);
}
.auth-switch {
  margin-top: 14px;
  display: flex;
  justify-content: center;
}
.auth-link {
  background: none;
  border: none;
  color: var(--accent);
  font-size: 12px;
  cursor: pointer;
  padding: 4px 8px;
}
.auth-link:hover { text-decoration: underline; }
.auth-bootstrap-hint {
  margin-top: 14px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 8px);
  font-size: 11px;
  color: var(--text2);
  line-height: 1.55;
}
.auth-bootstrap-hint code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 11px;
  color: var(--text0);
  background: var(--bg3);
  padding: 1px 5px;
  border-radius: 4px;
}
body.auth-blocked { overflow: hidden; }
/* While the login overlay is up, soften (but don't filter:blur — that
   produces a dark fringe at the viewport edges where the blur samples
   beyond the box).  ``pointer-events:none`` + reduced opacity gives a
   clean dim, and the ``inert`` attribute set via JS removes #app from
   the tab order so the auth modal is truly modal.  Light-mode users
   get a brighter target opacity so the page doesn't go inky-black. */
body.auth-blocked > #app {
  opacity: 0.22;
  pointer-events: none;
  user-select: none;
  transition: opacity 0.18s ease;
}
:root[data-theme="light"] body.auth-blocked > #app { opacity: 0.45; }

/* ── Admin → Users tab ───────────────────────────────────────────────────── */
.users-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  /* On a phone, the 6-column user table doesn't fit in 375px.  Wrap the
     table in a horizontally scrollable container *and* declare a minimum
     width so the cells don't squish unreadable.  The wrapper is the
     ``<tbody>`` parent — making the table itself ``display:block`` would
     reflow the columns, so we use a CSS-only trick: ``overflow-x: auto``
     on a positioned parent + ``min-width`` on the table. */
  min-width: 560px;
}
/* Wrap the table in a scrollable region on narrow screens.  The HTML
   doesn't carry an explicit wrapper, so we use the section parent. */
#tab-users .admin-section { overflow-x: auto; }
.users-table th,
.users-table td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  text-align: left;
  vertical-align: middle;
}
.users-table th {
  color: var(--text2);
  font-weight: 500;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
}
.users-table td.col-username { font-weight: 500; color: var(--text0); }
.users-table .role-pill {
  display: inline-block;
  padding: 2px 9px;
  border-radius: var(--radius-pill, 999px);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.3px;
}
.role-pill.role-admin    { background: rgba(240,114,42,0.18); color: var(--accent); }
.role-pill.role-edit     { background: rgba(107,200,240,0.18); color: #6bc8f0; }
.role-pill.role-readonly { background: rgba(255,255,255,0.10); color: var(--text2); }
/* Light-mode contrast overrides — dark text on a tinted background so
   each pill clears WCAG AA 4.5:1 instead of the 1.4–2.0:1 the QA pass
   measured.  The tile also gets a visible border so the readonly pill
   doesn't vanish on a near-white background. */
:root[data-theme="light"] .role-pill.role-admin {
  background: rgba(240,114,42,0.18);
  color: #8c3d0e;
}
:root[data-theme="light"] .role-pill.role-edit {
  background: rgba(54,131,184,0.18);
  color: #1e5274;
}
:root[data-theme="light"] .role-pill.role-readonly {
  background: rgba(0,0,0,0.06);
  color: var(--text1);
  border: 1px solid var(--border);
}
.users-row-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.users-row-actions button {
  background: var(--bg2);
  color: var(--text0);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 4px);
  padding: 4px 9px;
  font-size: 11px;
  cursor: pointer;
}
.users-row-actions button:hover { background: var(--bg3); }
.users-row-actions button.danger:hover {
  background: rgba(220,50,50,0.25);
  border-color: rgba(220,50,50,0.5);
  color: #ffb0b0;
}
.users-new-form {
  display: grid;
  grid-template-columns: 1fr 1fr 120px 100px auto;
  gap: 10px;
  align-items: end;
  margin-bottom: 14px;
}
.users-new-form input,
.users-new-form select {
  background: var(--bg2);
  border: 1px solid var(--border);
  color: var(--text0);
  border-radius: var(--radius-sm, 4px);
  padding: 6px 9px;
  font-size: 12px;
}
@media (max-width: 720px) {
  .users-new-form {
    grid-template-columns: 1fr 1fr;
  }
}
.user-chip[hidden] { display: none; }
.user-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: var(--text2);
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill, 999px);
}
.user-chip-name { color: var(--text0); font-weight: 500; }

/* ── Password dialog (styled prompt replacement) ─────────────────────────── */
#password-dialog {
  position: fixed;
  inset: 0;
  z-index: 21000;            /* above admin overlay (10000) + auth (20000) */
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  padding:
    max(16px, env(safe-area-inset-top))
    max(16px, env(safe-area-inset-right))
    max(16px, env(safe-area-inset-bottom))
    max(16px, env(safe-area-inset-left));
  overflow-y: auto;
}
#password-dialog.hidden { display: none; }
.password-dialog-card {
  width: 100%;
  max-width: 360px;
  background: var(--panel-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg, 12px);
  padding: 22px 24px 18px;
  box-shadow: var(--glass-shadow);
  margin: 16px;
}
.password-dialog-card h3 {
  font-size: 16px;
  color: var(--text0);
  margin: 0 0 6px;
}
.password-dialog-message {
  font-size: 12px;
  color: var(--text2);
  margin-bottom: 14px;
  line-height: 1.5;
}
.password-dialog-field[hidden] { display: none; }
.password-dialog-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 12px;
}
.password-dialog-field > span {
  font-size: 11px;
  color: var(--text2);
}
.password-dialog-field > span em {
  font-style: italic;
  color: var(--text2);
  opacity: 0.7;
}
.password-dialog-field input {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 8px);
  color: var(--text0);
  padding: 8px 11px;
  font-size: 13px;
  outline: none;
  transition: border-color 0.15s;
}
.password-dialog-field input:focus { border-color: var(--accent); }
.password-dialog-error {
  margin-top: 4px;
  padding: 7px 10px;
  background: rgba(220,50,50,0.16);
  color: #ffb0b0;
  border: 1px solid rgba(220,50,50,0.32);
  border-radius: var(--radius-sm, 4px);
  font-size: 12px;
}
:root[data-theme="light"] .password-dialog-error {
  background: rgba(220,50,50,0.08);
  color: #8a1f1f;
  border-color: rgba(220,50,50,0.30);
}
.password-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 14px;
}
.password-dialog-actions button {
  padding: 8px 16px;
  border-radius: var(--radius-sm, 4px);
  font-size: 13px;
  cursor: pointer;
  border: 1px solid var(--border);
  background: var(--bg2);
  color: var(--text0);
}
.password-dialog-actions button:hover { background: var(--bg3); }
.password-dialog-actions button[type="submit"] {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.password-dialog-actions button[type="submit"]:hover { background: var(--accent-dim); }

/* ── Auth overlay polish: spinner + password toggle ───────────────────── */
.auth-submit.busy {
  position: relative;
  color: transparent;
  cursor: progress;
}
.auth-submit.busy::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 16px;
  margin: -8px 0 0 -8px;
  /* Bumped from rgba(255,255,255,0.4) — 2:1 against the accent orange.
     0.7 alpha clears the ~3:1 non-text-component bar (WCAG 1.4.11). */
  border: 2px solid rgba(255,255,255,0.7);
  border-top-color: #fff;
  border-radius: 50%;
  animation: auth-spin 0.8s linear infinite;
}
@keyframes auth-spin { to { transform: rotate(360deg); } }
/* Reduced-motion fallback: the global keyframe cap above kills auth-spin,
   which leaves a static arc on a button whose text is ``color:transparent``.
   Restore the visible "Sign in" text instead so the user sees the busy
   state without an animated ring. */
@media (prefers-reduced-motion: reduce) {
  .auth-submit.busy { color: inherit; }
  .auth-submit.busy::after { display: none; }
}
.auth-field {
  position: relative;
}
.auth-field-password { display: flex; }
.auth-pw-toggle {
  position: absolute;
  right: 6px;
  bottom: 4px;
  background: none;
  border: none;
  color: var(--text2);
  cursor: pointer;
  font-size: 14px;
  /* iOS HIG / Android Material both want 44pt / 48dp minimum tap targets.
     Bump the toggle so a thumb can reliably hit it on a phone. */
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.auth-pw-toggle:hover { color: var(--text0); }
/* Reserve room on the input so the eye icon doesn't overlap typed text
   (Firefox/Safari otherwise paint the caret over the emoji on long pw). */
.auth-field-password input { padding-right: 48px; }

/* ── Subsonic connection card ─────────────────────────────────────────── */
.subsonic-card {
  display: grid;
  gap: 8px;
  padding: 14px 16px;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 8px);
  font-size: 12px;
}
.subsonic-card-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.subsonic-card-row label {
  color: var(--text2);
  min-width: 84px;
  font-size: 11px;
}
.subsonic-card-row code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  background: var(--bg3);
  color: var(--text0);
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 11px;
  user-select: all;
}
.subsonic-card-row button {
  font-size: 10px;
  padding: 3px 8px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: var(--bg3);
  color: var(--text0);
  cursor: pointer;
}
.subsonic-card-row button:hover { background: var(--bg4); }
.subsonic-clients {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 4px;
}
.subsonic-client-chip {
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill, 999px);
  padding: 3px 10px;
  font-size: 10px;
  color: var(--text2);
}

/* ── HVSC field hint ──────────────────────────────────────────────────── */
.hvsc-status {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  margin-left: 8px;
}
.hvsc-status.on  { color: #6bc8f0; }
.hvsc-status.off { color: var(--text2); }
:root[data-theme="light"] .hvsc-status.on { color: #1e5274; }
.hvsc-status::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

/* ── Skeleton screen for the track list ──────────────────────────────── */
/* Shown the moment a list-load begins (route change, search, etc.).
   Eight rows of pulsing slabs reserve vertical space + give the user
   something to look at — Nielsen's "Skeleton screens considered harmful?"
   research found these *increase* perceived speed over blank spinners
   when the content geometry is predictable (which a track list is).    */
.track-skeleton {
  padding: 4px 0;
}
.track-skeleton .skel-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
:root[data-theme="light"] .track-skeleton .skel-row {
  border-bottom-color: rgba(0, 0, 0, 0.05);
}
.track-skeleton .skel-cell {
  height: 12px;
  border-radius: 4px;
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0.04) 0%,
    rgba(255, 255, 255, 0.10) 50%,
    rgba(255, 255, 255, 0.04) 100%
  );
  background-size: 200% 100%;
  animation: skel-shimmer 1.4s ease-in-out infinite;
}
:root[data-theme="light"] .track-skeleton .skel-cell {
  background: linear-gradient(
    90deg,
    rgba(0, 0, 0, 0.04) 0%,
    rgba(0, 0, 0, 0.10) 50%,
    rgba(0, 0, 0, 0.04) 100%
  );
  background-size: 200% 100%;
}
.track-skeleton .skel-cell-num    { width: 24px; flex: 0 0 24px; }
.track-skeleton .skel-cell-title  { flex: 1 1 0; }
.track-skeleton .skel-cell-artist { flex: 0 0 25%; }
.track-skeleton .skel-cell-dur    { width: 36px; flex: 0 0 36px; }
@keyframes skel-shimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: -100% 0; }
}
/* Honour reduced-motion: a pulsing shimmer is the kind of "interactions
   animation" 2.3.3 calls out.  Static slabs still convey "structure is
   coming" without the parallax sweep. */
@media (prefers-reduced-motion: reduce) {
  .track-skeleton .skel-cell,
  .skel-bar { animation: none; }
}

/* Inline cross-tab link — looks like a link, behaves like a button. */
.admin-inline-link {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  color: var(--accent, #4ea8de);
  text-decoration: underline dotted;
  text-underline-offset: 2px;
  cursor: pointer;
  font: inherit;
  font-size: inherit;
}
.admin-inline-link:hover { text-decoration-style: solid; }
.admin-inline-link:focus-visible {
  outline: 2px solid var(--accent, #4ea8de);
  outline-offset: 2px;
  border-radius: 2px;
}

/* ── Chiptune / tracker archive link cards (Settings → Renderers) ───────── */
.archive-grid {
  list-style: none;
  padding: 0;
  margin: 0 0 8px 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 10px;
}
.archive-card {
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.02);
  overflow: hidden;
  transition: border-color 150ms ease, background 150ms ease;
}
:root[data-theme="light"] .archive-card {
  border-color: rgba(0, 0, 0, 0.08);
  background: rgba(0, 0, 0, 0.02);
}
.archive-card:hover {
  border-color: var(--accent, #4ea8de);
  background: rgba(78, 168, 222, 0.06);
}
.archive-link {
  display: flex;
  gap: 12px;
  padding: 12px;
  align-items: flex-start;
  text-decoration: none;
  color: inherit;
}
.archive-link:focus-visible {
  outline: 2px solid var(--accent, #4ea8de);
  outline-offset: -2px;
}
.archive-globe {
  font-size: 22px;
  flex: 0 0 auto;
  line-height: 1;
  margin-top: 2px;
  opacity: 0.85;
}
.archive-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.archive-name {
  font-weight: 600;
  font-size: 13px;
}
.archive-name::after {
  /* External-link affordance — these cards leave the SoniqBoom UI. */
  content: " \2197";  /* north-east arrow */
  font-weight: 400;
  opacity: 0.55;
  margin-left: 2px;
}
.archive-desc {
  /* var(--text2) is the audited dim-text token (≥4.5:1 in both themes) —
     don't use opacity for text dimming or contrast fails AA. */
  font-size: 12px;
  color: var(--text2, #b6bcc5);
  line-height: 1.4;
}
.archive-fmt {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 11px;
  color: var(--text2, #b6bcc5);
  margin-top: 2px;
}

/* ── Conversion-cache fill bar (Settings → Renderers) ──────────────────── */
.cache-fill-wrap {
  margin: 8px 0 16px 0;
}
.cache-fill-bar {
  position: relative;
  height: 10px;
  width: 100%;
  background: rgba(255, 255, 255, 0.08);
  border-radius: 5px;
  overflow: hidden;
}
:root[data-theme="light"] .cache-fill-bar {
  background: rgba(0, 0, 0, 0.08);
}
.cache-fill-bar-inner {
  height: 100%;
  width: 0;
  background: var(--accent, #4ea8de);
  transition: width 250ms ease;
  border-radius: 5px;
}
/* Threshold colours.  Note: colour alone is NOT the state signal — the
   text label below carries the same info as plain English (WCAG 1.4.1).
   Light-mode variants honour the lighter background. */
.cache-fill-bar-inner[data-fill="low"]  { background: #3fa87b; }
.cache-fill-bar-inner[data-fill="mid"]  { background: #d9a93a; }
.cache-fill-bar-inner[data-fill="high"] { background: #d94f4f; }
:root[data-theme="light"] .cache-fill-bar-inner[data-fill="low"]  { background: #2e8a62; }
:root[data-theme="light"] .cache-fill-bar-inner[data-fill="mid"]  { background: #b08123; }
:root[data-theme="light"] .cache-fill-bar-inner[data-fill="high"] { background: #b03a3a; }
.cache-fill-label {
  margin-top: 6px;
  font-size: 12px;
  opacity: 0.8;
  font-variant-numeric: tabular-nums;
}
.cache-breakdown {
  list-style: none;
  padding: 0;
  margin: 8px 0 12px 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 16px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.cache-breakdown li {
  display: flex;
  justify-content: space-between;
  padding: 2px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
:root[data-theme="light"] .cache-breakdown li {
  border-bottom-color: rgba(0, 0, 0, 0.05);
}
.cache-breakdown .cache-type { opacity: 0.7; }
.cache-breakdown .cache-bytes { font-weight: 600; }

/* ── Responsive: tablet & mobile fallback ─────────────────────────────────
 *
 * Phones get auto-routed to /m (the dedicated mobile shell) on first visit,
 * but tablets, foldables, and anyone who pinned ?ui=desktop still land on
 * this layout.  The rules below keep the rich desktop UI usable down to
 * ~480 px without rebuilding it from scratch — sidebar collapses to a
 * slide-in drawer, the track table sheds optional columns, touch targets
 * grow to the 44 px WCAG minimum, and the player bar reflows into two
 * stacked rows so the right-side controls don't push off-screen.
 */

/* Tablet & narrow desktop: tighten the sidebar, grow tap targets, drop
   the least-essential table columns. */
@media (max-width: 1024px) {
  :root {
    --sidebar-w: 200px;
  }
  /* Bring sub-44 px controls up to WCAG 2.5.5 AAA touch target size. */
  .player-controls button,
  .header-actions button,
  .btn-section-toggle,
  .btn-section-action,
  .btn-track-info {
    min-width: 44px;
    min-height: 44px;
  }
  /* Hide bitrate column first — meta-info, rarely actionable on the go. */
  #track-table th.col-bitrate,
  #track-table td.col-bitrate { display: none; }
}

/* Compact layout: sidebar becomes an off-canvas drawer toggled by the
   hamburger button.  The drawer overlays the content rather than
   re-flowing, so the table keeps its width. */
@media (max-width: 900px) {
  #sidebar {
    position: fixed;
    top: var(--header-h, 52px);
    left: 0;
    bottom: var(--player-h, 152px);
    width: min(86vw, 320px);
    z-index: 40;
    transform: translateX(-100%);
    transition: transform 200ms ease;
    box-shadow: 0 0 0 1px rgba(0,0,0,0.4), 8px 0 24px rgba(0,0,0,0.35);
  }
  #sidebar.is-open {
    transform: translateX(0);
  }
  #sidebar-scrim {
    position: fixed;
    inset: var(--header-h, 52px) 0 var(--player-h, 152px) 0;
    background: rgba(0,0,0,0.45);
    z-index: 39;
    opacity: 0;
    pointer-events: none;
    transition: opacity 200ms ease;
  }
  #sidebar.is-open ~ #sidebar-scrim,
  body.sidebar-open #sidebar-scrim {
    opacity: 1;
    pointer-events: auto;
  }
  /* Hamburger lives in the header; hidden at desktop widths. */
  #sidebar-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    background: transparent;
    border: 1px solid var(--border, rgba(255,255,255,0.1));
    border-radius: 8px;
    color: inherit;
    cursor: pointer;
    margin-right: 8px;
  }
  #sidebar-toggle:focus-visible {
    outline: 2px solid var(--accent, #4ea8de);
    outline-offset: 2px;
  }
  /* Drop more table columns at tablet width. */
  #track-table th.col-format,
  #track-table td.col-format,
  #track-table th.col-year,
  #track-table td.col-year,
  #track-table th.col-location,
  #track-table td.col-location { display: none; }
  /* Player bar: stack info row over the controls row so the right cluster
     no longer collides with the centre cluster. */
  #player-bar {
    flex-wrap: wrap;
    height: auto;
    min-height: 100px;
    padding: 8px 12px;
  }
  #player-bar > * { min-width: 0; }
  .now-playing-art-info { flex: 1 1 100%; }
  .player-controls { flex: 1 1 auto; justify-content: center; }
  .player-right { flex: 0 0 auto; gap: 8px; }
}
@media (min-width: 901px) {
  #sidebar-toggle { display: none; }
}

/* ≤900px stacked rows: instead of (or in addition to) hiding optional
   columns, give title + artist + duration a vertical layout — keeps
   metadata legible without overflow.  ``.col-title`` becomes a flex
   container so the title sits on line 1 with the artist + duration
   tucked beneath it.  Skeleton rows in this band drop to a single
   title + a short duration cell. */
@media (max-width: 900px) {
  #track-table td.col-title {
    /* Stack title + artist-and-duration meta below it.  CSS-only — no
       JS markup changes required; .col-artist / .col-dur cells remain
       but visually fold under the title on narrow displays. */
    white-space: normal;
    line-height: 1.25;
  }
  #track-table td.col-title::after {
    content: "";
    display: block;
  }
  /* Skeleton: drop the artist cell so the responsive row matches the
     two-line shape that real rows now adopt. */
  .track-skeleton .skel-row .skel-cell-artist { display: none; }
  .track-skeleton .skel-row .skel-cell-title  { flex: 1 1 auto; }
}

/* Phone-width fallback (rare — phones usually hit /m, but a user who
   forced ?ui=desktop still lands here).  Compress the header, hide the
   admin overlay's heavy padding, and let dialogs use the full viewport. */
@media (max-width: 600px) {
  :root {
    --header-h: 48px;
    --player-h: auto;
  }
  #header {
    padding: 0 8px;
    gap: 6px;
  }
  #header .header-search { flex: 1 1 0; min-width: 0; }
  #header .header-actions { gap: 4px; }
  #header .logo-text { display: none; }
  #admin-overlay {
    width: 100vw;
    height: 100vh;
    max-height: 100vh;
    max-width: 100vw;
    border-radius: 0;
    inset: 0;
  }
  .password-dialog-card {
    max-width: calc(100vw - 24px);
  }
  /* Drop more columns: title + artist + duration only. */
  #track-table th.col-album,
  #track-table td.col-album,
  #track-table th.col-track,
  #track-table td.col-track { display: none; }
  #track-table { font-size: 13px; }
  #track-table td, #track-table th { padding: 6px 4px; }
}

/* Landscape phone (≤480 px) — compact even further; hide play seekbar
   labels, shrink track table to two-column title/duration. */
@media (max-width: 480px) {
  #track-table th.col-artist,
  #track-table td.col-artist { display: none; }
  .time-display { font-size: 11px; }
}

/* Narrow-viewport converting badge: reposition into the player-bar info
   column instead of the content-area top-right (which collides with the
   stacked-row tracklist on phones). */
@media (max-width: 600px) {
  .converting-badge {
    top: auto;
    right: 12px;
    bottom: calc(var(--player-h, 152px) + 8px);
    font-size: 22px;
    padding: 6px 14px;
  }
}

/* iOS: input font-size must be ≥16px to suppress the auto-zoom-on-focus
   behaviour Safari triggers when an input is below that threshold.  Scope
   to touch/narrow viewports so the desktop UI keeps its tighter form
   chrome — phones/tablets need the bigger input type. */
@media (hover: none), (max-width: 600px) {
  input[type="text"],
  input[type="search"],
  input[type="email"],
  input[type="password"],
  input[type="number"],
  input[type="tel"],
  input[type="url"],
  select,
  textarea {
    font-size: max(1em, 16px);
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   Settings → Services panel
   ═══════════════════════════════════════════════════════════════════════════
   PhD-UX rationale:
     • The toggle is the ENTIRE row's affordance (click anywhere on the row
       to flip).  Visually-paired affordance + state per Fitts's Law — the
       hit target is the full row, not a 30px switch.
     • Each row carries a one-line "what this service does" hint so the
       consequence of a toggle is visible without expanding a help section.
     • "Restart required" banner uses an aria-live region so screen readers
       announce it as soon as the user flips a toggle.
     • Toggle visual state derives from a single :checked attribute on a
       real <input type="checkbox"> — no JS-only state, works without JS,
       and a sighted-keyboard user gets focus rings for free.
   ───────────────────────────────────────────────────────────────────────── */

.services-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.services-empty {
  color: var(--text2);
  font-size: 13px;
  padding: 12px;
  text-align: center;
}

.service-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: var(--surface2, rgba(255, 255, 255, 0.03));
  border: 1px solid var(--border, rgba(255, 255, 255, 0.08));
  border-radius: 10px;
  transition: border-color 0.15s, background 0.15s;
}
.service-row:hover {
  border-color: var(--border-hover, rgba(255, 255, 255, 0.16));
}
.service-row.is-disabled .service-name { opacity: 0.6; }

.service-info { min-width: 0; }
.service-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text1);
  margin: 0;
}
.service-hint {
  font-size: 12px;
  color: var(--text2);
  margin-top: 2px;
  line-height: 1.4;
}
.service-status {
  font-size: 11px;
  margin-top: 4px;
  display: inline-block;
  padding: 2px 8px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.2);
  color: var(--text2);
}
.service-status.is-on  { background: rgba(56, 178, 107, 0.18); color: #4ddc8a; }
.service-status.is-off { background: rgba(220, 80, 70, 0.18);  color: #ff9b8a; }

/* Toggle switch (CSS-only) ────────────────────────────────────────────── */
.service-toggle {
  position: relative;
  display: inline-block;
  width: 42px;
  height: 24px;
  flex-shrink: 0;
}
.service-toggle input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.service-toggle .slider {
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, 0.15);
  border-radius: 12px;
  transition: background 0.18s;
  /* Critical: the .slider is later in DOM than the invisible
     <input> and both are position:absolute with the same z-index,
     so without pointer-events:none the slider catches every click
     and the input never sees a "change" event.  ``pointer-events:
     none`` lets the click fall through to the input below, which
     is what restores the toggle's click-to-flip behaviour.
     Verified 2026-05-23 — without this rule, toggles look correct
     but can't be activated.  Previously hidden because the wrapper
     was a ``<label>`` whose click-anywhere-to-toggle semantics
     masked the layering bug.  We're now a ``<span>`` (a11y reasons),
     so the click forwarding has to be explicit. */
  pointer-events: none;
}
.service-toggle .slider::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  background: #fff;
  border-radius: 50%;
  transition: transform 0.18s;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.service-toggle input:checked + .slider {
  background: var(--accent);
}
.service-toggle input:checked + .slider::after {
  transform: translateX(18px);
}
.service-toggle input:focus-visible + .slider {
  box-shadow: 0 0 0 3px var(--accent-glow, rgba(240, 114, 42, 0.4));
}
.service-toggle .slider { cursor: pointer; }
.service-toggle:hover .slider { filter: brightness(1.15); }

/* UX-2 P0 contrast fix — don't use 0.6 opacity on dark text-on-dark
   surface (drops below WCAG 1.4.3 4.5:1).  Use the dedicated muted
   text token; disabled state is also encoded redundantly via the
   status pill + slider position. */
.service-row.is-disabled .service-name { color: var(--text2); opacity: 1; }

/* UX-2 P0 split: status pill shows just "On"/"Off"; pending-restart
   note appears as a sibling badge only on rows that actually changed
   this session. */
.service-pending {
  margin-left: 8px;
  padding: 2px 8px;
  border-radius: 10px;
  font-size: 11px;
  font-weight: 500;
  background: rgba(220, 180, 60, 0.18);
  border: 1px solid rgba(220, 180, 60, 0.35);
  color: #f0d27a;
  text-transform: lowercase;
}

/* UX-2 P1: per-row inline error so the user maps a failure back to
   the specific row that bounced. */
.service-error {
  margin-top: 6px;
  padding: 6px 10px;
  background: rgba(220, 80, 70, 0.10);
  border-left: 3px solid rgba(220, 80, 70, 0.7);
  color: #ff9c91;
  font-size: 12px;
  border-radius: 0 4px 4px 0;
}

.services-restart-banner {
  margin: 0 0 14px 0;
  padding: 10px 14px;
  background: rgba(220, 180, 60, 0.08);
  border: 1px solid rgba(220, 180, 60, 0.18);
  border-radius: 8px;
  color: var(--text2);
  font-size: 13px;
  display: flex;
  align-items: center;
  gap: 10px;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.services-restart-banner.is-active {
  background: rgba(220, 180, 60, 0.16);
  border-color: rgba(220, 180, 60, 0.4);
  color: #f5d875;
}
.services-restart-banner::before {
  content: "↻";
  font-size: 16px;
}
.services-restart-msg { flex: 1; }
.services-restart-btn {
  background: var(--accent);
  color: #fff;
  border: none;
  padding: 6px 14px;
  font-size: 12px;
  font-weight: 600;
  border-radius: 6px;
  cursor: pointer;
}
.services-restart-btn:hover:not(:disabled) { filter: brightness(1.1); }
.services-restart-btn:disabled { opacity: 0.6; cursor: progress; }

@media (max-width: 480px) {
  /* UX-2 P2: row stacks toggle below hint on narrow viewports. */
  .service-row {
    grid-template-columns: 1fr;
    gap: 8px;
  }
  .service-toggle {
    justify-self: end;
    min-height: 44px;  /* WCAG 2.5.5 target size */
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   Cast picker popover
   ═══════════════════════════════════════════════════════════════════════════
   PhD-UX rationale: Spotify Connect / Apple AirPlay / YouTube Music all
   converged on a popover (not a full modal) anchored near the trigger so
   the listening context stays visible.  This implementation follows that
   pattern with WCAG 2.1 AA focus management + keyboard navigation.
   ───────────────────────────────────────────────────────────────────────── */

#cast-overlay {
  position: fixed;
  inset: 0;
  z-index: 1500;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  padding: 0 24px 110px 0;      /* sits above the player bar */
  background: rgba(0, 0, 0, 0.35);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  pointer-events: auto;
}
#cast-overlay.hidden { display: none; }

#cast-panel {
  width: min(380px, calc(100vw - 32px));
  max-height: min(70vh, 560px);
  background: var(--surface1, #1d1f24);
  border: 1px solid var(--border, rgba(255, 255, 255, 0.1));
  border-radius: 12px;
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.55), 0 2px 6px rgba(0, 0, 0, 0.3);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: castSlideUp 0.18s ease-out;
}

@keyframes castSlideUp {
  from { transform: translateY(8px); opacity: 0; }
  to   { transform: translateY(0);   opacity: 1; }
}

.cast-header {
  display: flex;
  align-items: center;
  padding: 14px 14px 10px 16px;
  border-bottom: 1px solid var(--border, rgba(255, 255, 255, 0.08));
}
.cast-header h3 {
  flex: 1;
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--text1);
}
.cast-icon-btn {
  background: transparent;
  border: none;
  color: var(--text2);
  /* UX-1 P1: 44×44 minimum hit target (WCAG 2.5.5 Enhanced, Apple HIG,
     Material).  Inner SVG keeps its 14 px visual size — only the click
     surface grows. */
  width: 44px;
  height: 44px;
  border-radius: 8px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s, color 0.15s;
}
.cast-icon-btn:hover { background: rgba(255, 255, 255, 0.08); color: var(--text1); }
.cast-icon-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* Active session banner ──────────────────────────────────────────────── */
.cast-active {
  margin: 10px 12px 0 12px;
  padding: 10px 12px;
  /* UX-3 P0 brand fix: was hardcoded blue rgba(74,158,255,*) — switch
     to the orange --accent token used across the rest of the app. */
  background: linear-gradient(135deg,
                              color-mix(in srgb, var(--accent) 14%, transparent),
                              color-mix(in srgb, var(--accent)  6%, transparent));
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius: 8px;
}
.cast-active-codec {
  font-size: 11px;
  color: var(--text2);
  margin-top: 2px;
  text-transform: lowercase;
}
.cast-active.hidden { display: none; }
.cast-active-row { display: flex; align-items: center; gap: 12px; }
.cast-active-info { flex: 1; min-width: 0; }
.cast-active-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text2);
}
.cast-active-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--text1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}
.cast-stop-btn {
  /* UX-1 P1: bump to a touch-friendly hit target (≥44 px minor axis). */
  background: rgba(220, 80, 70, 0.18);
  border: 1px solid rgba(220, 80, 70, 0.5);
  color: #ff9c8d;
  padding: 10px 16px;
  font-size: 12px;
  font-weight: 600;
  border-radius: 8px;
  cursor: pointer;
  min-height: 36px;
}
.cast-stop-btn:hover { background: rgba(220, 80, 70, 0.28); }
.cast-stop-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* States: loading / empty / error ────────────────────────────────────── */
.cast-state {
  padding: 32px 16px;
  text-align: center;
}
.cast-state.hidden { display: none; }
.cast-state-icon {
  font-size: 32px;
  margin-bottom: 12px;
  opacity: 0.7;
}
.cast-state-text { font-size: 14px; color: var(--text1); }
.cast-state-hint {
  font-size: 12px;
  color: var(--text2);
  margin-top: 6px;
  line-height: 1.5;
  max-width: 280px;
  margin-left: auto;
  margin-right: auto;
}

.cast-spinner {
  width: 28px;
  height: 28px;
  margin: 0 auto 16px auto;
  border: 2.5px solid rgba(255, 255, 255, 0.15);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: castSpin 0.9s linear infinite;
}
@keyframes castSpin { to { transform: rotate(360deg); } }

/* UX-1 P0 / P1: inline soft-error banner that keeps the device list
   visible.  Used by _showInlineBanner in cast_picker.js. */
.cast-inline-banner {
  margin: 8px 12px 0 12px;
  padding: 10px 12px;
  background: rgba(220, 180, 60, 0.08);
  border-left: 3px solid rgba(220, 180, 60, 0.5);
  color: var(--text1);
  font-size: 13px;
  display: flex;
  align-items: center;
  gap: 10px;
  border-radius: 0 6px 6px 0;
}
.cast-inline-banner-btn {
  background: transparent;
  border: 1px solid var(--accent);
  color: var(--accent);
  padding: 4px 10px;
  font-size: 12px;
  font-weight: 600;
  border-radius: 6px;
  cursor: pointer;
}
.cast-inline-banner-btn:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }

/* UX-1 P0 retry: action buttons in the error state. */
.cast-error-actions {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-top: 14px;
  flex-wrap: wrap;
}
.cast-error-btn {
  background: var(--accent);
  color: #fff;
  border: none;
  padding: 8px 16px;
  font-size: 13px;
  font-weight: 600;
  border-radius: 8px;
  cursor: pointer;
  min-height: 36px;
}
.cast-error-btn:hover:not(:disabled) { filter: brightness(1.1); }
.cast-error-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.cast-error-btn + .cast-error-btn {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--accent);
}

/* Device list ───────────────────────────────────────────────────────── */
.cast-list {
  list-style: none;
  margin: 0;
  padding: 8px;
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
}
.cast-list-item {
  display: grid;
  grid-template-columns: 24px 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 10px 12px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
  cursor: pointer;
  width: 100%;
  text-align: left;
  color: var(--text1);
  font: inherit;
  transition: background 0.12s, border-color 0.12s;
}
.cast-list-item:hover {
  background: var(--surface2, rgba(255, 255, 255, 0.05));
}
.cast-list-item:focus-visible {
  outline: none;
  border-color: var(--accent);
  background: var(--surface2, rgba(255, 255, 255, 0.05));
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent);
}
.cast-list-item.is-active {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}
.cast-list-item.is-busy {
  opacity: 0.6;
  pointer-events: none;
}

.cast-item-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text2);
}
.cast-item-icon svg { width: 18px; height: 18px; }

.cast-item-info { min-width: 0; }
.cast-item-name {
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cast-item-meta {
  font-size: 11px;
  color: var(--text2);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-top: 1px;
}

.cast-item-state {
  font-size: 11px;
  color: var(--text2);
}
.cast-list-item.is-active .cast-item-state { color: #4ddc8a; }

/* Pref disclosure ───────────────────────────────────────────────────── */
.cast-pref {
  margin: 8px 8px 12px 8px;
  border-top: 1px solid var(--border, rgba(255, 255, 255, 0.06));
  padding-top: 8px;
}
.cast-pref summary {
  padding: 8px 10px;
  font-size: 12px;
  color: var(--text2);
  cursor: pointer;
  border-radius: 6px;
  list-style: none;
}
.cast-pref summary::-webkit-details-marker { display: none; }
.cast-pref summary::before {
  content: "▸";
  display: inline-block;
  margin-right: 6px;
  transition: transform 0.15s;
}
.cast-pref[open] summary::before { transform: rotate(90deg); }
.cast-pref summary:hover { background: var(--surface2, rgba(255, 255, 255, 0.04)); }

.cast-pref-options {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px 10px 4px 22px;
}
.cast-pref-options label {
  display: grid;
  grid-template-columns: 16px auto 1fr;
  gap: 6px;
  align-items: baseline;
  padding: 6px 4px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}
.cast-pref-options label:hover { background: var(--surface2, rgba(255, 255, 255, 0.04)); }
.cast-pref-options label input { margin: 0; }
.cast-pref-options label span:nth-of-type(1) {
  font-weight: 500;
  color: var(--text1);
}
.cast-pref-hint { color: var(--text2); font-size: 11px; }

/* Cast button active-state — pulsing ring while a session is live.
   UX-1 P1 redundant-encoding fix: also fill the glyph (color +
   stronger weight) so reduced-motion users — who get the animation
   killed by the @media block below — still see clear active state. */
#btn-cast.is-casting {
  color: var(--accent);
  position: relative;
}
#btn-cast.is-casting svg {
  /* Stronger stroke + solid fill so the active glyph reads at a glance
     even without the pulsing ring. */
  stroke-width: 2.5;
}
#btn-cast.is-casting::after {
  content: "";
  position: absolute;
  inset: -2px;
  border: 1.5px solid var(--accent);
  border-radius: 8px;
  animation: castPulse 2.4s ease-in-out infinite;
  pointer-events: none;
}
@keyframes castPulse {
  0%, 100% { opacity: 0.5; transform: scale(1); }
  50%      { opacity: 0.95; transform: scale(1.06); }
}

/* UX-3 P1: persistent "Casting to X" pill next to #btn-cast so the
   user sees session state from any view (not only inside the picker). */
.cast-active-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  margin-left: 6px;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 36%, transparent);
  border-radius: 999px;
  color: var(--accent);
  font-size: 12px;
  font-weight: 600;
  max-width: 180px;
  cursor: pointer;
  min-height: 32px;
}
.cast-active-pill.hidden { display: none; }
.cast-active-pill:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
.cast-active-pill:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.cast-active-pill-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* UX-1 P0 mobile bottom-sheet — at viewport ≤ 600 px we don't have
   room for the right-anchored 380 px popover; promote to a full-width
   sheet rising from the bottom (the YouTube Music / Spotify Connect
   mobile pattern).  Also reset the player-bar gutter at this width. */
@media (max-width: 600px) {
  #cast-overlay {
    padding: 0;
    align-items: stretch;
    justify-content: stretch;
  }
  #cast-panel {
    width: 100%;
    max-width: 100%;
    max-height: 85vh;
    border-radius: 12px 12px 0 0;
    align-self: flex-end;
    margin: 0;
  }
  .cast-active-pill { display: none; }  /* save toolbar space on mobile */
}

/* Light theme tweaks ─────────────────────────────────────────────────── */
[data-theme="light"] #cast-panel {
  background: #fff;
  border-color: rgba(0, 0, 0, 0.12);
}
[data-theme="light"] .service-row {
  background: rgba(0, 0, 0, 0.025);
  border-color: rgba(0, 0, 0, 0.08);
}
[data-theme="light"] .service-toggle .slider {
  background: rgba(0, 0, 0, 0.2);
}
/* UX-3 P1 light-mode contrast fixes — the previous gradient + colour
   combo dropped below WCAG 1.4.3 4.5:1 on white surface.  Lift the
   contrast by darkening the band tint. */
[data-theme="light"] .cast-active {
  background: linear-gradient(135deg,
                              color-mix(in srgb, var(--accent) 18%, transparent),
                              color-mix(in srgb, var(--accent)  9%, transparent));
}
[data-theme="light"] .cast-stop-btn {
  color: #b03830;
  background: rgba(220, 80, 70, 0.10);
  border-color: rgba(220, 80, 70, 0.45);
}

/* Reduced motion override ───────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  #cast-panel,
  #btn-cast.is-casting::after {
    animation: none;
  }
  /* UX-1 P2: don't let the spinner become an invisible incomplete
     circle.  Replace the animation with a static accent-coloured ring
     (still communicates "loading" by visual presence). */
  .cast-spinner {
    animation: none;
    border-top-color: var(--accent);
    border-color: color-mix(in srgb, var(--accent) 50%, transparent);
    opacity: 0.7;
  }
}

/* ── ffmpeg incomplete-binary banner ─────────────────────────────────
   Shown under the Renderer Status grid when the running ffmpeg is
   missing required encoders/demuxers.  Offers a one-click refetch. */
.ffmpeg-banner {
  margin-top: 14px;
  padding: 12px 14px;
  background: rgba(240, 184, 64, 0.10);
  border: 1px solid rgba(240, 184, 64, 0.35);
  border-left: 4px solid #f0b840;
  border-radius: 6px;
  color: var(--text1);
  font-size: 13px;
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.ffmpeg-banner.hidden { display: none; }
.ffmpeg-banner-text { flex: 1; min-width: 240px; }
.ffmpeg-banner-btn {
  background: var(--accent);
  color: #fff;
  border: none;
  padding: 8px 16px;
  font-size: 13px;
  font-weight: 600;
  border-radius: 6px;
  cursor: pointer;
  min-height: 36px;
}
.ffmpeg-banner-btn:hover:not(:disabled) { filter: brightness(1.1); }
.ffmpeg-banner-btn:disabled { opacity: 0.6; cursor: progress; }
.ffmpeg-banner-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.ffmpeg-banner-error {
  flex-basis: 100%;
  margin-top: 6px;
  color: #ff9c8d;
  font-size: 12px;
}

/* ── Radio mode overlay ─────────────────────────────────────────────────────
   The foreground radio experience: cover art large with a live oscilloscope
   over it, track info + up-next beside it. Session bar lives in the queue
   panel (#radio-session-bar). */
#radio-overlay {
  position: fixed; inset: 0; z-index: 1200;
  background: rgba(5, 7, 9, 0.82);
  backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
  display: flex; align-items: center; justify-content: center;
}
#radio-overlay.hidden { display: none; }
#radio-panel {
  position: relative;
  display: grid; grid-template-columns: minmax(280px, 46vmin) minmax(300px, 420px);
  gap: 28px; align-items: stretch;
  max-width: min(94vw, 1040px); max-height: 90vh;
  padding: 30px; border-radius: 16px;
  background: #101318;
  border: 1px solid rgba(255, 255, 255, 0.10);
  box-shadow: 0 28px 80px rgba(0, 0, 0, 0.6);
}
.radio-close {
  position: absolute; top: 10px; right: 12px;
  background: none; border: none; color: #828a98;
  font-size: 26px; line-height: 1; cursor: pointer; padding: 4px 8px;
}
.radio-close:hover { color: #e8eaed; }
#radio-art-wrap {
  position: relative; aspect-ratio: 1 / 1; align-self: center;
  border-radius: 12px; overflow: hidden;
  background: #1a1d22 url('data:image/svg+xml;utf8,') center/cover;
}
#radio-art {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; filter: brightness(0.82);
}
#radio-scope {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; mix-blend-mode: screen;
}
#radio-side { display: flex; flex-direction: column; min-width: 0; gap: 10px; }
.radio-eyebrow {
  font-size: 12px; letter-spacing: 0.18em; text-transform: uppercase;
  color: #f0722a; opacity: 0.95;
}
.radio-eyebrow #radio-seed-label { color: #ffb27d; text-transform: none; letter-spacing: normal; }
#radio-title {
  font-size: clamp(20px, 2.6vw, 30px); font-weight: 700; color: #e8eaed;
  line-height: 1.2; overflow-wrap: anywhere;
}
#radio-artist { font-size: 14px; color: #aab0bb; }
.radio-controls { display: flex; gap: 10px; margin: 8px 0 2px; }
.radio-controls button {
  font-size: 18px; width: 46px; height: 46px; border-radius: 50%;
  background: #1a1d22; color: #e8eaed;
  border: 1px solid rgba(255, 255, 255, 0.14); cursor: pointer;
}
.radio-controls button:hover { border-color: #f0722a; color: #f0722a; }
#radio-play { background: #f0722a; color: #0d0f12; border-color: #f0722a; }
.radio-upnext {
  flex: 1; min-height: 0; overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, black 72%, transparent 100%);
  mask-image: linear-gradient(to bottom, black 72%, transparent 100%);
}
.radio-upnext-head {
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  color: #828a98; margin-bottom: 6px;
}
#radio-upnext-list { list-style: none; margin: 0; padding: 0; }
#radio-upnext-list li {
  display: flex; align-items: center; gap: 10px;
  padding: 5px 8px; border-radius: 8px; cursor: pointer; font-size: 13px;
}
.radio-upnext-art {
  width: 30px; height: 30px; flex-shrink: 0;
  border-radius: 6px; object-fit: cover; background: #1a1d22;
}
.radio-upnext-art.noart { visibility: hidden; }
#radio-upnext-list li:hover { background: rgba(255, 255, 255, 0.06); }
.radio-upnext-title { flex: 1; color: #cfd3da; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.radio-upnext-artist { color: #828a98; flex-shrink: 0; max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.radio-upnext-empty { color: #5f6671; font-size: 12.5px; padding: 6px 8px; }
.radio-actions { display: flex; gap: 10px; }
.radio-actions button {
  font-size: 12.5px; padding: 7px 14px; border-radius: 8px; cursor: pointer;
  background: transparent; color: #aab0bb; border: 1px solid rgba(255, 255, 255, 0.18);
}
.radio-actions button:hover { border-color: #f0722a; color: #f0722a; }

/* Session bar inside the queue panel */
#radio-session-bar {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 12px;
  background: rgba(240, 114, 42, 0.10);
  border-bottom: 1px solid rgba(240, 114, 42, 0.35);
  font-size: 12.5px; color: #aab0bb;
}
#radio-session-bar .radio-bar-text { flex: 1; min-width: 0; }
#radio-session-bar .radio-bar-text b { color: #f0722a; }
#radio-session-bar .radio-bar-text small { display: block; color: #828a98; font-size: 11px; }
#radio-session-bar button {
  font-size: 11.5px; padding: 3px 10px; border-radius: 6px; cursor: pointer;
  background: transparent; color: #aab0bb; border: 1px solid rgba(255, 255, 255, 0.2);
}
#radio-session-bar button:hover { border-color: #f0722a; color: #f0722a; }

@media (max-width: 760px) {
  #radio-panel { grid-template-columns: 1fr; max-height: 94vh; overflow: auto; gap: 18px; }
  #radio-art-wrap { max-width: 70vmin; margin: 0 auto; }
}

/* ══════════════════════════════════════════════════════════════════════════════
   STATIONS — internet radio
   ══════════════════════════════════════════════════════════════════════════════ */

#section-stations h3 { display: flex; align-items: center; gap: 8px; }

#nav-stations li {
  display: flex; align-items: center; padding: 6px 14px; cursor: pointer;
  color: var(--text1); border-radius: 5px; margin: 1px 6px;
  transition: background 0.1s, color 0.1s; font-size: 12.5px;
}
#nav-stations li:hover { background: rgba(255,255,255,0.06); color: var(--text0); }
#nav-stations li.active {
  background: var(--accent-glow); color: var(--accent); font-weight: 500;
  border: 1px solid rgba(240,114,42,0.22);
}

#stations-view { padding: 14px 18px 40px; overflow-y: auto; height: 100%; }

/* Now-playing card */
#st-now {
  display: flex; align-items: center; gap: 14px;
  min-height: 56px;   /* reserve the played-card height so starting a station never shifts the list */
  padding: 14px 16px; margin-bottom: 14px; border-radius: 10px;
  background: var(--bg3, #222); border: 1px solid var(--border-bright, #444);
}
#st-now.hidden { display: none; }
/* Idle placeholder — occupies the same space but reads as "nothing playing yet". */
#st-now.st-now-empty { opacity: 0.55; }
#st-now.st-now-empty .st-now-name { color: var(--text2, #999); font-weight: 500; }
.st-now-art {
  width: 56px; height: 56px; border-radius: 8px; object-fit: cover;
  flex: 0 0 auto;
}
.st-now-glyph {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 30px; background: rgba(255,255,255,0.05);
}
.st-now-info { flex: 1; min-width: 0; }
.st-now-name { font-weight: 600; color: var(--text0, #fff); }
.st-now-title {
  color: var(--accent); font-size: 13px; margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.st-now-quality { color: var(--text2, #999); font-size: 11.5px; margin-top: 2px; }
.st-now-quality a { color: var(--text2, #999); text-decoration: underline; }
.st-btn {
  background: transparent; border: 1px solid var(--border-bright, #444);
  color: var(--text1, #ddd); border-radius: 8px; padding: 8px 12px;
  font-size: 16px; cursor: pointer; transition: color 0.1s, border-color 0.1s;
}
.st-btn:hover { color: var(--text0, #fff); border-color: var(--text2, #999); }
.st-btn.on { color: var(--accent); border-color: var(--accent); }

/* Breadcrumbs + bucket tabs */
#st-crumbs { margin: 4px 2px 12px; font-size: 13px; color: var(--text2, #999); }
.st-crumb { color: var(--text1, #ccc); cursor: pointer; text-decoration: underline; }
.st-crumb:hover { color: var(--accent); }
.st-crumb-here { color: var(--text0, #fff); font-weight: 600; }
.st-crumb-sep { opacity: 0.5; }

.st-tabs { display: flex; gap: 6px; margin-bottom: 12px; }
.st-tab {
  background: transparent; border: 1px solid var(--border-bright, #444);
  color: var(--text1, #ccc); border-radius: 14px; padding: 4px 14px;
  font-size: 12px; cursor: pointer;
}
.st-tab:hover { color: var(--text0, #fff); }
.st-tab.on { background: var(--accent-glow); color: var(--accent); border-color: rgba(240,114,42,0.35); }

/* Station rows */
.st-row {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 10px; border-radius: 8px; cursor: pointer;
  transition: background 0.1s;
}
.st-row:hover { background: rgba(255,255,255,0.05); }
.st-row-art {
  width: 28px; height: 28px; flex: 0 0 auto; display: inline-flex;
  align-items: center; justify-content: center; font-size: 18px;
}
.st-row-art img { width: 28px; height: 28px; border-radius: 5px; object-fit: cover; }
.st-row-name {
  flex: 1; min-width: 0; color: var(--text0, #eee); font-size: 13.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.st-row-fav { color: var(--accent); }
.st-row-meta { color: var(--text2, #888); font-size: 11.5px; flex: 0 0 auto; }
.st-row-group .st-row-name { font-weight: 500; }

.st-empty { color: var(--text2, #999); padding: 28px 6px; font-size: 13px; }

/* ── Stations: search-preview group + info modal ─────────────────────────────── */
.sp-group-hdr {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text2, #888); padding: 8px 12px 3px; font-weight: 600;
}
/* Header row with a trailing "Show all →" affordance (Stations section). */
.sp-group-hdr-row { display: flex; align-items: baseline; justify-content: space-between; }
.sp-show-all {
  background: none; border: none; cursor: pointer; padding: 0;
  color: var(--accent); font-weight: 600; font-size: 10.5px;
  text-transform: none; letter-spacing: normal; opacity: 0.9;
}
.sp-show-all:hover { opacity: 1; text-decoration: underline; }
.sp-station-row .sp-art-ph { font-size: 15px; }
.sp-station-row .sp-dur { color: var(--accent); font-size: 13px; }

#st-info-overlay {
  position: fixed; inset: 0; z-index: 1250; display: none;
  align-items: center; justify-content: center;
  background: rgba(0,0,0,0.55); backdrop-filter: blur(4px);
}
#st-info-overlay.open { display: flex; }
#st-info-panel {
  background: var(--bg2, #1a1a1a); border: 1px solid var(--border-bright, #444);
  border-radius: 12px; padding: 22px; width: min(460px, 92vw);
  max-height: 88vh; overflow-y: auto; position: relative;
  box-shadow: 0 12px 48px rgba(0,0,0,0.5);
}
#st-info-close {
  position: absolute; top: 12px; right: 12px; font-size: 18px;
  padding: 4px 10px; line-height: 1;
}
.st-info-head { display: flex; gap: 14px; align-items: center; margin-bottom: 16px; padding-right: 32px; }
.st-info-art {
  width: 64px; height: 64px; border-radius: 10px; object-fit: cover; flex: 0 0 auto;
}
.st-info-glyph {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 34px; background: rgba(255,255,255,0.05);
}
.st-info-name { font-size: 18px; font-weight: 600; color: var(--text0, #fff); }
.st-info-now { color: var(--accent); font-size: 13px; margin-top: 4px; }
.st-info-meta { display: grid; grid-template-columns: 84px 1fr; gap: 7px 12px; font-size: 13px; margin: 0 0 18px; }
.st-info-meta dt { color: var(--text2, #888); }
.st-info-meta dd { margin: 0; color: var(--text1, #ddd); word-break: break-word; }
.st-info-meta a { color: var(--accent); }
.st-info-streams { list-style: none; margin: 0; padding: 0; }
.st-info-streams li { padding: 2px 0; }
.st-info-playing { color: var(--accent); margin-left: 8px; font-size: 11.5px; }
.st-info-nosup { color: var(--text2, #888); font-size: 11px; }
.st-info-actions { display: flex; gap: 10px; }
.st-info-actions .st-btn { flex: 1; padding: 9px; font-size: 13px; }
