/* ============================================================
   AQAI PLAYER — dark edition
   background #0E1F1A (near-black green)  ·  3D object/accent mint #9AF7B4
   text & controls white
   TRACKS are loaded live from /api/tracks instead of being hardcoded.
   Open with  #sync  in the URL to reveal the Sync Studio.
   ============================================================ */
@font-face{font-family:'Brice';src:url('assets/fonts/Brice-Light.woff2') format('woff2');font-weight:300;font-style:normal;font-display:swap}
@font-face{font-family:'Brice';src:url('assets/fonts/Brice-Regular.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'Brice';src:url('assets/fonts/Brice-Semi-Bold.woff2') format('woff2');font-weight:600;font-style:normal;font-display:swap}
@font-face{font-family:'Brice';src:url('assets/fonts/Brice-Bold.woff2') format('woff2');font-weight:700;font-style:normal;font-display:swap}
@font-face{font-family:'Brice';src:url('assets/fonts/Brice-Black.woff2') format('woff2');font-weight:900;font-style:normal;font-display:swap}
@font-face{font-family:'Brice Condensed';src:url('assets/fonts/Brice-Regular-Condensed.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'Brice Condensed';src:url('assets/fonts/Brice-Semi-Bold-Condensed.woff2') format('woff2');font-weight:600;font-style:normal;font-display:swap}
@font-face{font-family:'Brice Condensed';src:url('assets/fonts/Brice-Bold-Condensed.woff2') format('woff2');font-weight:700;font-style:normal;font-display:swap}
@font-face{font-family:'Brice Condensed';src:url('assets/fonts/Brice-Black-Condensed.woff2') format('woff2');font-weight:900;font-style:normal;font-display:swap}
@font-face{font-family:'Brice Expanded';src:url('assets/fonts/Brice-Black-Expanded.woff2') format('woff2');font-weight:900;font-style:normal;font-display:swap}
:root{
  --bg:         #0E1F1A;
  --bg-dark:    #0B1915;
  --bg-soft:    #16332A;
  --object:     #9AF7B4;
  --object-deep:#7FE39C;
  --active-green: #7CFF9E;
  --lyric-active: #127F86;
  --lyric-yellow: #FFE066;
  --gate-blue: #5AC8FA;
  --fg:         #FFFFFF;
  --fg-soft:    rgba(255,255,255,.6);
  --fg-faint:   rgba(255,255,255,.32);
  --line:       rgba(255,255,255,.18);
  --wash:       rgba(255,255,255,.08);
  --nav-bg:     rgba(14,31,26,.88);
  --hover-gradient: var(--lyric-active);
  --font-display:'Brice', sans-serif;
  --font-body:  'Brice', sans-serif;
  --font-title: 'Brice Condensed', sans-serif;
  --nav-h:      56.64px;
}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{height:100%;overscroll-behavior:none}
/* black behind everything: shows in the phone's safe areas / overscroll instead of the theme color */
html{background:#000}
/* owner-only editing controls (delete/edit/flag/video-export/creature-tune/
   color-picker/...) - hidden the instant CSS parses, before app.js even
   runs, let alone before its /api/tracks fetch resolves and confirms this
   visitor isn't the owner. Without this they render visible from the
   static HTML for however long that round trip takes, then vanish - a
   real, visible flash on a fresh page load. body.owner-mode (added by
   updateEditControlsVisibility() in app.js, only once the server
   confirms real owner access) is the only thing that reveals them. */
body:not(.owner-mode) #pano-btns,
body:not(.owner-mode) #btn-delete,
body:not(.owner-mode) #btn-edit-title,
body:not(.owner-mode) #btn-edit-artist,
body:not(.owner-mode) #btn-edit-lyrics,
body:not(.owner-mode) #btn-relocate-artist,
body:not(.owner-mode) #lf-edit-btns,
body:not(.owner-mode) #lyrics-audit-block,
body:not(.owner-mode) #btn-flag-lyrics,
body:not(.owner-mode) #btn-make-video,
body:not(.owner-mode) #btn-preview-video,
body:not(.owner-mode) #btn-creature-tune,
body:not(.owner-mode) #btn-color-picker{
  display:none !important;
}
body{
  color:var(--fg);
  font-family:var(--font-body);
  touch-action:manipulation;
  overflow:hidden;
}
body::before{
  content:"";position:fixed;inset:0;z-index:-1;
  background:#000000;
}
body::after{
  content:"";position:fixed;left:0;right:0;bottom:max(env(safe-area-inset-bottom), 4px);z-index:60;
  height:10px;background:var(--artist-color, var(--active-green));pointer-events:none;
}
/* the strips of screen OUTSIDE the app: under the phone's status bar / notch
   (top) and home-indicator / browser toolbar (bottom). viewport-fit=cover lets
   the page draw there, and newer iPhones show whatever is under those bars, so
   without these the artist-colored wash and accent strip bled through as
   green bars. env() is 0 wherever there is no such area, so this changes
   nothing on desktop. */
html::before,html::after{
  content:"";position:fixed;left:0;right:0;z-index:1000;background:#000000;pointer-events:none;
}
/* at least 4px even where there is no safe area: Safari (iOS 26+ ignores
   theme-color) tints its address bar / status bar from whatever the page draws
   at its very top and bottom edge, so a thin black band there keeps them black */
html::before{top:0;height:max(env(safe-area-inset-top), 4px)}
html::after{bottom:0;height:max(env(safe-area-inset-bottom), 4px)}
/* everywhere except the start screen the bottom edge is the artist's color (this
   band replaces the old 10px accent strip, so browsers tint their bottom bar with it) */
html:has(body:not(.gate-active))::after{
  background:var(--artist-color, var(--active-green));
  height:max(env(safe-area-inset-bottom), 10px);
}
body:not(.gate-active)::after{display:none}
/* phones/tablets only: the status bar (time, camera, battery) is tinted by the page
   content underneath the whole bar, ~60px, not just its top edge - so the top is solid
   black there, fading out into the app. It lives INSIDE #app (see #top-fade in
   index.html), below the AQAI logo, so the logo sits in front of the fade instead of
   being dimmed by it. */
#top-fade{display:none}
@media (hover:none) and (pointer:coarse){
  #top-fade{
    display:block;position:fixed;top:0;left:0;right:0;z-index:2;pointer-events:none;
    height:calc(env(safe-area-inset-top) + 60px);
    background:linear-gradient(to bottom, #000000 0, #000000 calc(env(safe-area-inset-top) + 24px), rgba(0,0,0,0) 100%);
  }
}
button{font-family:inherit;color:inherit;background:none;border:0;cursor:pointer}
button:focus-visible{outline:2px solid var(--fg);outline-offset:3px;border-radius:8px}
/* belt-and-suspenders: every clickable/interactive control shows a hand
   cursor, even non-<button> ones (the seek bar, range inputs, ...) */
button,[role="button"],[role="slider"],input[type="range"],a,.nav-btn,label{cursor:pointer}
textarea,input{font-family:var(--font-body);color:var(--fg)}
img.logo{display:block;user-select:none;-webkit-user-drag:none;filter:brightness(0) invert(1)}
body.theme-light img.logo{filter:brightness(0)}

/* ---------- entry gate (autoplay policy) ---------- */
/* while the gate is up, hide the rest of the Home screen's chrome so
   only its #stage (the panorama sphere) shows through the transparent
   gate behind the logo - otherwise the player controls, top bar and
   bottom nav would all be visible (and clickable) underneath it too */
body.gate-active .home-top,
body.gate-active #lyrics,
body.gate-active #wave-canvas,
body.gate-active .player,
body.gate-active nav,
/* intro sphere shows raw: drop the artist-color tint and the bottom
   readability scrim so no overlay sits on top of it (shader vignette /
   scanlines / darkening are disabled via the uGate uniform in app.js) */
body.gate-active #artist-color-overlay,
body.gate-active #bg-gradient{display:none}
/* the bottom accent bar is also part of that same chrome - no artist is
   loaded yet on the gate screen, so it'd otherwise show as a flat green
   strip with no purpose */
body.gate-active::after{display:none}
/* transparent - the panorama sphere from #stage (the same one the music
   screen uses) shows straight through, so the gate gets its background
   video, mouse-look, vignette and CRT lines for free instead of a
   separate copy of any of that (see loadPanoFile in app.js's boot code) */
#gate{
  position:fixed;inset:0;z-index:60;
  display:flex;align-items:center;justify-content:center;
  background:transparent;
  transition:opacity .6s ease, visibility .6s;
  padding:24px;
  overflow:hidden;
}
#gate.hidden{opacity:0;visibility:hidden}
/* the artist creatures, standing along the bottom of the start screen: a
   transparent-background WebM (black keyed out), centered, at least the
   75% of the screen wide (570px at least, cropped at the sides on narrow phones), behind the
   logo/button */
#gate-creatures{
  position:absolute;left:50%;bottom:0;z-index:1;
  width:max(75%, 570px);height:auto; /* 25% smaller than full width */
  transform:translateX(-50%);
  pointer-events:none;
}
/* phones: 75% of the size it has above (570px wide minimum -> 427.5px) */
@media (max-width:480px){
  #gate-creatures{width:max(56.25%, 427.5px)}
}
/* logo/button/hint sit statically - only the logo itself tilts with the
   mouse (see #gate-logo-tilt below), not the button/hint text */
/* only the logo is in normal flow, so #gate's flex-centering puts it dead
   center (both axes); the button + hint float below it, offset 80px lower */
/* the button is what #gate's flex-centering puts dead center (vertically and
   horizontally); the logo hangs 15px above it (see #gate-logo-tilt) */
#gate-content{
  position:relative;z-index:2;
  display:flex;flex-direction:column;align-items:center;
  transform:translateY(-25vh); /* logo + button + hint live in the top half of the screen */
}
.gate-actions{
  position:relative;
  display:flex;flex-direction:column;align-items:center;gap:34px;
  white-space:nowrap;
}
/* tilts in 3D with the mouse, same mechanism as the lyrics carousel */
#gate-logo-tilt{
  /* 15px above the button; centered with margins because app.js owns this element's transform (mouse tilt) */
  position:absolute;bottom:calc(100% + 15px);left:50%;width:341.25px;margin-left:-170.625px; /* 30% smaller than the original 487.5px */
  perspective:800px;transform-style:preserve-3d;
  /* fully opaque, no blend - the logo is solid black and the screen-blended
     reflection layer turns its front face into a full mirror of the sphere */
}
/* extruded 3D logo: stacked copies of the same silhouette a few px
   apart in depth (see app.js, which builds the .logo3d-layer stack),
   spinning slowly around its own Y axis independent of the mouse tilt */
.logo3d{
  /* every .logo3d-layer child is position:absolute (so they can stack
     exactly on top of each other at different depths) - without an
     explicit size here the whole thing collapses to 0x0, since
     absolutely-positioned children can't establish their parent's size */
  /* fixed px, not vw-based - this used to shrink along with narrower
     (mobile) viewports since both width and font-size were viewport-
     relative; pinned to the values they already capped out at on desktop
     (375px / 108px) so the logo now renders the same physical size on
     every screen instead of scaling down on mobile */
  width:341.25px;aspect-ratio:8/3; /* 30% smaller (was 487.5px) */
  position:relative;transform-style:preserve-3d;
  animation:logo3dSpin 10.8s linear infinite; /* 20% heavier/slower spin */
  font-family:var(--font-display);font-weight:900;letter-spacing:-.02em;
  font-size:98.28px;line-height:1;white-space:nowrap;
}
@keyframes logo3dSpin{ to { transform:rotateY(360deg); } }
/* every face (front/back end caps + side walls) is a canvas-rendered
   bitmap <img> - gradient + specular sheen (+ bevel on the end caps)
   baked in, see renderLogoFace() in app.js - rather than a live CSS/SVG
   gradient, which silently paints as a flat color once nested inside
   this transform-style:preserve-3d stack */
.logo3d-layer{
  position:absolute;inset:0;
  display:flex;align-items:center;justify-content:center;
}
/* front and back faces (end caps) - not the side/depth walls in between -
   at reduced opacity */
.logo3d-front,.logo3d-back{opacity:1}
/* (opacity + overlay blend for the intro logo are on #gate-logo-tilt, its
   wrapper, so #gate-logo3d keeps preserve-3d and its 50px extrusion shows) */
/* sphere-control toggle: lit in the artist colour while the user is
   steering the sphere with the mouse (auto music-reactive drift otherwise) */
.gate-btn{
  font-family:var(--font-display);font-weight:600;font-size:13px;letter-spacing:.2em;
  color:var(--bg);background:var(--object);
  padding:19px 46px;border-radius:999px;
  box-shadow:0 20px 68px rgba(0,0,0,.315);
  animation:pulse 2.4s ease-in-out infinite, gateBtnColorCycle 4s ease-in-out infinite;
  transition:background .2s,transform .1s;
}
.gate-btn:hover{background:var(--object-deep)}
.gate-btn:active{background:var(--active-green)}
/* shown while the library manifest is still loading - disabled, dimmer,
   no color-cycle (that would read as "ready", which it isn't yet) */
.gate-btn.loading{
  opacity:.55;cursor:default;pointer-events:none;
  animation:pulse 1.1s ease-in-out infinite;
}
@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}
/* white -> yellow -> green -> blue -> green -> yellow -> white, then a
   short pause (still white) before looping */
@keyframes gateBtnColorCycle{
  0%{background-color:#FFFFFF}
  13.33%{background-color:var(--lyric-yellow)}
  26.67%{background-color:var(--active-green)}
  40%{background-color:var(--gate-blue)}
  53.33%{background-color:var(--active-green)}
  66.67%{background-color:var(--lyric-yellow)}
  80%,100%{background-color:#FFFFFF}
}
/* karaoke word fade: dark artist -> artist -> yellow -> white */
@keyframes wordFadeBlueGreenWhite{
  0%{color:color-mix(in srgb, var(--artist-color, var(--active-green)) 49%, black)}
  55%{color:var(--artist-color, var(--active-green))}
  80%{color:var(--lyric-yellow)}
  100%{color:var(--fg)}
}
/* karaoke word activation: no fade - the word snaps straight to white the
   instant it becomes active (still scheduled via animation-delay, same
   as the fade above, just a constant color instead of a gradient) */
@keyframes wordActiveWhite{
  0%,100%{color:var(--fg)}
}
/* the word that's currently active also gets a 2px white outline the
   instant it's set, which then fades out to 0% opacity over a fixed 0.5s -
   started at the same delay as wordActiveWhite above (see
   startActiveLineFade() in app.js), but with its own fixed 0.5s duration
   instead of running for the word's whole active span */
@keyframes wordActiveStroke{
  0%{-webkit-text-stroke:2px rgba(255,255,255,1)}
  100%{-webkit-text-stroke:2px rgba(255,255,255,0)}
}
.gate-hint{
  /* 15px below the bottom of the button, at 75% of its old 11px size */
  position:absolute;top:calc(100% + 15px);left:50%;transform:translateX(-50%);
  white-space:nowrap;
  font-size:8.25px;color:var(--fg-soft);letter-spacing:.16em;text-transform:uppercase;
}
/* same white->yellow->green->blue->green->yellow->white->pause cycle as
   the button, just on text color, slower, and staggered per letter (see
   app.js, which sets --i on each .gate-hint-letter) so it sweeps left
   to right instead of changing all at once */
.gate-hint-letter{
  display:inline-block;
  animation:gateTextColorCycle 9s ease-in-out infinite;
  animation-delay:calc(var(--i) * .1s);
}
@keyframes gateTextColorCycle{
  0%{color:#FFFFFF}
  13.33%{color:var(--lyric-yellow)}
  26.67%{color:var(--active-green)}
  40%{color:var(--gate-blue)}
  53.33%{color:var(--active-green)}
  66.67%{color:var(--lyric-yellow)}
  80%,100%{color:#FFFFFF}
}

/* ---------- app shell ---------- */
#app{position:fixed;inset:0;display:flex;flex-direction:column}
.view{
  position:absolute;inset:0;bottom:calc(var(--nav-h) + env(safe-area-inset-bottom));
  display:none;flex-direction:column;
}
.view.active{display:flex}

/* ============================ HOME ============================ */
#view-home{overflow:hidden}
/* solid artist-color wash sat behind the sphere itself (#stage is
   z-index:0, this is -1) - shows through wherever the sphere/pano scene
   doesn't fully cover (transparent canvas clear color, curved-patch edges) */
#pre-sphere-layer{
  position:fixed;inset:0;z-index:-1;pointer-events:none;
  background:color-mix(in srgb, var(--artist-color, var(--active-green)) 85%, transparent);
}
#stage{position:fixed;inset:0;z-index:0;filter:brightness(.75)} /* every 3D background scene (sphere/panorama, tunnel, environments) 25% darker */
/* the 3D environments' screen treatment: instead of the sphere's shader-
   retired: the vertical-grid/VCR overlay is gone - the 3D scenes render
   clean with no scanline treatment */
#grid-overlay{display:none}
/* a plain 10% black wash over every 3D scene (and the intro tunnel) -
   sits above the WebGL stage, below all UI */
#dim-overlay{
  position:fixed;inset:0;z-index:1;pointer-events:none;display:none;
  background:#000000;opacity:.1;
}
body.scene-3d #dim-overlay,
body.gate-active #dim-overlay{display:block}
/* SPHERE: a corner vignette over the video (transparent through the
   middle, sinking to 50% black at the corners) plus a subtle repeating
   1px horizontal VCR-scanline texture on top */
#sphere-vignette{
  position:fixed;inset:0;z-index:1;pointer-events:none;display:none;
  background-image:
    repeating-linear-gradient(to bottom,
      rgba(0,0,0,.15) 0px, rgba(0,0,0,.15) 1px, transparent 1px, transparent 3px),
    radial-gradient(circle at center,
      transparent 0%, transparent 40%, rgba(0,0,0,.5) 100%);
}
body.scene-sphere #sphere-vignette{display:block}
/* retired: the PRISM edge fade is gone - beams keep full brightness out
   to the frame edges */
#prism-fade{display:none}
/* Polaroid road sky: the WebGL canvas clears transparent there, revealing
   this gradient - a little brighter near the horizon, darker up top */
body.scene-road #stage{background:linear-gradient(to top, #302864 0%, #17133e 55%, #0c071d 100%)}
/* Aveluna sky: dark blue low, fading to black at the top */
body.scene-mist #stage{background:linear-gradient(to top, #101a3e 0%, #060a1c 55%, #000000 100%)}
/* ROADS sky: a slight vertical gradient in the back - faintly lighter
   near the horizon, near-black up top */
/* ROADS runs its own fixed blue palette regardless of artist, so its
   horizon gradient stays fixed blue too instead of following
   --artist-color (which would clash with the scene's own blue tones) */
body.scene-beams #stage{background:linear-gradient(to top,
  #123064 0%, #000000 60%)}
/* NATURE horizon: black up top sinking into a modest dark blue (was the
   artist color) - a taller gradient slowly drifting its visible window up
   and down so the horizon itself reads as moving with the scene */
body.scene-check #stage{
  background-image:linear-gradient(to top, #16324f 0%, #000000 60%);
  background-size:100% 200%;
  animation:natureHorizonDrift 30s ease-in-out infinite;
}
@keyframes natureHorizonDrift{
  0%, 100%{background-position:0% 0%}
  50%{background-position:0% 100%}
}
/* BLOCKS background: black sinking into a dark red/blue gradient, the
   same drifting-window trick as NATURE so it visibly moves with the scene */
body.scene-maze #stage{
  background-image:linear-gradient(to top, #2a0a12 0%, #0a1030 50%, #000000 100%);
  background-size:100% 200%;
  animation:mazeGradientDrift 30s ease-in-out infinite;
}
@keyframes mazeGradientDrift{
  0%, 100%{background-position:0% 0%}
  50%{background-position:0% 100%}
}
/* EYES: radial gradient from the middle - artist color (20% dark) at the
   center, fading out to dark grey at the edges */
body.scene-eyes #stage{background:radial-gradient(circle at center,
  color-mix(in srgb, var(--artist-color, #7CFF9E) 80%, black) 0%, #101010 100%)}
/* PRISM: replaces the old 3D background blooms - a moving gradient at
   3.75% visible (50% darker again, was 7.5%), plus a fixed radial
   vignette on top that sinks to almost black toward the corners. Two
   layers, so background-position needs a value per layer - only the
   drifting linear one moves, the vignette stays centered */
body.scene-prism #stage{
  background-image:
    radial-gradient(circle at center, transparent 0%, transparent 40%, rgba(0,0,0,0.92) 100%),
    linear-gradient(to top,
      color-mix(in srgb, var(--artist-color, #7CFF9E) 3.75%, transparent) 0%, transparent 60%);
  background-size:100% 100%, 100% 200%;
  animation:prismGradientDrift 30s ease-in-out infinite;
}
@keyframes prismGradientDrift{
  0%, 100%{background-position:center, 0% 0%}
  50%{background-position:center, 0% 100%}
}
/* RINGS: dark background, black up top sinking into the artist color as
   the primary color at the bottom (55% mix - still dark, not full bright) */
body.scene-rings #stage{background:linear-gradient(to top,
  color-mix(in srgb, var(--artist-color, #7CFF9E) 55%, black) 0%, #000000 60%)}
/* HANDS: a horizon behind the sphere tunnel - artist color low, fading to
   black higher up */
body.scene-hands #stage{background:linear-gradient(to top,
  color-mix(in srgb, var(--artist-color, #7CFF9E) 18%, black) 0%, #000000 69%)}
/* full-screen tint in the current track's artist color, sitting right on
   top of the 3D sphere/background video (z-index:0) but below every
   other overlay/UI layer (z-index:1+) - color set per-track in
   renderMeta() via the --artist-color custom property */
/* radial fade instead of a flat tint - center keeps the original .25
   darkness, fading out toward the edges to 40% less dark (.15) since
   alpha is baked into each gradient stop, opacity itself is no longer
   used (it would darken the whole element uniformly, undoing the fade) */
/* gradient.mp4 overlay: a subtle moving color-grade wash on top of every
   3D scene, always on regardless of which background is active - sits
   right above #stage (z-index:0), below the artist-color tint and all
   other UI (z-index:1+) */
#gradient-overlay{
  position:fixed;inset:0;z-index:1;pointer-events:none;
  width:100%;height:100%;object-fit:cover;
  /* .15 base, +15% then +25% more strength on top of that */
  opacity:.216;
  /* reads as a genuine color-grade wash over the scene, not just a
     translucent video sitting on top of it */
  mix-blend-mode:color;
}
#artist-color-overlay{
  position:fixed;inset:0;z-index:1;pointer-events:none;
  background:radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--artist-color, var(--active-green)) 18%, transparent) 0%,
    color-mix(in srgb, var(--artist-color, var(--active-green)) 10%, transparent) 100%);
  mix-blend-mode:multiply;
}
/* readability scrim behind the whole bottom UI cluster (title/artist,
   transport, duration/volume, side buttons) - sits in front of the 3D
   sphere (#stage, z-index:0) but behind all that UI content (z-index:2+);
   now spans the full viewport height (top to bottom) instead of just
   below the artist photo */
#bg-gradient{
  position:fixed;left:0;right:0;top:0;bottom:0;z-index:1;pointer-events:none;
  /* tinted scrim: the artist's color darkened 40%, fading in toward the
     bottom (was plain black) - still dark enough to keep the UI readable */
  background:linear-gradient(to bottom,
    transparent 0%,
    color-mix(in srgb, color-mix(in srgb, var(--artist-color, var(--active-green)) 60%, #000000) 55%, transparent) 100%);
}
#stage canvas{display:block;width:100%;height:100%}

.home-top{
  position:relative;z-index:2;
  display:flex;align-items:center;justify-content:center;
  margin-top:calc(env(safe-area-inset-top) + 16px);
  padding:0 20px;
}
/* solid wordmark in the current track's artist color (--artist-color,
   set per-track in renderMeta(); falls back to the default green),
   vertically aligned with the audio visualizer (top is set dynamically
   in JS by positionLogo(), which runs alongside positionWaveCanvas());
   sits above #wave-canvas in stacking order (z-index) so the canvas can
   be clipped to leave a gap under it, per drawWaveCanvas() */
.home-top .logo-text{
  font-family:var(--font-display);font-weight:900;font-size:47.817px;
  letter-spacing:-.02em;
  position:absolute;left:50%;transform:translate(-50%, -50%);
  z-index:3;
  animation:logoGreenArtistCycle 4s ease-in-out infinite;
}
/* artist color <-> a 20%-brighter version of it, looping */
@keyframes logoGreenArtistCycle{
  0%,100%{color:var(--artist-color, var(--active-green))}
  50%{color:color-mix(in srgb, var(--artist-color, var(--active-green)) 80%, white)}
}
.pano-btns{
  position:absolute;left:14px;top:14px;
  display:flex;align-items:center;gap:6px;z-index:2;
}
.pano-btns .util-btn svg{width:12.8px;height:12.8px}
/* background-video navigator: next to the scene navigator, same layout */
.util-btn{
  width:30.4px;height:30.4px;border-radius:11.2px;position:relative;
  display:flex;align-items:center;justify-content:center;
  color:var(--artist-color, var(--active-green));border:1px solid var(--artist-color, var(--active-green));
  outline:4px solid transparent;outline-offset:-2px;
  background:rgba(255,255,255,.06);backdrop-filter:blur(8px);
  font-family:var(--font-display);font-weight:900;font-size:15px;
  transition:background-color .3s ease,color .3s ease,border-color .3s ease,border-width .1s ease,box-shadow .3s ease;
  box-shadow:0 0 3px rgba(255,255,255,0);
}
.util-btn::before{
  content:"";position:absolute;inset:0;z-index:0;border-radius:inherit;
  background:var(--hover-gradient);opacity:0;transition:opacity .5s ease;
  pointer-events:none;
}
/* hover: no scaling - background/icon/text fade to solid white + black
   over .2s, and fade back on hover-out (the .danger/remove button keeps
   its own distinct red hover treatment below instead) */
.util-btn:not(.danger):hover{
  background:#ffffff;color:#000000;border-color:#ffffff;
  box-shadow:0 0 3px rgba(255,255,255,.2);
}
/* .danger (delete/remove buttons) - a distinct red warning treatment
   instead of the default white, reused anywhere a destructive action
   button appears (pano remove, delete song, ...) */
.util-btn.danger:hover{color:#000000;outline-color:#FF5C5C;border-color:#FF5C5C}
.util-btn.danger:hover::before{opacity:1;background:#FF5C5C}
.util-btn.danger:active{background:#FF5C5C;border-color:#FF5C5C}
.util-btn:active{background:var(--artist-color, var(--active-green));color:#000000;border-color:var(--artist-color, var(--active-green))}
.util-btn svg{width:14.4px;height:14.4px;position:relative;z-index:1}
.util-btn.spinning{pointer-events:none;opacity:.6}
.util-btn.spinning svg{animation:aqaiSpin 1s linear infinite}
@keyframes aqaiSpin{to{transform:rotate(360deg)}}
.util-btn-wide{width:auto;flex:0 0 auto;gap:4.8px;padding:0 9.6px}
.util-btn-wide span{position:relative;z-index:1;line-height:1;top:1.6px;
  font-family:var(--font-title);font-weight:700;font-size:12.8px;
  letter-spacing:.04em;text-transform:uppercase;white-space:nowrap;
}

/* music-reactive line visualizer - flat 2D canvas sitting between the
   logo and the lyrics viewer (not part of the 3D sphere); top/height are
   set in JS by positionWaveCanvas() since they depend on the live
   layout of both neighbors */
/* extends 10px past the viewport edge on both sides, so the line runs a
   little further left/right than the rest of the row content - width is
   explicit (not left+right auto-stretch) since <canvas> is a replaced
   element and doesn't stretch from left/right offsets the way a <div> does */
#wave-canvas{position:absolute;left:-10px;width:calc(100% + 20px);z-index:2;pointer-events:none;display:block}

/* giant, faint song-title watermark - sweeps slowly all the way across
   the screen, from fully off the right edge to fully off the left edge,
   on every track start (see animateBgTitleWatermark() in app.js),
   vertically anchored to wherever the karaoke carousel currently sits.
   Sits behind the actual lyrics (z-index:1 vs #lyrics' z-index:2).
   Positioned at top:0/left:0 with the actual x/y done entirely via a
   JS-computed translate() (see animateBgTitleWatermark()), so there's
   nothing here depending on this element's own box size except the
   self-centering -50%/-50% baked into that same translate() */
#bg-title-watermark{
  position:fixed;top:0;left:0;z-index:1;pointer-events:none;
  /* plain 'Brice' family (not var(--font-title)'s Condensed variant) at
     weight 600 = Brice-Semi-Bold.woff2 */
  font-family:'Brice';font-weight:600;text-transform:uppercase;
  white-space:nowrap;color:transparent;
  /* filled with the artist color (25% at the top, 0% at the bottom) plus a 1px outline (50%), both
     fading in going up: the fade is one mask over the letters - nothing
     at the baseline (the .125em of descender room under it is the line
     box's own spacing) up to fully opaque at cap height (~.7em above
     the baseline). line-height is tightened to roughly the
     capital height, and the extra top padding gives the mask (which only
     paints inside the element's box) room over the caps' overshoot */
  line-height:.75;
  padding-top:.3em;
  background:color-mix(in srgb, var(--artist-color, var(--active-green)) 25%, transparent);
  -webkit-background-clip:text;background-clip:text;
  -webkit-text-stroke:1px color-mix(in srgb, var(--artist-color, var(--active-green)) 50%, transparent);
  -webkit-mask-image:linear-gradient(to top, transparent .125em, #000 .825em);
  mask-image:linear-gradient(to top, transparent .125em, #000 .825em);
  /* 2200% of the normal song-title size (see .meta .title's own clamp()) */
  font-size:calc(22 * clamp(19.928847px, 6.227765vw, 34.875484px));
  letter-spacing:-.05em; /* -5% of the (huge) font-size */
  transform-origin:50% 100%; /* scale (export slider) keeps the bottom on the baseline */
  transition:transform 160s ease-out;
}

/* the lit-up outline on the big title (see updateScanLine() in app.js): a
   stroke-only copy of the text, revealed only inside a diagonal band
   (--glow-x, set every frame) that sweeps across. The mask box is 700px
   wide so a band tilted 20deg still spans the title's full height. */
#bg-title-watermark::after{
  content:attr(data-text);
  position:absolute;inset:0;padding-top:.3em;
  color:transparent;background:none;
  /* the artist color rotated +90deg in hue */
  -webkit-text-stroke:1px hsl(from var(--artist-color, var(--active-green)) calc(h + 90) s l);
  -webkit-mask-image:linear-gradient(110deg, transparent 34%, #000 50%, transparent 66%);
  mask-image:linear-gradient(110deg, transparent 34%, #000 50%, transparent 66%);
  -webkit-mask-size:700px 100%;mask-size:700px 100%;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:calc(var(--glow-x, -9999px) - 350px) 0;
  mask-position:calc(var(--glow-x, -9999px) - 350px) 0;
}
/* lyrics — 3-row carousel: the real previous/next lines sit 4px above and
   below the active line, all at the same size; only color/opacity marks
   which one is active */
/* 10% of the viewport reserved on each side (so the viewer is always
   exactly 80% wide, whatever the screen size) and centered on both
   axes - top:50%/translateY(-50%) for vertical, left/right:10% (instead
   of a fixed-px padding) for horizontal */
#lyrics{
  position:absolute;left:10%;right:10%;top:50%;transform:translateY(calc(-50% - 80px)) scale(1.5);
  z-index:2;pointer-events:none;
  text-align:center;
  height:calc(clamp(18px,calc(14vw - 10px),70px) * 4.6);overflow:hidden;
  /* very soft, light drop shadow lifting the karaoke text off the 3D scenes */
  filter:drop-shadow(0 6px 36.4px rgba(0,0,0,.42));
}
#lyric-rows{position:relative;width:100%;height:100%}
.lyric-row{
  position:absolute;left:50%;top:50%;width:100%;max-width:100%;
  white-space:nowrap;
  font-family:var(--font-title);font-weight:700;text-transform:uppercase;
  font-size:clamp(18px,calc(14vw - 10px),70px);line-height:calc(1.4em - 20px);
  letter-spacing:-2px;
  /* slow, continuous scale motion - shared by the entrance overshoot
     (130% -> 100%, see layoutLyricRows()) and every later depth change
     (active -> near -> gone), so scaling out moves at the same speed */
  /* translate and scale as separate transitionable properties (not the
     combined transform shorthand) so JS can snap position instantly for
     the "before" rows while still letting their scale/zoom ease out
     slowly - see layoutLyricRows() */
  transition:translate 9.3s ease-in-out, scale 9.3s ease-in-out;
}
.lyric-row .w{
  display:inline-block;margin:0 .12em;
  /* fourth sentence back (no "near"/"far" class - the deepest of the 3
     "before" rows, since upcoming lines are never shown): artist color
     40% darker, solid (not transparent) */
  color:color-mix(in srgb, var(--artist-color, var(--active-green)) 60%, black);opacity:1;
  transition:transform .3s ease;
}
/* second sentence back (the most recent previous sentence): artist color
   20% darker, solid (not transparent) */
.lyric-row.near .w{color:color-mix(in srgb, var(--artist-color, var(--active-green)) 80%, black)}
/* third sentence back: artist color 30% darker, solid (not transparent) */
.lyric-row.far .w{color:color-mix(in srgb, var(--artist-color, var(--active-green)) 70%, black)}
.lyric-row.active-row .w{color:color-mix(in srgb, color-mix(in srgb, var(--artist-color, var(--active-green)) 70%, black) 75%, white);opacity:1}
@media (max-width:640px){
  #lyrics{transform:translateY(calc(-50% - 110px)) scale(1.5)}
}

/* full lyrics overlay */
/* a floating vertical card over the Home screen: sits 40px below the
   logo and stops above the player/title, positioned in JS via
   positionLyricsFull() since it depends on the logo/player's live layout */
/* full-screen 50% black scrim behind the card - dims the whole app
   while the lyrics overview is open */
#lyrics-full-backdrop{
  position:fixed;inset:0;z-index:55;display:none;
  background:rgba(0,0,0,.5);
}
#lyrics-full-backdrop.open{display:block}
/* width is no longer a fixed percentage - set per-track in JS by
   buildFullLyrics() to fit the longest lyric line plus 20% breathing
   room on each side, so the card can overlap/exceed the screen when a
   line is wide instead of shrinking every line down to a fixed box */
#lyrics-full{
  position:fixed;left:50%;transform:translateX(-50%);z-index:56;
  display:none;flex-direction:column;
  background:#ffffff;border:1px solid var(--line);border-radius:20px;
  overflow:hidden;
  box-shadow:0 20px 60px rgba(0,0,0,.5);
}
#lyrics-full.open{display:flex}
.lf-close{
  position:absolute;right:13px;top:13px;z-index:2;
  width:30.4px;height:30.4px;border-radius:11.2px;
  display:flex;align-items:center;justify-content:center;
  color:#000000;border:1.5px solid #aaaaaa;
  outline:4px solid transparent;outline-offset:-2px;
  background:#ffffff;backdrop-filter:blur(8px);
  transition:transform .1s,color .5s ease,outline-color .5s ease;
}
.lf-close::before{
  content:"";position:absolute;inset:0;z-index:0;border-radius:inherit;
  background:var(--hover-gradient);opacity:0;transition:opacity .5s ease;
  pointer-events:none;
}
/* hover: solid black button with a white X icon */
.lf-close:hover{color:#ffffff;background:#000000;border-color:#000000;outline-color:transparent}
.lf-close:hover::before{opacity:0}
.lf-close:active{background:var(--artist-color, var(--active-green));color:#000000;border-color:var(--artist-color, var(--active-green))}
.lf-close svg{width:14.4px;height:14.4px;position:relative;z-index:1}
.lf-meta{
  padding:40px 22px 0 40px;text-align:left;
  display:flex;align-items:center;flex-wrap:wrap;gap:8px;
}
.lf-title{font-family:var(--font-title);font-weight:700;font-size:31.2px;text-transform:uppercase;color:color-mix(in srgb, var(--artist-color, var(--active-green)) 75%, black)}
.lf-by{font-family:var(--font-title);font-weight:400;font-size:21.84px;text-transform:uppercase;color:#666666}
.lf-sub{font-family:var(--font-title);font-weight:400;font-size:31.2px;text-transform:uppercase;color:color-mix(in srgb, var(--artist-color, var(--active-green)) 75%, black)}
/* rename-in-place on the overlay itself - same pattern as .title-edit-input
   on the main player (see beginEditTitle()/commitEditTitle()), sized to
   match .lf-title/.lf-sub instead of the smaller .meta pill text */
.lf-edit-input{
  display:none;
  font-family:var(--font-title);font-weight:700;font-size:31.2px;text-transform:uppercase;
  min-width:0;width:min(50vw,360px);
  color:#000000;
  background:transparent;border:none;border-bottom:1.5px solid var(--artist-color, var(--active-green));outline:none;
  padding:0;
}
.lf-meta.editing-title #lf-title{display:none}
.lf-meta.editing-title #lf-title-input{display:inline-block}
.lf-meta.editing-title #lf-btn-edit-title{display:none}
.lf-meta.editing-artist #lf-folder{display:none}
.lf-meta.editing-artist #lf-folder-input{display:inline-block}
.lf-meta.editing-artist #lf-btn-edit-artist{display:none}
/* owner-only cluster (rename pencils + delete) - hidden entirely unless
   the server says this request is local AND the gate password wasn't the
   restricted one (see EDITABLE/updateEditControlsVisibility() in app.js) */
.lf-edit-btns{display:flex;align-items:center;gap:6px;margin-left:auto;position:relative}
.lf-edit-btns .util-btn{width:26px;height:26px;border-radius:9px}
.lf-edit-btns .util-btn svg{width:13px;height:13px}
/* persistent pressed state (aria-pressed, not just :active/:hover) for
   toggles like #lf-btn-edit-lyrics */
.lf-edit-btns .util-btn[aria-pressed="true"]{
  background:var(--artist-color, var(--active-green));color:#000000;
  border-color:var(--artist-color, var(--active-green));
}
/* same persistent pressed look for the meta row's own flag toggle */
#btn-flag-lyrics[aria-pressed="true"]{
  background:var(--artist-color, var(--active-green));color:#000000;
  border-color:var(--artist-color, var(--active-green));
}
/* a dashed outline on every sentence while lyrics-edit mode is on, so
   click-to-edit reads as an active affordance instead of a hidden one */
#lyrics-full.lf-editing-lyrics .lyric-row{
  outline:1px dashed color-mix(in srgb, var(--artist-color, var(--active-green)) 55%, transparent);
  outline-offset:4px;border-radius:4px;
}
/* the "move to another artist" popover - a simple flyout list of every
   other artist, one button each */
.lf-artist-picker{
  display:none;position:absolute;top:calc(100% + 8px);right:0;z-index:20;
  min-width:160px;max-height:240px;overflow-y:auto;padding:6px;
  background:rgba(20,20,20,.92);backdrop-filter:blur(10px);
  border:1px solid var(--artist-color, var(--active-green));border-radius:10px;
  box-shadow:0 8px 24px rgba(0,0,0,.4);
}
.lf-artist-picker.open{display:flex;flex-direction:column;gap:2px}
.lf-artist-picker button{
  all:unset;cursor:pointer;padding:7px 10px;border-radius:6px;
  font-family:var(--font-display);font-size:12px;color:#fff;white-space:nowrap;
}
.lf-artist-picker button:hover{background:var(--artist-color, var(--active-green));color:#000}
#lf-list{
  flex:1;overflow-y:auto;padding:20px 8px 20px 38px; /* text nudged 2px left */
  /* 20px top padding keeps the first line (tight line-height) from being
     clipped at the scroll edge, and starts it ~20px below the title;
     40px of space at the bottom of the card */
  margin:0 0 40px 0;
  -webkit-overflow-scrolling:touch;
  text-align:left;
  display:flex;flex-direction:column;gap:12.5px; /* half the previous 25px cellspace between sentences */
}
/* scrollbar: a 15px thumb, horizontally centred under the close button
   (button at right:13px, width 30.4px → centre 28.2px in from the card
   edge; a 20.7px transparent right border with padding-box clip keeps the
   15px thumb centred on that same point). Hidden until the user hovers the
   list or scrolls. */
#lf-list::-webkit-scrollbar{width:35.7px}
#lf-list::-webkit-scrollbar-track{background:transparent}
#lf-list::-webkit-scrollbar-thumb{
  background:transparent;border-radius:7.5px;
  border:none;border-right:20.7px solid transparent;background-clip:padding-box;
  transition:background .2s ease;
}
#lf-list:hover::-webkit-scrollbar-thumb,
#lf-list.scrolling::-webkit-scrollbar-thumb{background:#d9d9d9}
.lf-row{
  font-family:var(--font-body);font-weight:300;text-transform:none;white-space:normal; /* Brice Light (was Condensed regular) */
  /* the old single-line-only line-height (calc(0.3375em - 1.25px), well
     under the font's own glyph height) made a wrapped second line overlap
     the first once white-space allowed wrapping - 1.3em gives a wrapped row
     room to breathe while still reading tight on the common single-line case;
     5px shaved back off on top of that */
  font-size:clamp(10.5px,calc(4.4vw - 9px),15.2px);line-height:calc(1.3em - 5px); /* 9px smaller than the original 24.2px */
  padding:1px 0;
  color:#000000;
  transition:color .25s ease;
  /* a fully-cleared line (see commitEditLyricRow()) has no text content
     left, which would otherwise collapse the row to ~0 height - keeping a
     minimum height means it stays visible/clickable to edit again */
  min-height:calc(1.3em - 5px);
}
.lf-row .w{
  display:inline-block;margin:0 .12em;
  color:inherit;
}
.lf-row.current{color:var(--lyric-active)}
/* editable lyrics (local-only, see EDITABLE in app.js): rows get a subtle
   hover affordance, click-to-edit swaps the row's text for a plain input
   sized/styled to match (see beginEditLyricRow()/commitEditLyricRow()) */
#lf-list.editable .lf-row{cursor:pointer;border-radius:6px;transition:background-color .15s ease,color .25s ease}
#lf-list.editable .lf-row:hover{background:rgba(0,0,0,.06)}
.lf-row-edit-input{
  font:inherit;color:inherit;text-transform:inherit;
  width:100%;background:transparent;border:none;border-bottom:1.5px solid var(--artist-color, var(--active-green));outline:none;
  padding:1px 0;
}
.lf-gap{height:22px}

/* bottom player block */
.player{
  position:absolute;left:0;right:0;bottom:45px;z-index:3;
  padding:26px 20px 14px;
}

/* artist photo placeholder - profilepic.png doubles as the mask shape
   for the per-artist photos (artist_1.png etc) once those are wired up;
   for now it's just shown directly, centered above the title/artist row.
   z-index:0 (lower than .meta-row's 3) keeps it behind the song-title
   pill if the two ever overlap; top is set dynamically in JS by
   positionArtistPhoto() - 60px above the controls row */
/* explicit aspect-ratio (profilepic.png/artist_N.png's fixed 638:497 oval
   shape) instead of leaving the height to the <img>'s own natural size -
   the wrap used to sit at 0 height until the photo actually finished
   loading, so positionArtistPhoto()/positionWaveCanvas() measured a
   collapsed box on the very first track (pinning the title/artist pill
   down near the controls row - it only "reset" to its right place once
   the image loaded before the *next* track's positioning pass) */
/* per-artist 3D creature (fox for now) - sits on TOP of the solid-colour
   photo disc (z-index:1, above the photo's 0) but stays under the title
   pill (.meta-row's z-index:3). Sized/positioned in JS by
   positionFoxCanvas(), vertically centred on .meta-row rather than the
   photo (see positionWaveCanvas) */
/* 3px outline around the creature itself, tinted the artist's own color
   (70% darker) instead of flat black: four 0-blur drop-shadows (up/down/
   left/right) traced from the canvas's own alpha channel - works on the
   exact rendered silhouette regardless of the model's own mesh topology
   (unlike a 3D "inflated shell" outline, which doesn't self-occlude
   cleanly on these low-poly, non-watertight, skinned models) */
#fox-3d-canvas{
  position:fixed;left:50%;z-index:1;pointer-events:none;transform:translateX(-50%);
  --outline-color:color-mix(in srgb, var(--artist-color, var(--active-green)) 50%, black);
  filter:
    drop-shadow(3px 0 0 var(--outline-color))
    drop-shadow(-3px 0 0 var(--outline-color))
    drop-shadow(0 3px 0 var(--outline-color))
    drop-shadow(0 -3px 0 var(--outline-color));
}
.artist-photo-wrap{position:relative;z-index:0;width:138.32775px;height:97.982156px;margin:0 auto -5px;transform:translateY(-393px) scale(1.32)}
/* soft glow behind the photo, tinted the current track's artist colour */
.artist-photo-wrap::before{
  content:"";position:absolute;left:-16%;top:50%;width:132%;aspect-ratio:1/1;z-index:0;
  border-radius:50%;pointer-events:none;
  background:radial-gradient(circle,
    color-mix(in srgb, var(--artist-color, var(--active-green)) 78%, transparent) 0%,
    transparent 68%);
  filter:blur(16px);
  /* purely visual - sizing/moving the circle this way (rather than via
     the parent's own position/transform) doesn't touch .artist-photo-wrap's
     own bounding rect, so it doesn't drag the creature/visualiser/title
     (all of which anchor off that live rect) along with it */
  transform:translateY(-50%) scale(.825);
}
/* 2px stroke around the disc, in the artist's own colour: the same
   profilepic.png silhouette, filled solid and sized 2px larger on every
   side, sitting just behind .artist-photo-fill so only a 2px rim of it
   shows around the edge - same trick as the glow above, just solid and
   tighter to the shape */
.artist-photo-wrap::after{
  content:"";position:absolute;left:-2px;top:50%;width:calc(100% + 4px);aspect-ratio:1/1;z-index:1;pointer-events:none;
  border-radius:50%;
  background:color-mix(in srgb, var(--artist-color, var(--active-green)) 50%, black);
  transform:translateY(-50%) scale(.825);
}
.artist-photo{
  display:block;width:100%;height:auto;position:relative;z-index:1;
}
/* replaces the actual photo with a solid disc in the artist's own colour
   (same profilepic.png oval mask as the rim stroke above, just filling
   the whole shape instead of a thin outline) - the fox now reads as the
   "face" here instead of a person, this is just its colour backdrop */
.artist-photo-fill{
  position:absolute;left:0;top:50%;width:100%;aspect-ratio:1/1;z-index:2;pointer-events:none;
  /* a true circle now (was the oval profilepic.png mask): as wide as the
     old oval, centered on the same spot, so it grows upward/downward
     instead of moving - black, tinted 20% toward the artist's color */
  border-radius:50%;
  background:color-mix(in srgb, var(--artist-color, var(--active-green)) 20%, #000000);
  transform:translateY(-50%) scale(.825);
}
/* the real photo underneath is fully replaced by the solid colour fill
   above it now (no more real people "pretending" to be the band) -
   still loaded/measured by JS (positionArtistPhoto() etc.), just never
   shown */
.artist-photo{visibility:hidden}
/* artist_N.png photos are plain rectangular RGB images (no alpha of
   their own) - clipped into profilepic.png's oval shape via mask so they
   read the same as the profilepic.png placeholder does on its own;
   profilepic.png is the exact same pixel dimensions as the artist_N.png
   files, hence the 100% 100% (no need for contain/cover math) */
.artist-photo.masked{
  -webkit-mask-image:url('assets/profilepic.png');
  -webkit-mask-size:100% 100%;
  -webkit-mask-repeat:no-repeat;
  -webkit-mask-position:center;
  mask-image:url('assets/profilepic.png');
  mask-size:100% 100%;
  mask-repeat:no-repeat;
  mask-position:center;
}
/* title/by/artist: taken out of the normal .player flow entirely and
   fixed in place instead, vertically centered (translateY(-50%)) on a
   Y set in JS by positionWaveCanvas() - together with the visualizer,
   both anchored 25px below the AQAI logo's own bottom edge */
.meta-row{
  position:fixed;left:0;right:0;top:0;z-index:3;
  transform:translateY(-50%);
  display:flex;align-items:center;justify-content:center;
  text-align:center;
  /* .meta's own scale(1.37) transform (below) enlarges its actual
     clickable/hit-tested area well past what the visible text suggests -
     on short/narrow viewports that invisible overflow can reach down far
     enough to steal clicks from the seek bar/volume slider underneath.
     Disabled here and re-enabled only on the one thing inside that's
     actually meant to be clickable (the artist name - see .meta .sub) */
  pointer-events:none;
}
/* pill in the artist's color behind the song/by/artist text, black text
   for contrast; scaled to 75% of its previous size overall, with
   trimmed left/right padding (10px/12px, was 22px each) */
.meta{
  position:relative;z-index:1;
  min-width:0;max-width:100%;
  display:flex;align-items:center;justify-content:center;gap:8px;
  border-radius:200px;
  padding:6.05px 13.2px 6.05px 11px; /* +10% */
  color:#000000;
  transform:scale(1.3687875); /* 15% bigger than the former 1.19025 */
  box-shadow:none; /* pill temporarily removed - see background below */
  background:none;
}
/* title on its own line, "by <artist>" stacked 5px below it */
.meta > .meta-text{
  display:flex;flex-direction:column;align-items:center;gap:0px;
  min-width:0;
  position:relative;top:0px;
  /* single source of truth for how far the title (and, tracking it, the
     by/artist line 8px below) has been shifted from its normal flow
     position - keeps them a fixed 8px apart regardless of this value */
  --title-shift:-65px;
}
.meta-sub-row{display:flex;align-items:center;gap:7px;min-width:0;margin-top:8px;transform:translateY(calc(var(--title-shift) - 10px));}
/* edit buttons pulled out of the centered title group entirely and
   pinned to the right edge of the screen instead, in their own fixed
   column - .meta-row itself stays a normal flex row (just .meta-text
   now, centered) alongside this */
.meta-actions{
  position:fixed;z-index:10;
  top:calc(env(safe-area-inset-top) + 14px);right:14px;
  display:flex;flex-direction:column;align-items:center;gap:8px;
}
#video-export-panel{
  display:none;flex-direction:column;gap:6px;
  position:fixed;z-index:20;
  top:calc(env(safe-area-inset-top) + 14px);right:14px;
  width:min(260px,72vw);
  padding:10px 12px;border-radius:12px;
  background:rgba(10,10,10,.72);backdrop-filter:blur(10px);
  box-shadow:0 4px 16px rgba(0,0,0,.4);
}
#video-export-panel.show{display:flex}
.video-export-title{
  font-family:var(--font-title);font-weight:700;font-size:10px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--fg-soft);
}
.video-export-row{
  display:flex;align-items:center;gap:8px;
  font-family:var(--font-body);font-size:11px;color:var(--fg-soft);
}
.video-export-label{width:92px;flex:none}
.video-export-preview{
  flex:none;font-family:var(--font-body);font-weight:700;font-size:10px;
  color:var(--fg-soft);background:transparent;border:1px solid var(--fg-soft);
  border-radius:999px;padding:3px 8px;cursor:pointer;
}
.video-export-preview:hover{color:var(--artist-color, var(--active-green));border-color:var(--artist-color, var(--active-green))}
.video-export-bar{
  flex:1;height:5px;border-radius:3px;background:rgba(255,255,255,.14);overflow:hidden;
}
.video-export-fill{
  height:100%;width:0%;border-radius:3px;
  background:var(--artist-color, var(--active-green));
  transition:width .25s ease;
}
.video-export-link{
  flex:none;display:none;font-weight:700;color:var(--artist-color, var(--active-green));
  text-decoration:none;font-size:11px;
}
.video-export-row.done .video-export-link{display:inline}
.video-export-row.done .video-export-bar{display:none}
.video-export-row.error .video-export-label{color:#FF5C5C}
/* owner-only 3D creature tuner - same floating-panel look as
   #video-export-panel, pinned to the opposite (top-left) corner so the
   two never overlap */
#creature-tune-panel{
  display:none;flex-direction:column;gap:8px;
  position:fixed;z-index:20;
  top:calc(env(safe-area-inset-top) + 14px);left:14px;
  width:min(240px,72vw);
  padding:10px 12px;border-radius:12px;
  background:rgba(10,10,10,.72);backdrop-filter:blur(10px);
  box-shadow:0 4px 16px rgba(0,0,0,.4);
}
#creature-tune-panel.show{display:flex}
.ct-row{
  display:grid;grid-template-columns:64px 1fr;align-items:center;gap:6px;
  font-family:var(--font-body);font-size:11px;color:var(--fg-soft);
}
.ct-label{flex:none}
.ct-row-select select{
  width:100%;background:rgba(255,255,255,.08);color:var(--fg);
  border:1px solid var(--fg-soft);border-radius:6px;padding:3px 4px;
  font-family:var(--font-body);font-size:11px;
}
.ct-hint{
  font-family:var(--font-body);font-size:10px;color:var(--fg-faint);
  line-height:1.4;margin:0;
}
.ct-readout{
  font-family:var(--font-body);font-size:11px;color:var(--fg-soft);
  font-variant-numeric:tabular-nums;line-height:1.5;margin:0;
  white-space:pre-line;
}
.ct-actions{display:flex;gap:8px;margin-top:2px}
.ct-actions .video-export-preview{flex:1;text-align:center}
/* owner-only artist color picker - same floating-panel look as the other
   two, offset further down the right edge so it doesn't stack on top of
   #video-export-panel (also right-anchored, top) */
#color-tune-panel{
  display:none;flex-direction:column;gap:8px;
  position:fixed;z-index:20;
  top:calc(env(safe-area-inset-top) + 230px);right:14px;
  width:min(240px,72vw);
  padding:10px 12px;border-radius:12px;
  background:rgba(10,10,10,.72);backdrop-filter:blur(10px);
  box-shadow:0 4px 16px rgba(0,0,0,.4);
}
#color-tune-panel.show{display:flex}
#cp-swatch{
  flex:none;width:32px;height:32px;border-radius:8px;
  border:1px solid rgba(255,255,255,.3);
  background:var(--cp-color, var(--artist-color, var(--active-green)));
}
#color-tune-panel #cp-input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
/* while the tuner is open, the creature stays exactly where it's
   normally positioned in the live layout (positionFoxCanvas()) - the
   canvas just becomes interactive (drag/scroll/shift-drag) in place,
   with a faint outline so it's clear it's now editable */
#fox-3d-canvas.tuning{
  z-index:15;pointer-events:auto;cursor:grab;
  outline:2px dashed rgba(255,255,255,.35);outline-offset:-2px;
  border-radius:16px;
}
#fox-3d-canvas.tuning:active{cursor:grabbing}
/* same artist-color <-> 20%-brighter cycle as the logo, just on
   background-color instead of text color */
@keyframes bgArtistBrightCycle{
  /* pill background 40% lighter (mixed with 40% white), still pulsing brighter */
  0%,100%{background-color:color-mix(in srgb, var(--artist-color, var(--active-green)) 60%, white)}
  50%{background-color:color-mix(in srgb, var(--artist-color, var(--active-green)) 45%, white)}
}
.meta > *{vertical-align:middle;position:relative;top:3px}
.meta .title{
  position:relative;z-index:0;
  font-family:var(--font-title);font-weight:700;font-size:clamp(19.928847px,6.227765vw,34.875484px);
  word-spacing:.15em;min-width:0;text-transform:uppercase;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  color:#ffffff;
  text-shadow:0 0 5.75px rgba(0,0,0,.15);
  /* overflow:hidden (for the ellipsis above) clips at the padding edge,
     which was cutting the shadow's 20px blur off right at the glyphs -
     padding+matching negative margin gives the shadow room to paint
     without shifting any surrounding layout (title width/position,
     the 0px gap to the "by" line below, all unchanged) */
  padding:40px;margin:-40px;
  transform:translateY(var(--title-shift));
}
/* title text is split into one span per letter (see renderScaledTitle()
   in app.js) so each one can carry its own font-size - 100% at the first
   and last letter, easing up to 110% at the middle of the string */
.title-fill{position:relative;z-index:1}
/* the stroke lives on a second, stroke-only copy of the same per-letter
   spans (cloned 1:1 by renderMeta() in app.js, so it always matches the
   fill layer's sizing exactly) sat directly behind the real one -
   -webkit-text-stroke alone always straddles the glyph edge (half paints
   over the fill, half outside it); doubling the width here and hiding it
   behind the real fill leaves only that outer half showing, which reads
   as a clean 2px stroke entirely outside the letters */
.title-stroke{
  position:absolute;inset:40px;
  z-index:0;
  color:transparent;
  -webkit-text-stroke:4px var(--artist-color, var(--active-green));
}
.meta .by{
  position:relative;z-index:0;
  flex:0 0 auto;font-family:var(--font-title);font-weight:400;font-size:clamp(6.6241125px,2.06955vw,11.5902px);
  text-transform:uppercase;color:var(--artist-color, var(--active-green));
}
/* same outside-only stroke trick as .meta .title::before - a second,
   stroke-only copy sat directly behind the real (transparent-fill) text,
   doubled width so only the outer half (1px visible) shows */
.meta .by::before{
  content:"BY";
  position:absolute;inset:0;
  z-index:-1;
  color:transparent;
  -webkit-text-stroke:2px color-mix(in srgb, var(--artist-color, var(--active-green)) 20%, transparent);
}
.meta .sub{
  position:relative;z-index:0;
  flex:0 0 auto;font-family:var(--font-title);font-weight:400;font-size:clamp(12.7544625px,3.98576953125vw,22.320309375px);
  word-spacing:.15em;text-transform:uppercase;
  white-space:nowrap;
  color:var(--artist-color, var(--active-green));
  cursor:pointer;
  pointer-events:auto; /* .meta-row disables pointer-events - this is the one thing in it that's actually clickable */
}
.meta .sub::before{
  content:attr(data-text);
  position:absolute;inset:0;
  z-index:-1;
  color:transparent;
  -webkit-text-stroke:2px color-mix(in srgb, var(--artist-color, var(--active-green)) 20%, transparent);
}
/* song title/artist: artist color -> white -> artist color, looping */
@keyframes metaGreenWhiteCycle{
  0%,100%{color:var(--artist-color, var(--active-green))}
  50%{color:var(--fg)}
}
/* rename-in-place: clicking a pencil swaps the static title/artist span
   for its input (same type styling so nothing visually jumps), typing
   and then blurring/Enter saves it - see beginEditTitle()/commitEditTitle()
   and beginEditArtist()/commitEditArtist() */
.title-edit-input{
  display:none;
  font-family:var(--font-title);font-weight:700;font-size:clamp(17.00595px,5.314359375vw,29.7604125px);letter-spacing:.02em;
  word-spacing:.15em;min-width:0;width:min(60vw,420px);
  color:#eeeeee;
  background:transparent;border:none;border-bottom:1.5px solid var(--artist-color, var(--active-green));outline:none;
  padding:0;
  pointer-events:auto; /* .meta-row disables pointer-events - these owner-only edit fields still need to be typeable */
}
.meta-row.editing-title #m-title{display:none}
.meta-row.editing-title #m-title-input{display:inline-block}
.meta-row.editing-title #btn-edit-title{display:none}
.meta-row.editing-artist #m-folder{display:none}
.meta-row.editing-artist #m-folder-input{display:inline-block}
.meta-row.editing-artist #btn-edit-artist{display:none}
/* quick fade in on hover, slower 1s fade back to the original look on
   hover-out (the :hover rule's own faster transition wins while
   hovering; this slower one only applies once the pointer leaves) */
#btn-edit-title,#btn-edit-artist{transition:background-color 1s ease,color 1s ease,border-color 1s ease,box-shadow 1s ease}
#btn-edit-title:hover,#btn-edit-artist:hover{transition:background-color .3s ease,color .3s ease,border-color .3s ease,box-shadow .3s ease}
/* rename buttons are shown (edit song title / edit artist name) */
/* transport (left) · progress/volume (middle) · lyrics/download/share/etc
   (right), sitting together as one centered cluster with exactly 40px
   between each group (not spread to the row's full width) */
/* one continuous "single line" tier all the way from full width down to
   the 650px breakpoint below - controls-row stays a centered 80%-wide
   cluster throughout, and .bar-row (duration/volume) fluidly narrows
   with it via max-width:75% (a percentage of controls-row's own width,
   so it shrinks smoothly as the viewport does); there is no separate
   fixed-width tier in between anymore, only this and the 650px stack */
.controls-row{
  position:relative;top:25px;
  display:flex;align-items:center;justify-content:center;flex-wrap:wrap;
  gap:25px;min-height:44px;margin-bottom:14px;
  width:80%;margin-left:auto;margin-right:auto;
}
/* base .bar-row rule - MUST stay above the responsive overrides below
   (their equal-specificity .bar-row rules only win by coming later in
   the file; this one used to live far below them and silently clobbered
   every breakpoint's override) */
.bar-row{
  display:flex;align-items:center;justify-content:center;gap:10px;
  flex:1 1 auto;max-width:75%;min-width:0;
  /* only meaningful while it shares a row with .transport/.side-btns (not
     wrapped to its own line) - matches their -15px so all three stay
     vertically aligned; cancelled below once it wraps onto its own row */
  position:relative;top:-15px;
}
.transport{display:flex;align-items:center;gap:8px;position:relative;top:-15px}
/* random-song button: 2x the usual .ctl size, fully round, and 40px clear
   of its neighbour (8px from the shared .transport gap + 32px margin) */
/* 75% of the previous 2x size (67.2px -> 50.4px) - pulses and cycles
   through the same colors as the "TAP TO LISTEN" gate button */
#c-random{
  width:52.92px;height:52.92px;border-radius:50%;margin-right:32px; /* 5% bigger */
  border:none;color:#000000;
  animation:pulse 2.4s ease-in-out infinite, gateBtnColorCycle 4s ease-in-out infinite;
}
#c-random svg{width:22.8px;height:22.8px}
.transport-center{display:flex;align-items:center;gap:8px}
.ctl.big-play{width:44px;height:44px;border-radius:15px}
.ctl.big-play svg{width:20px;height:20px}
/* now a direct sibling of .transport/.bar-row in .controls-row, so the
   40px gap from the volume slider comes for free via .controls-row's own
   gap - no extra margin needed */
.side-btns{display:flex;align-items:center;gap:10px;position:relative;top:-15px}
.mobile-more-btn{display:none;flex:0 0 auto}

/* wide layout (above the 650px stack): the controls row spans the full
   width as four columns - [left gutter] [prev/play/next] [progress+volume]
   [right gutter]. The shuffle button is centered inside the left gutter
   (halfway between the screen's left edge and the prev/play/next
   controls) and Lyrics/download/share are centered inside the right one
   (halfway between the volume slider and the screen's right edge).
   .transport becomes display:contents so shuffle and prev/play/next can be
   placed in the grid individually. */
@media (min-width:651px){
  .controls-row{
    display:grid;grid-template-columns:1fr auto minmax(0,clamp(220px,calc(80% - 426px),60%)) 1fr; /* middle column reproduces the progress bar's old width (80% of the row minus the fixed-size controls) */
    justify-content:normal;column-gap:0;
    /* .player has 20px side padding - pull the row out to the true screen edges */
    width:calc(100% + 40px);margin-left:-20px;margin-right:-20px;
  }
  .transport{display:contents}
  #c-random{grid-column:1;justify-self:center;margin-right:0;position:relative;top:-15px}
  .transport-center{grid-column:2;position:relative;top:-15px}
  .bar-row{grid-column:3;margin-left:25px;max-width:none}
  .side-btns{grid-column:4;justify-self:center}
}
/* Lyrics button: icon only (same square size as download/share) */
#btn-fulllyrics{width:30.4px;padding:0}
#btn-fulllyrics span{display:none}

/* narrower still: not enough room for transport + duration/volume side by
   side, so the duration/volume block (with Lyrics/download/share riding
   along with it) drops to its own row, sitting between the transport
   controls above and the bottom tab bar below, instead of squeezing
   everything onto one line */
@media (max-width:650px){
  .controls-row{flex-wrap:wrap;row-gap:12px}
  /* the duration/volume bar is what actually drops to its own row here;
     transport and the side buttons keep sharing the row above it,
     side-btns sitting right of the transport controls */
  .transport{order:1}
  .side-btns{order:2;margin-left:20px}
  .bar-row{order:3;flex-basis:100%;width:100%;max-width:100%;flex-wrap:wrap;justify-content:center;top:0}
  /* same size as the plain icon buttons (download/share) instead of the
     wider text pill, so all three side buttons match */
  #btn-fulllyrics{width:30.4px;padding:0}
  #btn-fulllyrics span{display:none}
  /* 80% of the full-size photo (167.0625px * 1.32 scale) */
  .artist-photo-wrap{transform:translateY(-393px) scale(1.056)}
}

/* true mobile: exactly 15px from the actual screen edge (not the wider
   80%-centered cluster used above), on both the transport row and the
   duration/volume row. Transport becomes three zones spanning the full
   width - shuffle pinned left, prev/play/next centred, the "..." button
   pinned right - and side-btns (Lyrics/download/share) hide behind that
   "..." button instead of sitting inline, only appearing (as their own
   centred row) once #btn-more toggles #side-btns' .open class. */
@media (max-width:480px){
  /* nav tabs (Player/Songs/AQAI) height at this breakpoint: 30% shorter
     than the base 56.64px, then +15px - --nav-h also drives the reserved
     space at the bottom of every view and the toast's resting position,
     so both resize to match instead of leaving a gap/overlap */
  :root{--nav-h:54.648px}
  /* song-title outside stroke, 2px thinner at this breakpoint (doubled
     width convention - see .title-stroke - so this is 1px less visible) */
  .title-stroke{-webkit-text-stroke-width:2px}
  /* volume slider removed at this breakpoint entirely */
  #c-volume{display:none}
  /* current-position/total-duration time labels either side of the seek
     bar removed at this breakpoint entirely */
  #t-cur,#t-tot{display:none}
  /* song-position bar (track + fill) and its round knob both 2x their
     usual height (knob's width scales along with it, so it stays round) */
  #bar::before,#bar-fill{height:39.68px;border-radius:19.84px}
  #bar{margin-left:10%;margin-right:10%}
  #bar-knob{width:39.68px;height:39.68px}
  .player{padding-left:15px;padding-right:15px}
  .controls-row{width:100%;margin-left:0;margin-right:0}
  /* bleeds past .player's own 15px side padding, so the position/volume
     row alone reaches the true screen edge (transport row above keeps it) */
  .bar-row{width:calc(100% + 30px);margin:0 -15px;padding:0}
  .transport{width:100%;justify-content:space-between;gap:0;position:relative}
  /* prev/play/next always dead-centre in .transport, independent of
     whatever width the random/more buttons on either side take up -
     taken out of the space-between flow and centred absolutely instead */
  .transport-center{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%)}
  #c-random{transform:scale(.9);margin-left:5%}
  .mobile-more-btn{display:flex;margin-right:5%}
  /* popover anchored above the "..." button (bottom edge of .controls-row,
     which is where the button itself sits, right:0 lining up with its
     right edge) instead of reflowing into its own row below the bar-row -
     that used to land it two rows down from the button that opened it */
  /* bottom:100% (flush against the "..." button, no dead gap) so the
     mouse can travel straight from the button into the popover without
     crossing empty space and losing :hover along the way */
  .side-btns{
    display:none;flex-direction:column;gap:10px;position:absolute;top:auto;right:0;bottom:100%;z-index:5;
    background:var(--nav-bg);backdrop-filter:blur(10px);
    padding:12px;border-radius:18px;box-shadow:0 8px 24px rgba(0,0,0,.4);
  }
  .side-btns.open{display:flex}
  /* white pill while its popover is open, matching the "active" look the
     other toggle-style controls (fullscreen, playing) use elsewhere */
  .mobile-more-btn.active{background:#ffffff;color:#000000;border-color:#ffffff}
  /* icon only at this breakpoint, same footprint as download/share */
  #btn-fulllyrics{width:30.4px;padding:0}
  #btn-fulllyrics span{display:none}
  /* bigger targets inside the popover specifically - easier to land a
     tap/click on the one you want (id + class beats the plain-id rule
     above, so this wins despite coming after it in source) */
  .side-btns .util-btn, .side-btns #btn-fulllyrics{width:44px;height:44px;border-radius:15px;padding:0}
  .side-btns .util-btn svg{width:19px;height:19px}
}

.ctl{
  flex:0 0 auto;width:33.6px;height:33.6px;border-radius:11.2px;position:relative;
  display:flex;align-items:center;justify-content:center;
  color:var(--artist-color, var(--active-green));border:1px solid var(--artist-color, var(--active-green));
  background:rgba(255,255,255,.06);backdrop-filter:blur(8px);
  outline:4px solid transparent;outline-offset:-2px;
  transition:background-color .3s ease,color .3s ease,border-color .3s ease,box-shadow .3s ease,transform .15s ease-out;
  box-shadow:0 0 3px rgba(255,255,255,0);
}
.ctl:hover{transform:scale(1.15)}
/* #c-random's own pulse keyframe animates transform too, which would
   otherwise fight this hover scale every frame - pause it while hovered
   so the 100% -> 115% scale reads clean, then it picks back up on
   hover-out (mid-cycle, not reset) */
#c-random:hover{animation-play-state:paused}
.ctl::before{
  content:"";position:absolute;inset:0;z-index:0;border-radius:inherit;
  background:var(--hover-gradient);opacity:0;transition:opacity .5s ease;
  pointer-events:none;
}
/* hover: no scaling - background/icon fade to solid white + black over
   .2s, and fade back on hover-out */
.ctl:hover{background:#ffffff;color:#000000;border-color:#ffffff;box-shadow:0 0 3px rgba(255,255,255,.2)}
.ctl:active{background:var(--artist-color, var(--active-green));color:#000000;border-color:var(--artist-color, var(--active-green))}
.ctl svg{width:15.2px;height:15.2px;position:relative;z-index:1}
.ctl.toggle.on{color:#000000;background:var(--artist-color, var(--active-green));border-color:var(--artist-color, var(--active-green))}
.ctl.toggle.on:hover{background:#ffffff;border-color:#ffffff;color:#000000;box-shadow:0 0 3px rgba(255,255,255,.2)}
.ctl.toggle.on:hover::before{opacity:0}

/* white at every breakpoint above the smallest one - #t-cur/#t-tot are
   hidden entirely at the smallest breakpoint (see the 480px media query
   below), so this base rule only ever paints on bigger breakpoints */
.bar-row .time{font-family:var(--font-title);font-size:11px;letter-spacing:.06em;color:var(--artist-color, var(--active-green));flex:0 0 auto;position:relative;top:1px}
#t-cur{left:-2px} /* current position text: net 2px left of centre */
#bar{
  position:relative;height:26px;flex:1 1 auto;display:flex;align-items:center;cursor:pointer;
  touch-action:none;min-width:0;
}
#bar::before{content:"";position:absolute;left:0;right:0;height:20px;border-radius:10px;background:color-mix(in srgb, var(--artist-color, var(--active-green)) 10%, rgba(0,0,0,.75))}
#bar-fill{
  position:absolute;left:0;height:20px;width:0%;
  /* same corner radius as #bar-knob's own circle (25.20672px diameter,
     50% = 12.60336px radius), not just half its own height */
  border-radius:12.60336px;
  background:var(--artist-color, var(--active-green));
}
#bar-knob{
  position:absolute;width:25.20672px;height:25.20672px;border-radius:50%; /* +5%, +10%, +10% (19.84px -> 20.832px -> 22.9152px -> 25.20672px) */
  /* artist-colour outer circle with a small grey (matching the bar) centre dot */
  background:radial-gradient(circle,
    #000000 0, #000000 4px,
    var(--artist-color, var(--active-green)) 4px, var(--artist-color, var(--active-green)) 100%);
  left:0%;transform:translateX(-50%);
  margin-top:-2px;
  border:0.2px solid rgba(0,0,0,.5);
  transition:transform .15s ease;
}
#bar:hover #bar-knob{transform:translateX(-50%) scale(1.15)}
/* same rounded-button chrome as .ctl (transport controls) - border,
   transparent fill, fade-to-white on hover, fade-to-artist-color on
   active - so the mute button matches every other round icon button */
.vol-btn{
  flex:0 0 auto;width:33.6px;height:33.6px;border-radius:50%;position:relative;
  display:flex;align-items:center;justify-content:center;
  color:var(--artist-color, var(--active-green));border:none;
  /* same pill look as the seek bar's unfilled track (see #bar::before) */
  background:color-mix(in srgb, var(--artist-color, var(--active-green)) 10%, rgba(0,0,0,.75));
  outline:4px solid transparent;outline-offset:-2px;
  transition:background-color .3s ease,color .3s ease,border-color .3s ease,box-shadow .3s ease;
  box-shadow:0 0 3px rgba(255,255,255,0);
}
.vol-btn::before{
  content:"";position:absolute;inset:0;z-index:0;border-radius:inherit;
  background:var(--hover-gradient);opacity:0;transition:opacity .5s ease;
  pointer-events:none;
}
.vol-btn:hover{background:#ffffff;color:#000000;border-color:#ffffff;box-shadow:0 0 3px rgba(255,255,255,.2)}
.vol-btn:active{background:var(--artist-color, var(--active-green));color:#000000;border-color:var(--artist-color, var(--active-green))}
.vol-btn svg{width:15.2px;height:15.2px;position:relative;z-index:1}
/* .on = sound currently on (not muted) - same filled "engaged" look as
   .ctl.toggle.on (the play button while playing) */
.vol-btn.on{color:#000000;background:var(--artist-color, var(--active-green));border-color:var(--artist-color, var(--active-green));box-shadow:0 8px 22px rgba(0,0,0,.4)}
.vol-btn.on:hover{background:var(--artist-color, var(--active-green));border-color:var(--artist-color, var(--active-green));color:#000000}
.vol-btn.muted{color:var(--fg-faint);border-color:var(--fg-faint)}
#c-volume{
  --vol-pct:100%;
  flex:0 1 55px;width:55px;height:26px;
  -webkit-appearance:none;appearance:none;cursor:pointer;
  background:transparent;
  margin-left:7px; /* extra space from the duration text, on top of the row's own gap */
}
/* same pill look as the main seek bar (see #bar::before/#bar-fill/
   #bar-knob): unfilled track tinted 10% artist color into black 75%,
   filled portion solid artist color, thumb with the same hairline stroke -
   and now the exact same height/size as the seek bar's own track/knob too */
#c-volume::-webkit-slider-runnable-track{
  height:20px;border-radius:10px;
  background:linear-gradient(to right,
    var(--artist-color, var(--active-green)) var(--vol-pct),
    color-mix(in srgb, var(--artist-color, var(--active-green)) 10%, rgba(0,0,0,.75)) var(--vol-pct));
}
#c-volume::-moz-range-track{height:20px;background:color-mix(in srgb, var(--artist-color, var(--active-green)) 10%, rgba(0,0,0,.75));border-radius:10px}
#c-volume::-moz-range-progress{height:20px;background:var(--artist-color, var(--active-green));border-radius:10px}
#c-volume::-webkit-slider-thumb{
  -webkit-appearance:none;margin-top:-2.60336px; /* centers the 25.20672px thumb on the 20px track */
  width:25.20672px;height:25.20672px;border-radius:50%;
  background:radial-gradient(circle,
    #000000 0, #000000 4px,
    var(--artist-color, var(--active-green)) 4px, var(--artist-color, var(--active-green)) 100%);
  border:0.2px solid rgba(0,0,0,.5);
  transition:transform .15s ease;
}
#c-volume:hover::-webkit-slider-thumb{transform:scale(1.15)}
#c-volume::-moz-range-thumb{
  width:19.84px;height:19.84px;border-radius:50%;
  background:radial-gradient(circle,
    #000000 0, #000000 4px,
    var(--artist-color, var(--active-green)) 4px, var(--artist-color, var(--active-green)) 100%);
  border:2px solid var(--artist-color, var(--active-green));
  transition:transform .15s ease;
}
#c-volume:hover::-moz-range-thumb{transform:scale(1.15)}

/* ============================ LIST ============================ */
#view-list,#view-mixes{padding:calc(env(safe-area-inset-top) + 18px) 0 0;overflow:hidden}
.list-head{padding:0 22px 14px;position:relative;z-index:3}
.list-head h2{font-family:var(--font-display);font-weight:900;font-size:14px;letter-spacing:-5px;text-transform:uppercase;
  /* Brice Black in the artist color, ~30% lighter toward the top */
  color:var(--artist-color,var(--active-green));
  background:linear-gradient(to top, var(--artist-color,var(--active-green)), color-mix(in srgb, var(--artist-color,var(--active-green)) 70%, #fff));
  -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
  line-height:1; /* fitScreenTitles() sets the bottom margin so the line below sits 10px under the letters */
}
.list-head p{margin-top:0;font-size:12px;color:var(--fg-soft)}
#list-search{
  width:100%;margin-top:12px;padding:11px 14px;border-radius:999px;
  border:1.5px solid var(--line);background:rgba(255,255,255,.08);
  font-size:13px;
}
#list-search::placeholder{color:var(--fg-faint)}
#tracklist,#mixes-body{
  flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;
  padding-bottom:70px; /* 20px + 50px extra requested space */
  scrollbar-width:none;-ms-overflow-style:none; /* scroll still works, just no visible scrollbar */
}
#tracklist::-webkit-scrollbar,#mixes-body::-webkit-scrollbar{display:none;width:0;height:0}
/* light black gradient stuck to the bottom of the list, fading the last
   rows out under the nav bar - purely decorative, doesn't block taps */
#tracklist-gradient,#mixes-gradient{
  position:absolute;left:0;right:0;bottom:0;height:120px;z-index:1;
  pointer-events:none;
  background:linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,.5) 100%);
}
/* --row-color is set inline per element in renderList() from that artist's
   own tr.artistColor, so each artist's section shows its own accent
   instead of one flat color across the whole list */
.group-header{
  display:flex;align-items:center;gap:10px;
  padding:39px 22px 6px;
  color:var(--row-color, var(--fg-soft));
}
.group-header-text{display:flex;flex-direction:column;gap:3px;min-width:0}
.group-header-top{
  display:flex;align-items:baseline;gap:10px;
  font-family:var(--font-display);font-weight:600;
  font-size:30px;letter-spacing:.2em;text-transform:uppercase;
}
.group-header-name{min-width:0}
.group-header-count{
  font-family:var(--font-body);font-weight:400;font-size:12px;letter-spacing:0;
  text-transform:none;white-space:nowrap;
  color:color-mix(in srgb, var(--row-color, var(--fg-soft)) 55%, transparent);
}
/* one-line music-style blurb (see ARTIST_STYLE_DESC in app.js) - small,
   light grey, plain-case so it reads as a caption under the artist name
   rather than competing with it */
.group-header-desc{
  font-family:var(--font-body);font-weight:400;font-size:11px;letter-spacing:0;
  text-transform:none;color:rgba(255,255,255,.4);
}
/* small preview of the artist's animal, shown in whole (carved out of the
   shared assets/animals.svg sprite via an #svgView(viewBox(...)) fragment -
   see CREATURE_SVG_CROP in app.js), pushed to the far right of the row
   (margin-left:auto) and vertically centred against the artist name via
   the row's own align-items:center. Width/height are set inline in JS,
   since each animal's own aspect ratio differs. */
.group-header-creature{
  margin-left:auto;flex:none;
  background-position:top center;background-repeat:no-repeat;
  pointer-events:none;
}
.track{
  width:100%;height:35px;display:flex;align-items:center;gap:14px;text-align:left;
  padding:0 22px;border-bottom:1.5px solid var(--line);box-sizing:border-box;
  transition:background .5s ease;
}
.track:active{background:rgba(124,255,158,.18)}
.track.playing{background:var(--wash)}
.track .idx{
  font-family:var(--font-display);font-size:11px;color:var(--fg-soft);
  width:24px;flex:0 0 auto;
  transition:color .5s ease;
  position:relative;top:2px;
}
.track.playing .idx{color:var(--row-color, var(--active-green))}
.track.playing .t-title{color:var(--row-color, var(--active-green))}
.track .t-meta{flex:1;min-width:0}
.track .t-title{
  font-family:var(--font-title);font-size:24.125px;font-weight:400;
  word-spacing:.15em;text-transform:uppercase;
  color:#eeeeee;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  transition:color .5s ease;
  position:relative;top:2px;
}
.track .t-len{font-size:11px;color:var(--fg-soft);flex:0 0 auto;transition:color .5s ease;position:relative;top:2px}
.track:hover{background:var(--row-color, var(--active-green))}
.track:hover .idx,.track:hover .t-title,.track:hover .t-len{color:#000000}
.eq{display:none;gap:2px;align-items:flex-end;height:14px;flex:0 0 auto}
.track.playing .eq{display:flex}
.eq i{width:3px;background:var(--row-color, var(--active-green));border-radius:2px;animation:eq .9s ease-in-out infinite}
.eq i:nth-child(2){animation-delay:.2s}.eq i:nth-child(3){animation-delay:.4s}
@keyframes eq{0%,100%{height:4px}50%{height:14px}}


/* ---- Mixes tab: one card per playlist (see renderMixes() in app.js) ---- */
.mix-card{padding:26px 0 0}
.mix-head{
  display:flex;align-items:center;gap:14px;padding:0 22px 8px;
  color:var(--mix-color, var(--artist-color, var(--active-green)));cursor:pointer;
}
.mix-head-text{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
.mix-name{
  font-family:var(--font-display);font-weight:600;font-size:26px;letter-spacing:.2em;text-transform:uppercase;
  display:flex;align-items:baseline;gap:10px;
}
/* the icon is as tall as the whole text block: from the top of the title down to the bottom of the track count line */
.mix-icon-box{align-self:stretch;flex:0 0 auto;background-repeat:no-repeat;background-position:center top;pointer-events:none} /* animal aligned to the top of the text block (level with the title) */ /* an artist animal, as tall as the text next to it - sized in sizeMixIcons() */
.mix-name .mix-chev{font-size:18.2px;letter-spacing:0;opacity:.6;transition:transform .2s ease} /* 30% bigger than the original 14px */
.mix-name .mix-toggle-label{font-family:var(--font-body);font-weight:400;font-size:11px;letter-spacing:0;text-transform:none;opacity:.7;align-self:center}
.mix-card.open .mix-chev{transform:rotate(90deg)}
.mix-blurb{font-family:var(--font-body);font-size:12px;color:rgba(255,255,255,.55)}
.mix-meta{font-family:var(--font-body);font-size:11px;color:color-mix(in srgb, var(--mix-color, var(--artist-color, var(--active-green))) 60%, transparent)}
.mix-play{
  flex:0 0 auto;display:flex;align-items:center;gap:1px; /* same icon-to-label space as the Player tab */
  padding:11.25px 20px;border-radius:999px;border:none; /* 25% bigger */
  background:var(--mix-color, var(--artist-color, var(--active-green)));color:#000;
  font-family:var(--font-display);font-weight:600;font-size:16px;letter-spacing:.07em;text-transform:uppercase;
}
.mix-play svg{width:16.9px;height:16.9px} /* the Player tab's icon, at the tab's icon-to-text proportion (23px : 20.41px) */
.mix-play:hover{background:#fff}
.mix-play-label{position:relative;top:1px} /* text 1px lower than the icon */
.mix-card.active .mix-name::after{
  content:"";width:8px;height:8px;border-radius:50%;background:var(--mix-color, var(--artist-color, var(--active-green)));
  align-self:center;animation:pulse 2.4s ease-in-out infinite;
}
.mix-tracks{display:none}
.mix-card.open .mix-tracks{display:block}
.track .t-artist{
  flex:0 1 auto;max-width:34%;font-size:11px;color:var(--fg-soft);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;position:relative;top:2px;
  transition:color .5s ease;
}
.track:hover .t-artist{color:#000}
@media (max-width:480px){
  .mix-name{font-size:20px;letter-spacing:.14em}
  .track .t-artist{display:none}
}

.mix-card .track{--row-color:var(--mix-color, var(--artist-color, var(--active-green)))}
/* ============================ INFO ============================ */
#view-info{padding:calc(env(safe-area-inset-top) + 18px) 22px 0;overflow-y:auto}
#view-info .list-head{padding-left:0;padding-right:0}
#view-info .logo{width:min(200px,56vw);height:auto;margin:26px auto 26px}
.info-block{max-width:560px;margin:0 auto 22px}
.info-block h3{font-family:var(--font-display);font-weight:600;font-size:11px;letter-spacing:.26em;text-transform:uppercase;margin-bottom:8px;
  display:inline-block;background:var(--object);color:var(--bg);padding:5px 10px;border-radius:6px}
.info-block p{font-size:14px;line-height:1.7}
.info-block p.dim{color:var(--fg-soft);font-size:12px}
#lyrics-audit-results{margin-top:10px}
#lyrics-audit-results ul{list-style:none;margin-top:6px;display:flex;flex-direction:column;gap:6px}
#lyrics-audit-results li{font-size:13px;line-height:1.5;padding:8px 10px;border:1px solid var(--line);border-radius:8px}

/* ============================ SYNC STUDIO ============================ */
#view-sync{padding:calc(env(safe-area-inset-top) + 18px) 22px 0;overflow-y:auto}
.sync-head h2{font-family:var(--font-display);font-weight:600;font-size:14px;letter-spacing:.24em;text-transform:uppercase}
.sync-head p{margin:6px 0 16px;font-size:12px;color:var(--fg-soft);line-height:1.6}
#sync-search{
  width:100%;margin-bottom:12px;padding:11px 14px;border-radius:999px;
  border:1.5px solid var(--line);background:rgba(255,255,255,.08);
  font-size:13px;
}
#sync-search::placeholder{color:var(--fg-faint)}
.sync-tracks{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;max-height:160px;overflow-y:auto}
.sync-track{
  font-size:12px;font-weight:700;padding:10px 16px;border-radius:999px;
  border:1.5px solid var(--line);
  transition:background .2s,border-color .2s;
}
.sync-track:hover{border-color:var(--object)}
.sync-track:active{background:var(--artist-color, var(--active-green));color:#000000;border-color:var(--artist-color, var(--active-green))}
.sync-track.sel{background:var(--artist-color, var(--active-green));color:#000000;border-color:var(--artist-color, var(--active-green))}
#sync-lyrics{
  width:100%;min-height:140px;resize:vertical;
  background:rgba(255,255,255,.08);border:1.5px solid var(--line);border-radius:12px;
  padding:12px;font-size:14px;line-height:1.6;
}
#sync-lyrics::placeholder{color:var(--fg-faint)}
.sync-actions{display:flex;gap:8px;margin:14px 0 24px;flex-wrap:wrap}
.btn{
  font-family:var(--font-display);font-weight:600;font-size:11px;letter-spacing:.14em;
  padding:14px 22px;border-radius:999px;border:1.5px solid var(--fg);
  transition:background .2s,color .2s;
}
.btn:hover:not(:disabled){background:var(--fg);color:var(--bg)}
.btn:active:not(:disabled){background:var(--artist-color, var(--active-green));color:#000000;border-color:var(--artist-color, var(--active-green))}
.btn.primary{background:var(--object);color:var(--bg);border-color:var(--object)}
.btn.primary:hover:not(:disabled){background:var(--object-deep);color:var(--bg)}
.btn:disabled{opacity:.35}
/* tapping overlay */
#tap-ui{position:fixed;inset:0;z-index:55;display:none;flex-direction:column;background:var(--bg)}
#tap-ui.on{display:flex}
.tap-top{padding:calc(env(safe-area-inset-top) + 16px) 20px 0;display:flex;justify-content:space-between;align-items:center}
.tap-top .t{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--fg-soft)}
.tap-words{flex:0 0 auto;text-align:center;padding:26px 20px 8px}
#tap-prev{font-size:13px;color:var(--fg-faint);min-height:20px}
#tap-word{font-family:var(--font-display);font-weight:900;font-size:clamp(30px,9vw,52px);margin:6px 0;color:var(--fg)}
#tap-next{font-size:14px;color:var(--fg-soft);min-height:22px}
#tap-count{font-size:11px;color:var(--fg-soft);margin-top:10px;letter-spacing:.14em}
#tap-zone{
  flex:1;margin:18px 20px calc(env(safe-area-inset-bottom) + 18px);
  border:2px dashed var(--fg-faint);border-radius:24px;
  display:flex;align-items:center;justify-content:center;
  font-family:var(--font-display);font-weight:600;font-size:13px;letter-spacing:.22em;
  color:var(--fg-soft);user-select:none;touch-action:none;
}
#tap-zone:active{background:var(--wash);border-style:solid}
.tap-side{display:flex;gap:8px;padding:0 20px}
#sync-out{
  width:100%;min-height:120px;display:none;
  background:rgba(255,255,255,.08);border:1.5px solid var(--line);border-radius:12px;
  padding:12px;font-size:11px;font-family:monospace;margin-bottom:24px;
}

/* track-loading scrim: #track-loader covers everything below the top
   toolbar (its own top edge is set in JS to the toolbar's bottom edge);
   the two .track-loader-piece divs cover the toolbar's own left/right
   halves (pano/util buttons) while leaving a gap for the centred AQAI
   logo - all three positioned live in JS, see positionLoaderPieces() */
#track-loader,.track-loader-piece{
  position:fixed;left:0;right:0;bottom:0;z-index:45;
  background:rgba(0,0,0,.2);
  opacity:0;pointer-events:none;
  transition:opacity .15s ease;
}
#track-loader.show,.track-loader-piece.show{opacity:1;pointer-events:auto}

/* ============================ NAV — tab bar ============================ */
nav{
  position:fixed;left:50%;bottom:4px;z-index:50;
  transform:translateX(-50%);
  width:calc(100% - 8px);max-width:650px;
  height:calc(var(--nav-h) + env(safe-area-inset-bottom));
  padding:0 0 env(safe-area-inset-bottom);
  display:flex;gap:2px;
  background:transparent;
}
.nav-btn{
  flex:1;display:flex;align-items:center;justify-content:center;gap:7px;position:relative;
  background:color-mix(in srgb, color-mix(in srgb, var(--artist-color, var(--active-green)) 21%, black) 70%, white);
  border:none;
  border-radius:28px 28px 0 0;
  color:var(--artist-color, var(--active-green));
  opacity:.81;
  transition:transform .2s ease,color .1s ease,background .2s ease,opacity .2s ease,border-color .2s ease;
}
.nav-btn:not(.active){
  border:none;
  /* same color as the seek bar's unfilled track (see #bar::before) */
  background:color-mix(in srgb, var(--artist-color, var(--active-green)) 10%, rgba(0,0,0,.75));
}
/* top corners 30% less rounded at the smallest breakpoint (28px -> 19.6px) -
   placed after the base rule above (same specificity) so it actually wins */
@media (max-width:480px){
  .nav-btn{border-radius:19.6px 19.6px 0 0}
}
.nav-btn.active{opacity:1}
/* hover: icon/text turn white and the tab lifts up - no stroke at all,
   here or at rest (the currently-active tab stays filled green instead,
   see .active below) */
.nav-btn:not(.active):hover{
  color:#ffffff;transform:translateY(-5px);
  background:#000000;
}
.nav-btn svg{width:clamp(16.1px,4.107143vw,23px);height:clamp(16.1px,4.107143vw,23px);position:relative;z-index:1}
.nav-btn-word{position:relative;z-index:1}
.nav-btn:active{background:var(--artist-color, var(--active-green));color:#000000;border-color:var(--artist-color, var(--active-green))}
.nav-btn.active{background:var(--artist-color, var(--active-green));color:#000000;border-color:var(--artist-color, var(--active-green))}
.nav-btn.hidden{display:none}
/* same clamp() approach as the rest of the app's text (e.g. .meta .title) -
   scales down with the viewport instead of staying a fixed px size */
.nav-btn-word{font-family:var(--font-display);font-weight:900;font-size:clamp(15.065px,3.850446vw,21.5625px);letter-spacing:-.02em}
/* Player/Songs labels - exact spec from the supplied Photoshop character
   panel: Brice Regular, 20.41pt (taken 1:1 as px), -40 tracking (Adobe/
   Photoshop tracking is in 1/1000 em, so -40 -> -.04em) - now scales down
   on smaller screens like the rest of the app's text instead of staying fixed */
.nav-label{position:relative;left:-6px;z-index:1;font-family:var(--font-display);font-weight:400;font-size:clamp(14.3px,3.644643vw,20.41px);letter-spacing:-.04em}
/* icon sits left of the label for free - .nav-btn is already a flex row
   with the svg first in markup, so no extra layout is needed here */
/* 50% of the shared 23px .nav-btn svg size - needs to out-specificity the
   general ".nav-btn svg" rule above (a single class alone won't beat it) */
.nav-btn svg.nav-icon-list{width:clamp(18.3px,4.665179vw,26.125px);height:clamp(13.3px,3.392857vw,19px);position:relative;left:-4px}
/* bars are transparent (fill:none) with a centered (straddling, not
   outside-only) artist-colour stroke */
.nav-icon-list rect{stroke:var(--artist-color, var(--active-green));stroke-width:4px}
.nav-btn.active .nav-icon-list rect{stroke:#000000}
.nav-btn:not(.active):hover .nav-icon-list rect{stroke:#ffffff}

/* toast */
#toast{
  position:fixed;left:50%;bottom:calc(var(--nav-h) + env(safe-area-inset-bottom) + 16px);
  transform:translateX(-50%) translateY(10px);z-index:70;
  background:var(--object);color:var(--bg);
  font-size:12px;font-weight:400;letter-spacing:.06em;
  padding:10px 20px;border-radius:999px;
  opacity:0;pointer-events:none;transition:opacity .25s,transform .25s;
}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}

@media (prefers-reduced-motion:reduce){
  .gate-btn{animation:none}
  .eq i{animation:none;height:8px}
}
