:root {
  /* Fixed chrome that shares the viewport with everything else. Every
     "sits above the bar" offset derives from these — never a hand-tuned px. */
  --dock-h: 64px;
  --tabbar-h: 0px;
  --rail-h: 0px;
  --safe-b: env(safe-area-inset-bottom, 0px);
  /* Genre-driven accent pair (mascot lighting, lasers, dock bars); the
     defaults are the brand cyan/magenta until setMascotGenre() runs. */
  --stage-a-rgb: var(--cyan-rgb);
  --stage-b-rgb: var(--magenta-rgb);
  /* Vertical budget shared with the dial (tablets: the pill + the mosaic
     strip under it + header); desktop overrides below. */
  --dial-chrome: calc(var(--dock-h) + var(--tabbar-h) + var(--safe-b) + 230px);
  /* ---- The palette ----
     Every colour in this file resolves to one of these. Before, the two brand
     hexes appeared raw in places, seven undocumented greys were scattered
     through the rules, two of them differed by 5/255 for no reason, and
     --text-dim was referenced twice while never being defined — so those sites
     silently fell back to #9aa, a colour used nowhere else.

     Surfaces run darkest to lightest. */
  --ink: #0a0a12;         /* deeper than the page, for wells and insets */
  --bg: #07070c;
  --panel-bg: #0d0d16;
  --surface-1: #14141f;   /* raised: tiles, covers */
  --surface-2: #1a1a26;   /* raised further: cells, chips */
  --surface-3: #262631;   /* hover and menu fills */
  --dim: #2a2a3a;         /* hairlines and inactive fills */
  --line-strong: #3a3a4a; /* visible borders */

  /* Brand, read off the approved mockups (Luis, 2026-09-03). The cyan was
     #00f0dc, a green-leaning aqua; the boards use a bluer sky-cyan, and the
     magenta is hotter. Channel copies exist because ~56 glows and rims are
     written as rgba() — without them a token change leaves every halo on the
     old colour, which is exactly the drift this pass exists to remove. */
  --cyan: #22d3ee;
  --cyan-rgb: 34, 211, 238;
  --magenta: #ff2d9c;
  --magenta-rgb: 255, 45, 156;
  --violet: #8b5cff;
  --violet-rgb: 139, 92, 255;

  /* Every primary action on the boards is this ramp, left to right. */
  --grad-cta: linear-gradient(90deg, var(--magenta) 0%, var(--violet) 52%, var(--cyan) 100%);
  --grad-brand: linear-gradient(100deg, var(--cyan) 0%, var(--violet) 50%, var(--magenta) 100%);

  --text: #c8c8e0;
  --text-dim: #7a7a90;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Segoe UI", system-ui, sans-serif;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0;
}

body > *:not(#hero) {
  padding-left: 16px;
  padding-right: 16px;
}

h1 {
  color: var(--cyan);
  font-size: 20px;
  letter-spacing: 2px;
  margin: 8px 0 4px;
}

/* Sign-in. The background plate is AMY on her podium with the right-hand third
   left empty, so the card sits in that space rather than on top of her — the
   board's composition. The plate is a JPG already on #07070C, so it butts
   straight into the page with no seam and needs no knockout. */
#hero {
  position: relative;
  width: 100%;
  min-height: 90vh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: url("/brand/studio-bg/03-listen-profile.jpg") center right / cover no-repeat;
}

/* Only darken where the card lands. Left untouched, AMY keeps her contrast. */
#hero-overlay {
  position: absolute;
  inset: 0;
  /* 50% over the left, where the form sits; her side stays clear. */
  background:
    linear-gradient(90deg, rgba(7, 7, 12, 0.86) 0%, rgba(7, 7, 12, 0.5) 42%, rgba(7, 7, 12, 0) 68%),
    linear-gradient(180deg, rgba(7, 7, 12, 0.35) 0%, rgba(7, 7, 12, 0) 30%, rgba(7, 7, 12, 0.9) 100%);
}

#hero-content {
  position: relative;
  z-index: 1;
  text-align: center;
  padding: 40px 20px;
  max-width: 480px;
}

/* Wide enough for AMY and the card to sit side by side, as on the board. */
@media (min-width: 900px) {
  #hero {
    justify-content: flex-start;
    background-position: right center;
  }
  #hero-content {
    margin-left: clamp(24px, 7vw, 120px);
    text-align: left;
  }
  #hero-content .brand-logo-xl { align-items: flex-start; }
  /* The plate already carries a dancer; the live three.js mascot here would be
     a second AMY on the same screen. */
  #mascot-3d-hero { display: none; }
}

#hero-title {
  margin: 0 0 12px;
}

/* ---------- The wordmark ----------
   Two stacked lines, as in the brand mock: BANGER in a heavy italic with a
   magenta B, STUDIO in wide-tracked cyan beneath it. Pure type, so it stays
   sharp at any size and needs no image. */
.brand-logo {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 0.9;
  text-decoration: none;
  font-family: "Segoe UI", "Helvetica Neue", Arial, system-ui, sans-serif;
}

.brand-logo .bl-banger {
  font-weight: 900;
  font-style: italic;
  letter-spacing: -0.02em;
  color: #fff;
  text-shadow: 0 0 14px rgba(255, 255, 255, 0.18);
}

.brand-logo .bl-banger b {
  color: var(--magenta);
  text-shadow: 0 0 14px rgba(var(--magenta-rgb), 0.55);
}

.brand-logo .bl-studio {
  font-weight: 700;
  font-style: normal;
  letter-spacing: 0.42em;
  color: var(--cyan);
  text-shadow: 0 0 12px rgba(var(--cyan-rgb), 0.5);
  /* the tracking adds trailing space; pull it back so the block centres */
  margin-right: -0.42em;
}

.brand-logo-xl { align-items: center; }
.brand-logo-xl .bl-banger { font-size: 58px; }
.brand-logo-xl .bl-studio { font-size: 22px; margin-top: 4px; }

.brand-logo-sm { vertical-align: middle; }
.brand-logo-sm .bl-banger { font-size: 20px; }
.brand-logo-sm .bl-studio { font-size: 8px; margin-top: 2px; letter-spacing: 0.5em; margin-right: -0.5em; }

@media (max-width: 600px) {
  .brand-logo-xl .bl-banger { font-size: 44px; }
  .brand-logo-xl .bl-studio { font-size: 17px; }
}

/* Iridescent chrome wordmark: an animated cyan→violet→magenta gradient
   clipped to the glyphs, with stacked drop-shadows faking a 3D extrusion
   plus a neon bloom — matches the tesseract's palette. */
.brand-wordmark {
  font-weight: 900;
  background: linear-gradient(115deg, var(--cyan) 0%, #4d7cff 22%, #b026ff 45%, var(--magenta) 68%, var(--cyan) 100%);
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: var(--cyan); /* fallback for browsers without background-clip:text */
  animation: brand-sheen 8s linear infinite;
  filter:
    drop-shadow(0 2px 0 rgba(8, 42, 46, 0.95))
    drop-shadow(0 4px 0 rgba(24, 10, 44, 0.8))
    drop-shadow(0 0 16px rgba(var(--cyan-rgb), 0.4))
    drop-shadow(0 0 36px rgba(var(--magenta-rgb), 0.25));
}

@keyframes brand-sheen {
  to { background-position: 300% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .brand-wordmark { animation: none; }
}


#hero-sound-toggle:hover {
  border-color: var(--cyan);
}

#hero-tagline {
  color: var(--text);
  font-size: 15px;
  line-height: 1.5;
  margin-bottom: 24px;
}

/* The Clerk card is the whole signed-out surface, so give it room and centre
   it under the wordmark. Colours come from CLERK_APPEARANCE in app.js — the
   card renders inside Clerk's own bundle and doesn't inherit our custom
   properties. */
#clerk-sign-in {
  display: flex;
  justify-content: center;
  margin-top: 24px;
}

#top-bar {
  width: 100%;
  max-width: 1000px;
  margin: 0 auto 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* ---------- Create / Vault / Floor shell ---------- */
.tab-pane { display: none; width: 100%; }
.tab-pane.is-active { display: block; }

/* Section plates from the asset pack. Each is fixed and behind everything, so
   scrolling doesn't drag the artwork, and each is faded into --bg by its own
   ::before rather than being placed on the element — that keeps text contrast
   under our control instead of at the mercy of the photo underneath.
   All three are JPGs already on #07070C, so the fade has no seam to hide. */
.tab-pane.is-active::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  opacity: 0.5;
  pointer-events: none;
}

/* A second layer sinks the plate into the page background at the edges. */
.tab-pane.is-active::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 40%, rgba(7, 7, 12, 0.15) 0%, rgba(7, 7, 12, 0.72) 60%, var(--bg) 100%);
}

#tab-create.is-active::before {
  background-image: url("/brand/backgrounds/bg-create-wormhole.jpg");
  opacity: 0.72;
}

#tab-floor.is-active::before {
  background-image: url("/brand/backgrounds/bg-floor-stage.jpg");
}

#tab-vault.is-active::before {
  background-image: url("/brand/studio-bg/04-empty-studio.jpg");
  /* The Vault is a dense grid of cover art — the plate is a texture behind it,
     not a picture competing with it. */
  opacity: 0.22;
}
.vault-view { display: none; }
.vault-view.is-active { display: block; }

/* Phones/tablets: a fixed bottom bar under the dock. */
#tab-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 91;
  display: flex;
  height: calc(var(--tabbar-h) + var(--safe-b));
  padding-bottom: var(--safe-b);
  background: rgba(13, 13, 22, 0.97);
  border-top: 1px solid var(--dim);
  backdrop-filter: blur(6px);
}

.tab-btn {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  background: none;
  border: 0;
  color: var(--text);
  opacity: 0.6;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  position: relative;
}

.tab-btn .tab-ico { font-size: 20px; line-height: 1; }
.tab-btn .tab-lbl { position: relative; }

.tab-btn.is-active {
  opacity: 1;
  color: var(--cyan);
  text-shadow: 0 0 10px rgba(var(--cyan-rgb), 0.6);
}

@media (max-width: 1149px) {
  :root { --tabbar-h: 56px; }
}

/* Phone header: logo + coin + avatar-ish account cluster on one line. */
@media (max-width: 700px) {
  #top-bar { gap: 8px; margin-bottom: 4px; }
  #top-bar h1 { font-size: 15px; margin: 6px 0; white-space: nowrap; }
  #top-bar h1 .topbar-logo { width: 24px; height: 24px; }
  #top-bar-account { gap: 6px; flex-wrap: nowrap; }
  #display-name-row { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  #display-name-edit-btn { display: none; }
}

/* Desktop: the bar becomes a rail of pills in the header. */
@media (min-width: 1150px) {
  #tab-bar {
    position: static;
    height: auto;
    padding: 0;
    gap: 4px;
    background: none;
    border: 0;
    backdrop-filter: none;
  }
  .tab-btn {
    flex: none;
    flex-direction: row;
    gap: 6px;
    padding: 6px 14px;
    border-radius: 18px;
    border: 1px solid transparent;
    font-size: 12px;
  }
  .tab-btn .tab-ico { font-size: 15px; }
  /* The boards show the rail as plain tracked uppercase, with only the active
     tab in a cyan-rimmed pill that glows. Inactive tabs carry no icon weight
     and no chrome — the one lit pill is the whole signal. */
  .tab-btn {
    letter-spacing: 0.14em;
    text-transform: uppercase;
    font-weight: 600;
    opacity: 0.55;
  }
  .tab-btn:hover { opacity: 0.85; }
  .tab-btn.is-active {
    opacity: 1;
    border-color: rgba(var(--cyan-rgb), 0.55);
    background: rgba(var(--cyan-rgb), 0.10);
    box-shadow: 0 0 18px rgba(var(--cyan-rgb), 0.22), inset 0 0 12px rgba(var(--cyan-rgb), 0.06);
  }
}

#app-ui {
  /* Hidden until sign-in via the .visible class, NOT an inline style —
     an inline display:block silently overrode the desktop grid layout
     below, which is why the two-column layout never actually rendered. */
  display: none;
  width: 100%;
  max-width: 1000px;
  margin: 0 auto;
}

#app-ui.visible {
  display: block;
}

/* The coin and crown are JPGs on their own #07070C ground, not cut-outs. A
   round mask hides the square corners without knocking the black out, so they
   keep their rendered edge lighting instead of reading as pasted-on. */
.icon-coin,
.chip-ico {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  object-fit: cover;
  vertical-align: -4px;
  flex: 0 0 auto;
}

.icon-coin { filter: drop-shadow(0 0 4px rgba(255, 210, 90, 0.6)); }

.chip-ico {
  width: 16px;
  height: 16px;
  filter: drop-shadow(0 0 4px rgba(255, 210, 90, 0.45));
}

.icon-badge {
  width: 20px;
  height: 20px;
  vertical-align: -5px;
  margin-left: 2px;
}

/* One gating mechanism for "not shown right now": the hidden attribute.
   Id-specific display rules otherwise outrank the UA's [hidden] rule. */
[hidden] { display: none !important; }

/* Credits: a coin and a number in the header, nothing more. The breakdown
   is the tooltip; tapping opens the packs. */
#coin-balance {
  color: var(--cyan);
  font-weight: bold;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  /* On the boards this is a dark bordered pill sitting next to the account
     chip, not a tinted gradient lozenge — the two read as a matched pair. */
  padding: 7px 14px;
  border-radius: 999px;
  font-size: 13px;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--line-strong);
}

#coin-balance:hover { border-color: rgba(var(--cyan-rgb), 0.55); }

.tier-badge {
  display: inline-flex;
  align-items: center;
  margin-right: 6px;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--dim);
  font-size: 9px;
  font-weight: bold;
  letter-spacing: 0.12em;
  vertical-align: middle;
}

.tier-badge:not(.is-paid) {
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  padding: 2px 6px;
  color: var(--text);
  opacity: 0.6;
}

.tier-badge:hover { opacity: 1; color: var(--cyan); }

.tier-badge .icon-badge { margin: 0; vertical-align: middle; }

#coin-balance.out-of-credits {
  color: #ff5577;
  cursor: pointer;
  animation: credits-pulse 1.6s ease-in-out infinite;
}

@keyframes credits-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

@keyframes logo-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

#mascot-3d-hero {
  display: block;
  margin: 0 auto 4px;
  pointer-events: none;
}

/* Phones: the 300px hero dancer dwarfed the wordmark — scale the canvas
   down (render resolution stays, CSS shrinks it). */
@media (max-width: 600px) {
  #mascot-3d-hero {
    width: 190px;
    height: 165px;
  }
}

.topbar-logo {
  border-radius: 50%;
  vertical-align: middle;
  margin-right: 8px;
  animation: logo-spin 14s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .topbar-logo { animation: none; }
}

#top-bar-account {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

#top-bar-account-actions {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

#display-name-row {
  color: #fff;
  font-size: 12px;
  display: inline-block;
  padding: 5px 12px;
  border-radius: 18px;
  background: rgba(var(--magenta-rgb), 0.13);
  box-shadow: 0 0 0 1px rgba(var(--magenta-rgb), 0.4) inset;
}

#display-name-edit-btn {
  padding: 1px 6px;
  font-size: 11px;
}

.queue-item {
  display: flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--dim);
  border-radius: 6px;
  padding: 8px 10px;
  background: var(--panel-bg);
  font-size: 12px;
  color: var(--text);
  margin-bottom: 6px;
}

/* 52px in the Just Dropped chips, 48px in the Most Played rows, per the brief.
   Both were 30px, which read as a favicon rather than cover art. */
.queue-thumb {
  width: 52px;
  height: 52px;
  border-radius: 8px;
  object-fit: cover;
  flex-shrink: 0;
  display: none;
}

.queue-item b {
  color: var(--cyan);
}

.queue-badge {
  margin-left: 2px;
}

.queue-time {
  color: var(--dim);
  font-size: 10.5px;
  display: block;
}

.queue-item .queue-eta {
  color: var(--dim);
  font-size: 11px;
}

.queue-item.queue-idle {
  /* var(--dim) reads fine on the darker .queue-item background it was
     designed for, but nearly disappears directly on #queue-section's
     lighter plate now that this state skips the card chrome below. */
  color: var(--text);
  opacity: 0.55;
  font-style: italic;
  border: none;
  background: none;
  padding: 2px 2px 0;
  margin-bottom: 0;
  font-size: 11.5px;
  text-align: center;
}

/* One coach mark, shown until the first successful generate (persisted). */
#coach-mark {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 8px 0 10px;
  padding: 8px 12px;
  border: 1px solid rgba(var(--cyan-rgb), 0.45);
  border-radius: 20px;
  background: rgba(var(--cyan-rgb), 0.06);
  color: var(--text);
  font-size: 12px;
}

#coach-mark .i { color: var(--cyan); flex-shrink: 0; }
#coach-mark span { flex: 1; }

#coach-close {
  background: none;
  border: 0;
  color: var(--text);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.7;
}

#coach-close:hover { opacity: 1; color: var(--cyan); }

/* ---------- icon system: one 24px neon line set, used everywhere ---------- */
.i {
  width: 24px;
  height: 24px;
  flex: none;
  vertical-align: middle;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.i.sm { width: 18px; height: 18px; }
.i.xs { width: 15px; height: 15px; }
.neon { filter: drop-shadow(0 0 6px currentColor); }

.btn .i { vertical-align: -5px; }

/* Play/pause: one button, two glyphs, CSS picks by .is-playing (set from
   the audio element's real state in updateBarChrome). */
#radio-play-btn .i-pause, #radio-play-btn.is-playing .i-play { display: none; }
#radio-play-btn.is-playing .i-pause { display: inline-block; }

/* ---------- "Your track is ready" ---------- */
#ready-card {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr);
  gap: 14px;
  align-items: start;
  margin: 12px 0 10px;
  padding: 12px;
  border: 1px solid rgba(var(--cyan-rgb), 0.45);
  border-radius: 14px;
  background: linear-gradient(120deg, rgba(var(--cyan-rgb), 0.08), rgba(var(--magenta-rgb), 0.08));
}

#ready-card .ready-cover { width: 96px; }
#ready-card .ready-cover .proc-title { font-size: 12px; left: 6px; right: 6px; bottom: 6px; }
#ready-card .ready-cover .proc-genre { display: none; }

.ready-body { min-width: 0; display: grid; gap: 4px; }

.ready-eyebrow {
  font-size: 10px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--cyan);
}

.ready-title {
  font-size: 18px;
  font-weight: 800;
  color: #fff;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ready-meta { font-size: 12px; color: var(--text); opacity: 0.8; }

.ready-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}

.ready-actions .btn { padding: 7px 12px; font-size: 12px; }

#upsell-line { margin-top: 6px; }

#upsell-btn {
  background: none;
  border: 0;
  padding: 0;
  color: var(--magenta);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
  opacity: 0.85;
}

#upsell-btn:hover { text-decoration: underline; opacity: 1; }

#downloads-eyebrow { margin: 6px 0 4px; }

/* The status plate only exists while it has something to say — before the
   first Generate it was an empty gradient box under the drawer. */
#status-card:not(.busy):has(#status-line:empty) { display: none; }

#dashboard-row {
  display: flex;
  gap: 18px;
  align-items: flex-start;
  /* Center the dial column in however much width the monitor gives —
     without this the dial hugs the left edge on ultrawides. */
  justify-content: center;
  width: 100%;
}

/* The Prompt tab hides this via the native `hidden` attribute — but an ID
   selector's display:flex above beats the browser's built-in [hidden]
   rule on specificity, so without this override toggling `hidden` would
   silently do nothing. */

/* The dial sits above the page plate; the card sits above the dial. The
   wrapper carries no surface of its own — every square that used to sit behind
   the dial came from here or from the canvas. */
#dial-wrap {
  z-index: 2;
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  padding: 0 !important;
  overflow: visible;
}
#panel { position: relative; z-index: 3; }

#dashboard-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Without this the column shrinks to the canvas's intrinsic attribute
     width, so the dial never grew into the space its column actually has —
     it just sat at 640px however wide the monitor was. */
  width: 100%;
}

/* Wrapper that hugs the canvas so the preview pill can be positioned
   against the dial itself rather than the whole column. */
#dial-wrap {
  position: relative;
  width: 100%;
  display: flex;
  justify-content: center;
}

/* The dial face, behind the genre orbs. Same box as the canvas so the two stay
   concentric at every width, screen-blended so its black ground disappears,
   and pointer-events:none so it can never swallow an orb click.

   This rule was written once already and then deleted by a later edit whose
   replacement range happened to span it — which is why it shipped unstyled and
   rendered as a full-width image that pushed the dial sideways. */
#dial-face {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  object-fit: contain;
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: .45;
  z-index: 1;
  /* Clipped like the dial. The PNG's ground is dark but not transparent, and
     screen lightens it against the page — which put a faint square back around
     the dial, the exact thing this pass exists to remove. */
  clip-path: circle(50% at 50% 50%);
}

/* The GENERATE core. Placed from the canvas's own geometry — the tesseract was
   r=68 centred on (320, 308) in a 640 grid — so 21.25% wide at 50% / 48.125%.
   screen drops the #07070C ground the still was rendered on, which is what
   keeps its bloom instead of hard-edging it the way a knockout would. */
/* The GENERATE hub, centred on the dial's own geometry: the core sat at
   (320, 308) in a 640 grid, so 50% / 48.125%. 160px per the spec — comfortably
   inside the ~219px generate ring the canvas still draws around it. */
#generate-hub {
  position: absolute;
  left: 50%;
  top: 48.125%;
  transform: translate(-50%, -50%);
  width: 160px;
  height: 160px;
  border: 0;
  padding: 0;
  background: transparent;
  border-radius: 50%;
  overflow: hidden;
  cursor: pointer;
  z-index: 3;
}

#generate-hub iframe {
  width: 160px;
  height: 160px;
  border: 0;
  background: transparent;
  pointer-events: none;
  /* The core document paints #07070C behind a transparent canvas; screen drops
     that ground and keeps the bloom. */
  mix-blend-mode: screen;
  display: block;
}

#generate-hub span {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 18px;
  text-align: center;
  font: 700 11px/1 inherit;
  letter-spacing: .16em;
  color: var(--cyan);
  pointer-events: none;
}

#generate-hub:hover span { color: #fff; }
#generate-hub:focus-visible { outline: 2px solid var(--cyan); outline-offset: 4px; }

/* The dial is hidden below 1150px, so the core goes with it — no point
   decoding a looping video, or running a WebGL context, that nobody can see. */
@media (max-width: 1149px) {
  #generate-hub,
  #dial-face { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* Paused by script too; this keeps the still frame from being animated by
     anything else and leaves the idle PNG showing. */
  #generate-core { animation: none; }
}

#genre-preview {
  /* Under the circle, not inside it. It used to float at 64% of the canvas,
     which put a rounded rectangle across the dial's lower third — the one
     square-ish thing left after the card was stripped. Now it sits below the
     dial as its own chip. */
  position: relative;
  left: auto;
  top: auto;
  transform: none;
  z-index: 3;
  margin: -6px auto 0;
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.15s ease;
  width: 240px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border: 2px solid var(--cyan);
  border-radius: 24px;
  background: var(--surface-2);
  box-shadow: 0 0 24px rgba(var(--cyan-rgb), 0.35);
}

#genre-preview.visible {
  visibility: visible;
  opacity: 1;
}

#genre-preview video {
  /* Previews are audio-only mp3s — the element stays in the DOM as the
     audio source, but rendering it just showed an empty black rectangle. */
  display: none;
}

#genre-preview-sound {
  background: rgba(13, 13, 22, 0.75);
  border: 1px solid var(--dim);
  color: var(--text);
  border-radius: 50%;
  width: 26px;
  height: 26px;
  font-size: 12px;
  cursor: pointer;
  flex-shrink: 0;
}

#genre-preview-sound:hover {
  border-color: var(--cyan);
}

#genre-preview-label {
  color: var(--cyan);
  font-weight: bold;
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The pill lives under the dial on every screen size now, so mobile needs
   no special positioning — taps open it there just like hover does. */
@media (max-width: 900px) {
  #genre-preview {
    width: min(240px, 90%);
  }
}

/* A horizontal scroll strip rather than a tall sidebar — compact on
   desktop (no dead vertical space next to a short canvas-height column)
   and naturally mobile-friendly (a swipeable row instead of a narrow
   column squeezed under the dial). */
/* Grey plates so each area reads as its own card instead of floating on
   the page background — a genuinely lighter grey (like a search-bar pill),
   not another near-black. */
#history-section,
#queue-section,
#artbook-history-section {
  margin-top: 16px;
  background: var(--surface-3);
  border: 1px solid var(--line-strong);
  border-radius: 14px;
  padding: 16px 18px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.45);
}

/* One consistent heading tier for every sidebar/section title (Santiago's
   hierarchy note): small caps-style labels, clearly distinct from song
   titles below them. */
#history-title,
.section-title {
  color: var(--cyan);
  font-weight: bold;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  margin-bottom: 8px;
}

/* Dancing caveman mascot on his own little club stage: the render sits on
   pure black so it melts into the page background, with animated cyan and
   magenta laser beams sweeping behind him. */
/* A stage: the Floor tab's full-width one and the dock's expanded one. The
   canvas fills the box and mascot3d.js sizes its buffer to match. */
.mascot-stage {
  position: relative;
  perspective: 500px;
  overflow: hidden;
  width: 100%;
}

#floor-stage {
  aspect-ratio: 1 / 1;
  max-height: 62vh;
  margin-bottom: 12px;
}

@media (min-width: 700px) {
  #floor-stage {
    aspect-ratio: 16 / 9;
    /* max-height wins over aspect-ratio, so on a wide window the stage was
       rendering 1384x558 — an effective 2.48:1 with AMY centred and both
       outer thirds empty floor. Cap the WIDTH at whatever 16/9 allows inside
       the same height budget and the ratio is honoured instead of broken. */
    width: min(100%, calc(62vh * 16 / 9));
    margin-left: auto;
    margin-right: auto;
  }
}

#stage-amy-mount {
  aspect-ratio: 4 / 3;
  min-height: 240px;
  max-height: 52vh;
}

.mascot-canvas {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Lasers take their colours from the genre pair (see setMascotGenre). */
.laser {
  position: absolute;
  bottom: -10px;
  left: 50%;
  width: 3px;
  height: 110%;
  transform-origin: bottom center;
  border-radius: 2px;
}

.laser-1, .laser-3 {
  background: linear-gradient(to top, rgba(var(--stage-a-rgb), 0.85), transparent);
  box-shadow: 0 0 8px rgba(var(--stage-a-rgb), 0.8);
}

.laser-2, .laser-4 {
  background: linear-gradient(to top, rgba(var(--stage-b-rgb), 0.85), transparent);
  box-shadow: 0 0 8px rgba(var(--stage-b-rgb), 0.8);
}

.laser-1 { animation: laser-swing-l 2.1s ease-in-out infinite; }
.laser-2 { animation: laser-swing-r 1.7s ease-in-out infinite; }
.laser-3 { animation: laser-swing-r 2.6s ease-in-out infinite reverse; opacity: 0.5; }
.laser-4 { animation: laser-swing-l 1.4s ease-in-out infinite reverse; opacity: 0.5; }

@keyframes laser-swing-l {
  0%, 100% { transform: rotate(-38deg); }
  50% { transform: rotate(-8deg); }
}

@keyframes laser-swing-r {
  0%, 100% { transform: rotate(38deg); }
  50% { transform: rotate(8deg); }
}

@media (prefers-reduced-motion: reduce) {
  #mascot, .laser { animation: none; }
}

.mascot-name, #mascot-name {
  position: absolute;
  bottom: 6px;
  left: 0;
  right: 0;
  text-align: center;
  font-size: 10px;
  font-weight: bold;
  letter-spacing: 2px;
  color: var(--cyan);
  opacity: 0.8;
  text-shadow: 0 0 8px rgba(var(--cyan-rgb), 0.5);
  pointer-events: none;
}

#sidebar-tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 12px;
}

.sidebar-tab {
  flex: 1;
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  color: var(--text);
  border-radius: 10px;
  padding: 8px 4px;
  font-size: 12px;
  font-weight: bold;
  letter-spacing: 0.5px;
  cursor: pointer;
  opacity: 0.7;
}

.sidebar-tab.active {
  background: var(--surface-3);
  border-color: var(--cyan);
  color: var(--cyan);
  opacity: 1;
}

.sidebar-tab:hover {
  opacity: 1;
}

#live-tab-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  margin-left: 5px;
  background: transparent;
  vertical-align: middle;
}

#live-tab-dot.on {
  background: var(--magenta);
  box-shadow: 0 0 8px var(--magenta);
  animation: credits-pulse 1.4s ease-in-out infinite;
}


/* Cover-art generator busy state — same energy as the dial while a song
   renders: sweeping shimmer bar, rotating facts, pulsing modal frame. */
.art-shimmer {
  height: 4px;
  border-radius: 2px;
  margin-bottom: 8px;
  background: linear-gradient(90deg, transparent, var(--cyan), var(--magenta), transparent);
  background-size: 200% 100%;
  animation: art-sweep 1.4s linear infinite;
}

@keyframes art-sweep {
  from { background-position: 200% 0; }
  to { background-position: -200% 0; }
}

.art-fact {
  font-size: 12px;
  font-style: italic;
  color: var(--text);
  opacity: 0.85;
}

.btn.art-busy {
  animation: credits-pulse 1.4s ease-in-out infinite;
  border-color: var(--magenta);
  color: var(--magenta);
}

#artbook-modal.art-generating {
  border: 1px solid var(--magenta);
  box-shadow: 0 0 24px rgba(var(--magenta-rgb), 0.35);
}

#history-filters {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}

#history-filters input,
#history-filters select {
  padding: 6px 10px;
  background: var(--bg);
  border: 1px solid var(--dim);
  color: var(--text);
  border-radius: 6px;
  font-size: 12px;
}

#history-filters input { flex: 1; min-width: 0; }

#history-empty {
  color: var(--text);
  font-size: 12px;
  grid-column: 1 / -1;
}

#vault-view-toggle {
  padding: 6px 10px;
  font-size: 13px;
  line-height: 1;
  flex-shrink: 0;
}

/* ---------- cover tiles: the library as a wall of art ---------- */
/* Column count follows the container, not the viewport: 2 across on a
   phone, 2 in the desktop sidebar, and as many as fit once the library
   gets a full-width tab of its own. */
.cover-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 12px;
}

/* 130px is a phone minimum. Left alone it never rises, so a 1440px Vault
   showed 9 cramped 139px thumbnails — the grid was full but read as mobile.
   auto-fill still decides the count; this only raises the floor.

   Scoped to #tab-vault deliberately. A viewport breakpoint is only honest
   here because the Vault grid is full-bleed, so its container tracks the
   window. The same rule on a bare .cover-grid would misfire the moment one is
   reused in a narrow sidebar — that is exactly how an earlier version of this
   grid ended up with five 47px columns inside a 300px column. */
@media (min-width: 1150px) {
  #tab-vault .cover-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
}

@media (min-width: 1600px) {
  #tab-vault .cover-grid { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
}

.tile {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
  border-radius: 12px;
}

.tile-clickable { cursor: pointer; }

.tile-cover {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-1) center / cover no-repeat;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

.tile-clickable:hover .tile-cover,
.tile-clickable:focus-within .tile-cover {
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(var(--cyan-rgb), 0.45);
}

.tile-cover img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Procedural cover: genre photo + typeset title. The gradient keeps the
   title legible on any photo; the tint classes keep same-genre tiles from
   looking identical. */
.tile-cover.procedural::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(7, 7, 12, 0.92) 0%, rgba(7, 7, 12, 0.35) 45%, rgba(7, 7, 12, 0.05) 100%);
}

.tile-cover.proc-0::after,
.tile-cover.proc-1::after,
.tile-cover.proc-2::after,
.tile-cover.proc-3::after {
  content: "";
  position: absolute;
  inset: 0;
  mix-blend-mode: screen;
  opacity: 0.35;
  pointer-events: none;
}
.tile-cover.proc-0::after { background: radial-gradient(circle at 20% 20%, rgba(var(--cyan-rgb), 0.9), transparent 60%); }
.tile-cover.proc-1::after { background: radial-gradient(circle at 80% 20%, rgba(var(--magenta-rgb), 0.9), transparent 60%); }
.tile-cover.proc-2::after { background: radial-gradient(circle at 50% 10%, rgba(var(--violet-rgb), 0.9), transparent 60%); }
.tile-cover.proc-3::after { background: radial-gradient(circle at 80% 80%, rgba(255, 176, 71, 0.85), transparent 60%); }

.proc-genre {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 1;
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--cyan);
  background: rgba(7, 7, 12, 0.55);
  border: 1px solid rgba(var(--cyan-rgb), 0.35);
  border-radius: 10px;
  padding: 2px 7px;
}

.proc-title {
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 10px;
  z-index: 1;
  font-weight: 800;
  font-size: clamp(14px, 4vw, 22px);
  line-height: 1.05;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  color: #fff;
  text-shadow: 0 0 12px rgba(var(--cyan-rgb), 0.55), 0 2px 6px rgba(0, 0, 0, 0.8);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

.tile-play {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 2;
  width: 44px;
  height: 44px;
  margin: -22px 0 0 -22px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 16px;
  color: #fff;
  background: rgba(7, 7, 12, 0.7);
  border: 1px solid rgba(var(--cyan-rgb), 0.7);
  box-shadow: 0 0 16px rgba(var(--cyan-rgb), 0.45);
  opacity: 0;
  transition: opacity 0.15s ease;
  pointer-events: none;
}

.tile-clickable:hover .tile-play,
.tile-clickable:focus-within .tile-play { opacity: 1; }

@media (hover: none) {
  .tile-play { opacity: 0.55; }
}

.tile-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.tile-title {
  font-weight: bold;
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tile-artist {
  font-size: 11px;
  color: var(--text);
  opacity: 0.8;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tile-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 11px;
  color: var(--text);
}

.tile-meta .chip.genre {
  padding: 2px 8px;
  font-size: 10px;
  cursor: default;
  white-space: nowrap;
  max-width: 55%;
  overflow: hidden;
  text-overflow: ellipsis;
  flex-shrink: 0;
  color: var(--cyan);
  border-color: rgba(var(--cyan-rgb), 0.35);
}

.tile-stats {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.75;
}

/* Release state: what this song still needs before it's out. Missing chips are
   buttons where there's something to do about it, so the row is a to-do list
   rather than a label. */
.release-row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 2px 0 1px;
}

.rel-chip {
  font: inherit;
  font-size: 9.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid var(--dim);
  background: none;
  color: var(--text);
  opacity: 0.45;
  white-space: nowrap;
  text-decoration: none;
}

button.rel-chip { cursor: pointer; }

button.rel-chip:hover {
  opacity: 0.9;
  border-color: var(--cyan);
  color: var(--cyan);
}

.rel-chip.is-done {
  opacity: 1;
  color: var(--cyan);
  border-color: rgba(var(--cyan-rgb), 0.4);
}

/* Actually published — the one chip that links somewhere. */
.rel-chip.is-live {
  color: var(--magenta);
  border-color: rgba(var(--magenta-rgb), 0.5);
  background: rgba(var(--magenta-rgb), 0.08);
}

.rel-chip.is-live:hover { background: rgba(var(--magenta-rgb), 0.16); }

/* List view packs the row tight; the chips are the first thing to give. */
.cover-grid.is-list .release-row { display: none; }

.tile-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
}

.tile-actions > .btn,
.tile-actions .more-menu-wrap > .btn {
  padding: 5px 10px;
  font-size: 13px;
  line-height: 1;
}

/* List view: one row per song, small cover on the left. */
.cover-grid.is-list {
  grid-template-columns: 1fr;
  gap: 8px;
}

.cover-grid.is-list .tile,
.tile-compact {
  display: grid;
  grid-template-columns: 56px minmax(0, 1fr);
  gap: 10px;
  align-items: center;
  border: 1px solid var(--dim);
  border-radius: 8px;
  padding: 6px 8px;
  background: var(--panel-bg);
}

.cover-grid.is-list .tile:hover,
.tile-compact:hover { border-color: var(--cyan); }

.cover-grid.is-list .tile-cover,
.tile-compact .tile-cover {
  width: 56px;
  aspect-ratio: 1 / 1;
  border-radius: 6px;
  box-shadow: none;
  transform: none !important;
}

.cover-grid.is-list .proc-title,
.tile-compact .proc-title { display: none; }

.cover-grid.is-list .proc-genre,
.tile-compact .proc-genre { display: none; }

.cover-grid.is-list .tile-play,
.tile-compact .tile-play {
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  font-size: 11px;
}

.cover-grid.is-list .tile-body,
.tile-compact .tile-body {
  display: grid;
  grid-template-columns: minmax(90px, 1fr) max-content;
  grid-template-areas: "title actions" "meta actions";
  column-gap: 8px;
  row-gap: 2px;
  align-items: center;
}

/* The wide "Render Video" button belongs to the grid tile only; in a row
   it would squeeze the title to nothing (it's still in the ⋯ menu). */
.cover-grid.is-list .tile-actions .render-video-btn,
.tile-compact .tile-actions .render-video-btn { display: none; }

.cover-grid.is-list .tile-title,
.tile-compact .tile-title { grid-area: title; }

.cover-grid.is-list .tile-artist,
.tile-compact .tile-artist { grid-area: meta; }

.cover-grid.is-list .tile-meta,
.tile-compact .tile-meta { grid-area: meta; }

.cover-grid.is-list .tile-artist + .tile-meta,
.tile-compact .tile-artist + .tile-meta { display: none; }

.cover-grid.is-list .tile-actions,
.tile-compact .tile-actions { grid-area: actions; flex-wrap: nowrap; }

.more-menu-wrap {
  position: relative;
}

.more-menu {
  display: none;
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 50;
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  padding: 6px;
  /* Fixed width, not min-width: the menu is position:fixed when open, and
     an auto width there stretches to fit the widest possible line. */
  width: 230px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.6);
}

.more-menu.open {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.more-menu .more-item,
.more-menu .btn {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  color: var(--text);
  padding: 8px 10px;
  border-radius: 6px;
  font-size: 12px;
  cursor: pointer;
  opacity: 1;
  box-shadow: none;
  /* The SoundCloud entry is an <a> for the non-Diamond case, so it needs the
     underline off and the box model matched to the buttons around it. */
  text-decoration: none;
  box-sizing: border-box;
}

.more-menu .more-item:hover,
.more-menu .btn:hover {
  background: var(--surface-3);
  color: var(--cyan);
  transform: none;
  box-shadow: none;
}

canvas#dashboard {
  /* No background. This plus the clearRect in animate() is what removes the
     dark card around the dial — either one alone leaves the square. */
  background: radial-gradient(circle at 50% 50%,
    rgba(10, 10, 18, .55) 0%, rgba(7, 7, 12, .15) 70%, transparent 72%);
  clip-path: circle(50% at 50% 50%);
  border-radius: 50%;
  position: relative;
  z-index: 2;
  cursor: pointer;
  /* Logical 760x640 grid drawn on a 2x backing store (see buildDashboard),
     so it can stretch to fill a wide desktop column without going soft and
     still shrink to fit phones — pointer handlers map screen coords back to
     the logical grid. */
  width: 100%;
  /* Cap by viewport height too, not just width, so the bottom genre nodes
     never fall below the fold. The grid is square now, so the height cap is
     the height budget itself.

     --dial-chrome is everything that shares the vertical budget with the dial.
     The old flat 170px was measured wrong: under the canvas sit #genre-preview
     (50px) and #mix-panel (279px), plus the 84px fixed radio bar that body
     reserves — 377px, not 170. Being 207px short is what made the page scroll
     on every desktop, and worse the wider the monitor, since a square dial
     grows its height with its width. */
  max-width: min(1400px, calc(100vh - var(--dial-chrome)));
  height: auto;
  display: block;
  margin: 0 auto;
}

#panel {
  width: 100%;
  max-width: 1400px;
  margin-left: auto;
  margin-right: auto;
  /* Same clearly-lighter grey plate as the section cards. */
  background: var(--surface-3);
  border: 1px solid var(--line-strong);
  border-radius: 14px;
  padding: 16px 18px;
  margin-top: 10px;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.45);
}

/* The status/progress area gets its own standout card inside the panel,
   with a cyan accent edge — and comes alive while a render runs (EQ pulse,
   shimmering bar, percentage readout). */
#status-card {
  background: linear-gradient(100deg, rgba(var(--cyan-rgb), 0.1), rgba(var(--violet-rgb), 0.08) 55%, rgba(var(--magenta-rgb), 0.1));
  border: 1px solid var(--line-strong);
  border-left: 3px solid var(--cyan);
  border-radius: 12px;
  padding: 12px 16px;
  margin-bottom: 12px;
}

#status-card.busy {
  border-left-color: var(--magenta);
  box-shadow: 0 0 22px rgba(var(--magenta-rgb), 0.18);
}

#status-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 8px;
  gap: 10px;
}

#status-left {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

#status-right {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-shrink: 0;
}

#progress-pct {
  color: var(--magenta);
  font-weight: bold;
  font-size: 14px;
}

/* Tiny animated equalizer shown while rendering */
#status-eq {
  display: inline-flex;
  align-items: flex-end;
  gap: 2px;
  height: 14px;
  flex-shrink: 0;
}

#status-eq i {
  width: 3px;
  background: var(--cyan);
  border-radius: 1px;
  animation: eq-bounce 0.9s ease-in-out infinite;
}

#status-eq i:nth-child(1) { animation-delay: 0s; }
#status-eq i:nth-child(2) { animation-delay: 0.22s; }
#status-eq i:nth-child(3) { animation-delay: 0.44s; }
#status-eq i:nth-child(4) { animation-delay: 0.66s; }

@keyframes eq-bounce {
  0%, 100% { height: 4px; }
  50% { height: 14px; }
}

/* Same equalizer, magenta variant, inside the genre "now playing" pill */
.preview-eq {
  display: inline-flex;
  align-items: flex-end;
  gap: 2px;
  height: 14px;
  flex-shrink: 0;
}

.preview-eq i {
  width: 3px;
  background: var(--magenta);
  border-radius: 1px;
  height: 4px;
  animation: eq-bounce 0.9s ease-in-out infinite;
  /* Paused by default — it used to animate the instant the preview panel
     appeared on hover, before any audio had actually started, which read
     as something already playing (Santiago's feedback). JS toggles
     .is-playing on the video's real play/pause/ended events. */
  animation-play-state: paused;
}
.preview-eq.is-playing i {
  animation-play-state: running;
}

.preview-eq i:nth-child(1) { animation-delay: 0s; }
.preview-eq i:nth-child(2) { animation-delay: 0.22s; }
.preview-eq i:nth-child(3) { animation-delay: 0.44s; }
.preview-eq i:nth-child(4) { animation-delay: 0.66s; }

#genre-preview-label {
  flex: 1;
  min-width: 0;
}

@media (prefers-reduced-motion: reduce) {
  #status-eq i, .preview-eq i, #radio-icon.mini-eq i { animation: none; height: 8px; }
}

#status-line {
  color: var(--cyan);
  font-weight: bold;
}

#elapsed-timer {
  color: var(--text-dim);
  font-size: 12px;
  white-space: nowrap;
}

#fun-fact-line {
  color: var(--text-dim);
  font-size: 12px;
  font-style: italic;
  min-height: 16px;
  margin-bottom: 10px;
  opacity: 0;
  transition: opacity 0.4s ease;
}

#fun-fact-line.visible {
  opacity: 1;
}

#result-player {
  width: 100%;
  max-height: 360px;
  border-radius: 8px;
  background: black;
  margin-bottom: 10px;
}

#progress-track {
  width: 100%;
  height: 10px;
  background: var(--dim);
  border-radius: 5px;
  overflow: hidden;
  margin-bottom: 10px;
}

#progress-fill {
  height: 100%;
  width: 0%;
  background: var(--cyan);
  transition: width 0.3s ease;
}

/* Moving shimmer across the fill while a render runs */
#status-card.busy #progress-fill {
  background: linear-gradient(90deg, var(--cyan) 0%, #7dfff2 30%, var(--magenta) 50%, #7dfff2 70%, var(--cyan) 100%);
  background-size: 200% 100%;
  animation: progress-shimmer 1.6s linear infinite;
}

@keyframes progress-shimmer {
  from { background-position: 200% 0; }
  to { background-position: 0% 0; }
}

@media (prefers-reduced-motion: reduce) {
  #status-card.busy #progress-fill { animation: none; background: var(--cyan); }
}

/* ---------- The setlist ----------
   One row per act. Resting state is the readable one: every row is legible
   without animation, so reduced motion loses only the pulse, not the meaning. */
#setlist {
  list-style: none;
  margin: 0 0 10px;
  padding: 0;
  display: grid;
  gap: 2px;
}

.act {
  display: grid;
  grid-template-columns: 16px 1fr;
  align-items: baseline;
  gap: 8px;
  padding: 5px 0;
  font-size: 13px;
  color: var(--text);
  opacity: 0.4; /* upcoming */
}

.act-dot {
  width: 8px;
  height: 8px;
  margin: 0 auto;
  border-radius: 50%;
  background: var(--dim);
  align-self: center;
}

.act.is-done { opacity: 0.65; }
.act.is-done .act-dot { background: var(--cyan); }
/* A tick drawn in CSS rather than a glyph, so it can't pick up an emoji font. */
.act.is-done .act-dot {
  width: 10px;
  height: 6px;
  border-radius: 0;
  background: none;
  border-left: 2px solid var(--cyan);
  border-bottom: 2px solid var(--cyan);
  transform: rotate(-45deg) translate(1px, -1px);
}

.act.is-now {
  opacity: 1;
  color: #fff;
}

.act.is-now .act-dot {
  background: var(--magenta);
  box-shadow: 0 0 0 4px rgba(var(--magenta-rgb), 0.18);
  animation: act-pulse 1.4s ease-in-out infinite;
}

@keyframes act-pulse {
  50% { box-shadow: 0 0 0 7px rgba(var(--magenta-rgb), 0.06); }
}

/* The server's own sentence, attached to the act it describes. */
.act-said {
  grid-column: 2;
  display: block;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--text);
  opacity: 0.75;
}

.act-said:empty { display: none; }

@media (prefers-reduced-motion: reduce) {
  .act.is-now .act-dot { animation: none; }
}

.btn {
  background: var(--panel-bg);
  color: var(--text);
  border: 1px solid var(--dim);
  border-radius: 6px;
  padding: 8px 14px;
  cursor: pointer;
  font-size: 13px;
  /* Rest dimmed, ignite on hover (Santiago's feedback) */
  opacity: 0.65;
  transition: opacity 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease, transform 0.15s ease;
}

.btn:hover {
  border-color: var(--cyan);
  opacity: 1;
  box-shadow: 0 0 14px rgba(var(--cyan-rgb), 0.35);
  transform: translateY(-1px);
}
.btn:disabled { opacity: 0.4; cursor: default; box-shadow: none; transform: none; }

/* Every primary action on the mockups is the same magenta-to-cyan ramp, not a
   flat fill: "Generate this track", "Continue — start free", "Download all
   4 files", "Open the app". One rule, so they can't drift apart. */
.btn-primary {
  background: var(--grad-cta);
  color: #fff;
  border: none;
  font-weight: 700;
  border-radius: 10px;
  box-shadow: 0 6px 22px rgba(var(--magenta-rgb), 0.28);
}

.btn-primary:hover {
  /* Lift, don't recolour — the ramp is the identity. */
  box-shadow: 0 8px 28px rgba(var(--magenta-rgb), 0.42);
  filter: brightness(1.06);
}

#downloads { margin-top: 10px; display: flex; gap: 10px; flex-wrap: wrap; }

#pack-modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  /* The dial has to recede — it is bright, animated and directly behind this. */
  background: rgba(0, 0, 0, 0.72);
  backdrop-filter: blur(8px);
  align-items: center;
  justify-content: center;
  z-index: 100;
  padding: 16px;
  overflow-y: auto;
}

#pack-modal-backdrop.visible {
  display: flex;
}

#pack-modal {
  /* Dark glass, not a flat panel: it sits over the blurred dial. */
  background: linear-gradient(180deg, rgba(20, 20, 31, 0.96), rgba(13, 13, 22, 0.98));
  border: 1px solid var(--line-strong);
  border-radius: 16px;
  padding: 26px 24px 18px;
  width: 100%;
  max-width: 720px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
}

#pack-modal-head { text-align: center; margin-bottom: 20px; }

#pack-modal-coin {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
  filter: drop-shadow(0 0 18px rgba(255, 210, 90, 0.45));
}

#pack-modal-sub {
  margin: 6px 0 0;
  font-size: 13px;
  color: var(--text-dim);
}

#pack-modal h2 {
  color: var(--cyan);
  margin: 0 0 16px;
  font-size: 18px;
}

#artist-modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  align-items: center;
  justify-content: center;
  z-index: 100;
}

#artist-modal-backdrop.visible {
  display: flex;
}

#artist-modal {
  background: var(--panel-bg);
  border: 1px solid var(--dim);
  border-radius: 10px;
  padding: 24px;
  width: 90%;
  max-width: 420px;
}

#artist-modal h2 {
  color: var(--cyan);
  margin: 0 0 8px;
  font-size: 18px;
}

#artist-name-input {
  background: var(--bg);
  border: 1px solid var(--dim);
  color: var(--text);
  border-radius: 6px;
}

.suggestion-chip {
  display: inline-block;
  border: 1px solid var(--dim);
  border-radius: 12px;
  padding: 3px 10px;
  margin: 2px 4px 2px 0;
  cursor: pointer;
}

.suggestion-chip:hover {
  border-color: var(--cyan);
  color: var(--cyan);
}

.pack-option {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border: 1px solid var(--dim);
  border-radius: 8px;
  padding: 12px 14px;
  margin-bottom: 10px;
  cursor: pointer;
  background: var(--bg);
}

.pack-option:hover {
  border-color: var(--cyan);
}

.pack-option .pack-label {
  font-weight: bold;
}

.pack-option .pack-price {
  color: var(--magenta);
  font-weight: bold;
}

#pack-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
  margin-bottom: 4px;
}

@media (max-width: 640px) {
  #pack-list { grid-template-columns: 1fr; }
}

.pack-card {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--dim);
  border-radius: 10px;
  padding: 20px 14px 14px;
  background: var(--bg);
  text-align: center;
}

/* Only the badge survives from the old featured styling; the default plan
   carries the emphasis now. */
.pack-card.featured { border-color: var(--line-strong); }

/* Popular is the default: lifted, gradient-rimmed, and the only filled button.
   The border is a gradient painted into the padding box, which a plain
   border-color cannot do. */
.pack-card.is-default {
  transform: scale(1.06);
  z-index: 1;
  border-color: transparent;
  background:
    linear-gradient(var(--bg), var(--bg)) padding-box,
    var(--grad-brand) border-box;
  box-shadow: 0 12px 40px rgba(var(--magenta-rgb), 0.22);
}

.pack-card-badge {
  position: absolute;
  top: -11px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--magenta);
  color: #fff;
  font-size: 10px;
  font-weight: bold;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px 10px;
  border-radius: 10px;
  white-space: nowrap;
}

.pack-card-name {
  font-weight: bold;
  font-size: 15px;
}

.pack-card-price {
  color: var(--magenta);
  font-weight: bold;
  font-size: 24px;
  margin: 8px 0 0;
}

/* Sits under the button, as the quiet justification for the price above it.
   --dim is a hairline colour and was close to invisible as text. */
.pack-card-per-song {
  color: var(--text-dim);
  font-size: 11px;
  margin-top: 10px;
}

.pack-card-songs {
  color: var(--text-dim);
  font-size: 12px;
  margin-bottom: 12px;
}

.pack-card-btn { width: 100%; }

/* Outline for the two non-default plans, so one filled button leads. */
.pack-card:not(.is-default) .pack-card-btn {
  background: none;
  border: 1px solid var(--line-strong);
  color: var(--text);
}

.pack-card:not(.is-default) .pack-card-btn:hover {
  border-color: var(--cyan);
  color: var(--cyan);
}

#pack-modal-close {
  display: block;
  margin: 16px auto 0;
  background: none;
  border: 0;
  color: var(--text-dim);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  padding: 8px 14px;
}

#pack-modal-close:hover { color: var(--text); text-decoration: underline; }

/* Phones: one column, and the default plan leads. */
@media (max-width: 700px) {
  #pack-list { grid-template-columns: 1fr; }
  .pack-card.is-default { order: -1; transform: none; }
}

.pack-card-features {
  list-style: none;
  padding: 0;
  margin: 0 0 16px;
  font-size: 12px;
  text-align: left;
  flex: 1;
}

.pack-card-features li {
  padding: 4px 0 4px 18px;
  position: relative;
}

.pack-card-features li::before {
  content: "\2713";
  position: absolute;
  left: 0;
  color: var(--cyan);
}

.pack-card-btn {
  width: 100%;
}

#tier-modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  align-items: center;
  justify-content: center;
  z-index: 100;
}

#tier-modal-backdrop.visible {
  display: flex;
}

#tier-modal {
  background: var(--panel-bg);
  border: 1px solid var(--dim);
  border-radius: 10px;
  padding: 24px;
  width: 90%;
  max-width: 440px;
}

#tier-modal h2 {
  color: var(--cyan);
  margin: 0 0 4px;
  font-size: 18px;
}

#tier-modal-close {
  width: 100%;
  margin-top: 6px;
}

.tier-option-desc {
  font-size: 11px;
  color: var(--text);
  margin-top: 2px;
}

#activity-modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  align-items: center;
  justify-content: center;
  z-index: 100;
}

#activity-modal-backdrop.visible { display: flex; }

#activity-modal {
  background: var(--surface-1);
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  padding: 22px;
  width: 92%;
  max-width: 680px;
}

#activity-modal h2 {
  color: var(--cyan);
  margin: 0 0 10px;
  font-size: 18px;
}

#activity-summary {
  color: var(--text);
  font-size: 12px;
  margin-bottom: 10px;
}

#activity-list {
  max-height: 420px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 12px;
}

.activity-row {
  display: grid;
  grid-template-columns: 92px 1fr auto;
  gap: 10px;
  align-items: baseline;
  padding: 7px 10px;
  background: var(--panel-bg);
  border: 1px solid var(--dim);
  border-radius: 6px;
  font-size: 12px;
  color: var(--text);
}

.activity-row .a-when { color: var(--text-dim); font-size: 11px; }
.activity-row .a-title b { color: var(--cyan); }
.activity-row .a-who { color: var(--magenta); text-align: right; }
.activity-row .a-who small { color: var(--text-dim); display: block; }
.activity-row.a-error .a-title b { color: #ff6a6a; }

#settings-modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  align-items: center;
  justify-content: center;
  z-index: 100;
}

#settings-modal-backdrop.visible { display: flex; }

#settings-modal {
  background: var(--panel-bg);
  border: 1px solid var(--dim);
  border-radius: 10px;
  padding: 24px;
  width: 90%;
  max-width: 460px;
}

#settings-modal h2 {
  color: var(--cyan);
  margin: 0 0 16px;
  font-size: 18px;
}

.settings-section { margin-bottom: 18px; }

.settings-label {
  color: var(--cyan);
  font-weight: bold;
  font-size: 13px;
  margin-bottom: 6px;
}

.settings-hint {
  color: var(--dim);
  font-weight: normal;
  font-size: 11px;
}

.settings-row { display: flex; gap: 8px; }

.settings-row input {
  flex: 1;
  padding: 7px 10px;
  background: var(--bg);
  border: 1px solid var(--dim);
  color: var(--text);
  border-radius: 6px;
  font-size: 13px;
}

#settings-plan {
  color: var(--text);
  font-size: 13px;
  line-height: 1.6;
}

#settings-plan b { color: var(--magenta); text-transform: capitalize; }

#settings-status {
  color: var(--cyan);
  font-size: 12px;
  min-height: 18px;
  margin-bottom: 10px;
}

#demo-modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.8);
  align-items: center;
  justify-content: center;
  z-index: 100;
}

#demo-modal-backdrop.visible {
  display: flex;
}

#demo-modal {
  background: var(--panel-bg);
  border: 1px solid var(--dim);
  border-radius: 10px;
  padding: 16px;
  width: 90%;
  max-width: 900px;
}

#demo-modal-video {
  width: 100%;
  border-radius: 6px;
  display: block;
  background: black;
  margin-bottom: 12px;
}

/* Full-width player bar pinned to the bottom of the page, styled like the
   rest of the app (Santiago's feedback: static bar, prev/next controls). */
/* The dock. NO overflow:hidden anywhere on it — #reaction-picker opens
   upward out of the bar on purpose (bottom: calc(100% + 10px)), so only the
   blurred backdrop layer clips itself. body's side padding is overridden
   because the dock is a body child. */
#radio-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(var(--tabbar-h) + var(--safe-b));
  height: var(--dock-h);
  z-index: 90;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: 0 14px 0 12px !important;
  background: rgba(13, 13, 22, 0.94);
  border-top: 1px solid var(--dim);
  box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(6px);
}

#dock-bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  border-top: 1px solid transparent;
}

#dock-bg::before {
  content: "";
  position: absolute;
  inset: -24px;
  background: inherit;
  background-size: cover;
  background-position: center;
  filter: blur(22px) brightness(0.45) saturate(1.3);
}

#dock-bg { background-size: cover; background-position: center; background-image: none; }
#dock-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to right, rgba(13, 13, 22, 0.55), rgba(13, 13, 22, 0.85));
}

.radio-bar-top {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 1;
  min-height: 0;
}

#dock-meta {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

#radio-now .dock-title {
  display: block;
  color: #fff;
  font-size: 13px;
  font-weight: bold;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#radio-now .dock-artist {
  display: block;
  font-size: 11px;
  color: var(--text);
  opacity: 0.8;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#dock-wave {
  width: 160px;
  height: 36px;
  flex-shrink: 0;
  opacity: 0.9;
}

#dock-expand-btn {
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--dim);
  background: rgba(7, 7, 12, 0.5);
  color: var(--text);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

#dock-expand-btn:hover { border-color: var(--cyan); color: var(--cyan); }

#radio-bar.has-track { cursor: pointer; }

/* --- Stage: the dock, expanded --- */
#stage-overlay {
  position: fixed;
  inset: 0;
  bottom: calc(var(--dock-h) + var(--tabbar-h) + var(--safe-b));
  z-index: 95;
  background: var(--bg);
  overflow: auto;
  padding: 0 !important;
}

body.stage-open { overflow: hidden; }

#stage-inner {
  /* The overlay is full-bleed; capping the content at 1100 left ~170px of
     empty backdrop either side on a 1440 window while the queue column stayed
     cramped. */
  max-width: 1360px;
  margin: 0 auto;
  padding: 18px 16px 24px;
  display: grid;
  gap: 18px;
}

#stage-close {
  justify-self: center;
  width: 44px;
  height: 30px;
  border-radius: 15px;
  border: 1px solid var(--dim);
  background: var(--panel-bg);
  color: var(--text);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

#stage-hero {
  display: grid;
  gap: 14px;
  justify-items: center;
  text-align: center;
}

#stage-cover {
  width: min(70vw, 340px);
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 14px;
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.06);
  background: var(--surface-1);
}

#stage-title {
  font-size: 22px;
  font-weight: 800;
  color: #fff;
}

#stage-artist {
  font-size: 13px;
  color: var(--text);
  opacity: 0.85;
}

#stage-amy-mount { min-height: 0; }

#stage-side { display: grid; gap: 10px; }

#stage-lyrics {
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}

.stage-lyrics-block { margin-bottom: 12px; }

.stage-lyrics-label {
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--cyan);
  margin-bottom: 2px;
}

.stage-lyrics-note { color: var(--text); opacity: 0.6; font-size: 12px; }

#stage-queue { display: grid; gap: 8px; }

@media (min-width: 900px) {
  #stage-inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: "close close" "hero side" "amy side";
    align-items: start;
  }
  #stage-close { grid-area: close; }
  #stage-hero { grid-area: hero; }
  #stage-amy-mount { grid-area: amy; }
  #stage-side { grid-area: side; }
}

/* Mini equalizer used as the player-bar icon — same pattern as .preview-eq,
   cyan to match the bar's existing accent. Paused by default; JS toggles
   .is-playing to match whatever radio.audio is actually doing, so it never
   implies playback that isn't happening. */
#radio-like-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  background: none;
  border: 0;
  cursor: pointer;
  padding: 4px;
  flex-shrink: 0;
}

#radio-like-icon {
  width: 20px;
  height: 20px;
  transition: transform 0.15s ease;
}

#radio-like-btn:active #radio-like-icon {
  transform: scale(1.3);
}

#radio-like-count {
  font-size: 11px;
  color: var(--dim);
  min-width: 8px;
}

#radio-art {
  width: 48px;
  height: 48px;
  border-radius: 6px;
  object-fit: cover;
  flex-shrink: 0;
  background: var(--surface-1);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
}


#radio-icon.mini-eq {
  display: inline-flex;
  align-items: flex-end;
  gap: 2px;
  height: 14px;
  flex-shrink: 0;
  filter: drop-shadow(0 0 8px rgba(var(--cyan-rgb), 0.45));
}

#radio-icon.mini-eq i {
  width: 3px;
  height: 4px;
  background: var(--cyan);
  border-radius: 1px;
  animation: eq-bounce 0.9s ease-in-out infinite;
  animation-play-state: paused;
}

#radio-icon.mini-eq.is-playing i { animation-play-state: running; }
#radio-icon.mini-eq i:nth-child(1) { animation-delay: 0s; }
#radio-icon.mini-eq i:nth-child(2) { animation-delay: 0.22s; }
#radio-icon.mini-eq i:nth-child(3) { animation-delay: 0.44s; }
#radio-icon.mini-eq i:nth-child(4) { animation-delay: 0.66s; }

/* Seek lives as a thin strip along the dock's top edge; the time labels sit
   at its ends on desktop and disappear on phones. */
.radio-bar-progress {
  position: absolute;
  top: -1px;
  left: 0;
  right: 0;
  height: 14px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  z-index: 2;
}

#radio-time-cur, #radio-time-dur {
  font-size: 11px;
  color: var(--dim);
  flex-shrink: 0;
  min-width: 32px;
  font-variant-numeric: tabular-nums;
}
#radio-time-dur { text-align: right; }

#radio-seek {
  flex: 1;
  accent-color: var(--cyan);
  height: 4px;
}

#radio-now {
  font-size: 13px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

#radio-now b {
  color: var(--magenta);
}

#radio-controls {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}

#radio-controls .btn {
  padding: 5px 12px;
}

#player-volume {
  width: 90px;
  flex-shrink: 0;
  accent-color: var(--cyan);
}

#radio-latest {
  flex-shrink: 0;
  max-width: 300px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  color: var(--text);
  border-radius: 16px;
  padding: 5px 12px;
  font-size: 12px;
  cursor: pointer;
}

#radio-latest:hover {
  border-color: var(--cyan);
  color: var(--cyan);
}

/* Keep page content from hiding behind the fixed dock (+ tab bar on phones). */
body {
  padding-bottom: calc(var(--dock-h) + var(--tabbar-h) + var(--safe-b) + 12px);
}

#chat-fab {
  position: fixed;
  right: 22px;
  bottom: calc(var(--dock-h) + var(--tabbar-h) + var(--safe-b) + 14px);
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 1px solid var(--cyan);
  background: var(--panel-bg);
  color: var(--cyan);
  font-size: 22px;
  font-weight: bold;
  cursor: pointer;
  z-index: 90;
  box-shadow: 0 0 18px rgba(var(--cyan-rgb), 0.35);
}

#chat-fab:hover {
  background: #10202a;
}

/* AMY is the help button now — not a glyph and not a robot. The image fills
   the circle, so the button's own padding would only crop her. */
#chat-fab {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  overflow: hidden;
}

/* contain, not cover: the file is already framed, and cover would crop it a
   second time. The padding gives the framing room inside the circle. */
#chat-fab img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  padding: 6%;
  background: #07070C;
  border-radius: 50%;
  display: block;
  box-sizing: border-box;
}

#chat-modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  align-items: center;
  justify-content: center;
  z-index: 100;
}

#chat-modal-backdrop.visible {
  display: flex;
}

#chat-modal {
  background: var(--panel-bg);
  border: 1px solid var(--dim);
  border-radius: 10px;
  padding: 24px;
  width: 90%;
  max-width: 440px;
}

#chat-modal h2 {
  color: var(--cyan);
  margin: 0 0 12px;
  font-size: 18px;
}

#chat-log {
  height: 260px;
  overflow-y: auto;
  border: 1px solid var(--dim);
  border-radius: 6px;
  padding: 10px;
  margin-bottom: 10px;
  font-size: 13px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.chat-msg {
  padding: 8px 12px;
  border-radius: 8px;
  max-width: 85%;
  color: var(--text);
  white-space: pre-wrap;
}

.chat-msg.user {
  align-self: flex-end;
  background: #10202a;
  border: 1px solid var(--cyan);
}

.chat-msg.assistant {
  align-self: flex-start;
  background: var(--bg);
  border: 1px solid var(--dim);
}

#chat-input-row {
  display: flex;
  gap: 8px;
}

#chat-input {
  flex: 1;
  padding: 8px 10px;
  background: var(--bg);
  border: 1px solid var(--dim);
  color: var(--text);
  border-radius: 6px;
}

#artbook-modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  align-items: center;
  justify-content: center;
  z-index: 100;
}

#artbook-modal-backdrop.visible {
  display: flex;
}

#artbook-modal {
  background: var(--panel-bg);
  border: 1px solid var(--dim);
  border-radius: 10px;
  padding: 24px;
  width: 90%;
  max-width: 480px;
}

#artbook-modal h2 {
  color: var(--cyan);
  margin: 0 0 16px;
  font-size: 18px;
}

#artbook-prompt-input {
  background: var(--bg);
  border: 1px solid var(--dim);
  color: var(--text);
  border-radius: 6px;
  resize: vertical;
}

#artbook-result,
#artbook-inline-result {
  display: grid;
  /* auto-fill (not auto-fit) keeps empty tracks instead of collapsing them
     — with auto-fit, a single thumbnail (the common case: pick as they
     arrive one at a time) had no other tracks to share the row with, so
     its 1fr let it stretch to the full container width. */
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 10px;
}

.artbook-existing-heading {
  grid-column: 1 / -1;
  font-size: 12px;
  color: var(--dim);
  margin: 0 0 2px;
}

.artbook-suggestion {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.artbook-suggestion img {
  width: 100%;
  border-radius: 6px;
  display: block;
}

.artbook-suggestion .btn {
  width: 100%;
}

.artbook-use-for-select {
  width: 100%;
  padding: 4px 6px;
  background: var(--bg);
  border: 1px solid var(--dim);
  color: var(--text);
  border-radius: 6px;
  font-size: 11px;
}


.artbook-suggestion-row {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  padding-bottom: 6px;
}

.artbook-suggestion-row .artbook-suggestion {
  flex: 0 0 140px;
}

#audio-only-row {
  display: block;
  color: var(--text);
  font-size: 13px;
  margin-bottom: 10px;
  cursor: pointer;
}

#audio-only-row input {
  margin-right: 6px;
}

#lyrics-row {
  display: flex;
  gap: 8px;
  margin-bottom: 10px;
}

#lyrics-hook-input {
  flex: 1;
  padding: 6px 10px;
  background: var(--bg);
  border: 1px solid var(--dim);
  color: var(--text);
  border-radius: 6px;
  font-size: 13px;
}

#lyrics-suggest-btn {
  flex-shrink: 0;
  white-space: nowrap;
}

#voice-style-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  color: var(--text);
  font-size: 13px;
  margin-top: -8px;
  padding: 8px 20px;
  border: 1px solid var(--line-strong);
  border-radius: 14px;
  background: var(--surface-3);
}

#voice-style-label {
  color: var(--cyan);
  font-weight: bold;
}

#voice-style-row label {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

#artbook-inline-title {
  color: var(--cyan);
  font-weight: bold;
  font-size: 13px;
  margin-bottom: 6px;
}

#artbook-inline-prompt {
  width: 100%;
  padding: 8px;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--dim);
  color: var(--text);
  border-radius: 6px;
  resize: vertical;
  margin-bottom: 8px;
}

#artbook-inline-status {
  color: var(--text);
  font-size: 13px;
  margin: 8px 0;
}

/* Wide desktop: the centered single column (max-width:1000px) leaves dead
   space either side on large monitors, so split into a left column for the
   dial + generation panel and a right sidebar for history/art, using the
   canvas's own 760px width as the left column's basis. Below this the
   single-column stacked layout above is untouched. */
@media (min-width: 1150px) {
  /* Full-bleed: no centered max-width cap — the dial column takes all the
     width the monitor has (canvas itself caps at 1200px and centers), the
     sidebar hugs the right edge. Side padding is the only margin. */
  #top-bar {
    max-width: none;
    padding-left: 28px;
    padding-right: 28px;
  }

  #app-ui.visible {
    max-width: none;
    padding: 0 28px;
  }
}

/* Vault: the cover-art history wraps into a compact tile grid instead of
   the one-row strip it used inline under the panel. */
#tab-vault .artbook-suggestion-row {
  flex-direction: row;
  flex-wrap: wrap;
  overflow-x: visible;
}

#tab-vault .artbook-suggestion-row .artbook-suggestion {
  flex: none;
  width: 130px;
}

#tab-vault #history-section { margin-top: 0; }

#tab-floor #floor-stage { margin-bottom: 12px; }

#floor-latest-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 12px 0 4px;
}

#floor-latest-row .section-title { margin: 0; }

#dev-auth-banner {
  width: 100%;
  max-width: 760px;
  background: #2a1a10;
  border: 1px solid #a05820;
  color: #ffcf9a;
  border-radius: 6px;
  padding: 8px 12px;
  font-size: 12px;
  margin-bottom: 10px;
}

/* ---------- "New track" card ---------------------------------------------
   Form on the left, dial on the right (the layout Santiago's mockup used),
   keeping this app's own palette, buttons and radial rather than the
   mockup's. */
#panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 2px;
}

#panel-eyebrow {
  font-size: 11px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--text);
  opacity: 0.55;
}

/* On the board this card is titled with the style you're about to render, sat
   over a hairline rule — not with a question. A genre is always selected now,
   so the card can state it. */
#panel-title {
  margin: 0 0 14px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--dim);
  font-size: 26px;
  font-weight: 600;
  color: #fff;
  letter-spacing: 0;
}

/* The Fine-tune drawer: one collapsible for tempo / vocals / hook / video /
   mix, instead of five stacked cards under the button. */
#fine-tune {
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  background: var(--surface-2);
  margin: 0 0 14px;
}

#fine-tune > summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  cursor: pointer;
  user-select: none;
}

#fine-tune > summary::-webkit-details-marker { display: none; }

.drawer-title {
  font-size: 12px;
  font-weight: bold;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--cyan);
}

.drawer-hint {
  flex: 1;
  font-size: 11px;
  color: var(--text);
  opacity: 0.6;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.drawer-chev {
  font-size: 16px;
  line-height: 1;
  transition: transform 0.15s ease;
}

#fine-tune[open] > .drawer-chev,
#fine-tune[open] > summary .drawer-chev { transform: rotate(180deg); }

#fine-tune > #pane-prompt { padding: 0 14px 14px; }

#lyrics-row {
  position: relative;
}

#lyrics-hook-input {
  width: 100%;
  resize: vertical;
  min-height: 88px;
  padding: 12px 14px 26px;
  border-radius: 12px;
  border: 1px solid var(--cyan);
  background: var(--surface-1);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  line-height: 1.5;
}

#lyrics-hook-input:focus {
  outline: none;
  box-shadow: 0 0 0 1px var(--cyan) inset, 0 0 18px rgba(var(--cyan-rgb), 0.25);
}

#hook-count {
  position: absolute;
  right: 12px;
  bottom: 8px;
  font-size: 11px;
  color: var(--text);
  opacity: 0.45;
  pointer-events: none;
}

#hook-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
}

.chip {
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  color: var(--text);
  border-radius: 18px;
  padding: 7px 14px;
  font-size: 12px;
  cursor: pointer;
}

.chip:hover {
  border-color: var(--cyan);
  color: var(--cyan);
}

/* Genre mosaic. Phones: full-bleed 3-up photo tiles (the picker — the dial
   is hidden there). Tablets: a snap-scrolling strip under the dial.
   Desktop: gone, the dial is the picker. */
#style-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 3px;
  margin: 8px -16px 14px;
}

.style-cell {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 2px;
  text-align: left;
  aspect-ratio: 1 / 1;
  padding: 8px 9px;
  border: 0;
  border-radius: 0;
  background-color: var(--surface-2);
  background-size: cover;
  background-position: center;
  color: var(--text);
  cursor: pointer;
  font: inherit;
  position: relative;
  min-width: 0;
}

.style-cell.active::after {
  content: "▶";
  position: absolute;
  top: 8px;
  right: 8px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 11px;
  color: #fff;
  background: rgba(7, 7, 12, 0.7);
  border: 1px solid rgba(var(--cyan-rgb), 0.8);
}

.style-cell.active.is-playing::after { content: "❚❚"; }

@media (min-width: 700px) {
  #style-grid {
    display: flex;
    gap: 8px;
    margin: 12px 0 0;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    padding-bottom: 6px;
  }
  .style-cell {
    flex: 0 0 120px;
    aspect-ratio: auto;
    min-height: 74px;
    border-radius: 10px;
    border: 1px solid var(--line-strong);
    scroll-snap-align: start;
  }
}

@media (min-width: 1150px) {
  #style-grid { display: none; }
}

.style-cell:hover {
  border-color: var(--cyan);
}

.style-cell.active {
  border-color: var(--cyan);
  box-shadow: 0 0 14px rgba(var(--cyan-rgb), 0.35);
}

.style-name {
  font-size: 13px;
  font-weight: 600;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

.style-bpm {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

#control-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 10px;
  margin: 14px 0;
}

.control-box {
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  background: var(--surface-2);
  padding: 10px 12px;
}

.control-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text);
  opacity: 0.6;
  margin-bottom: 8px;
}

.control-label span {
  color: var(--cyan);
  font-weight: bold;
  letter-spacing: 0;
  text-transform: none;
  font-size: 13px;
  opacity: 1;
}

#tempo-slider {
  width: 100%;
  accent-color: var(--cyan);
}

.control-scale {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  opacity: 0.45;
}

#tempo-mode {
  cursor: pointer;
  text-transform: uppercase;
  letter-spacing: 1px;
}

#tempo-mode:hover {
  color: var(--cyan);
  opacity: 1;
}

#voice-style-row,
#audio-only-row {
  font-size: 12px;
}

#generate-btn {
  width: 100%;
  padding: 14px;
  font-size: 15px;
  font-weight: bold;
  margin-bottom: 14px;
}

#generate-btn:disabled {
  opacity: 0.6;
  cursor: default;
}

/* Tablets: vertical scroll must win over the dial's hover tracking. And a
   floor on the size — a short landscape viewport would otherwise shrink the
   height-budgeted dial to a coin; better to let the page scroll a little. */
canvas#dashboard { touch-action: pan-y; min-width: min(300px, 100%); }

/* Phones: the mosaic is the picker. The dial goes, the preview pill becomes
   a normal block under the mosaic, and Generate sticks above the dock. */
@media (max-width: 699px) {
  #dial-wrap { display: none; }
  #dashboard-row { display: block; }
  #genre-preview {
    position: static;
    transform: none;
    width: 100%;
    margin: 0 0 6px;
  }
  /* visibility:hidden still reserves the pill's height in static flow */
  #genre-preview:not(.visible) { display: none; }
  #generate-btn {
    position: sticky;
    bottom: calc(var(--dock-h) + var(--tabbar-h) + var(--safe-b) + 10px);
    z-index: 5;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
  }
}

/* Form left, dial right — the mockup's shape, on screens with room for it.
   Below this the old stacked order (dial first, then the card) is kept.

   Breakpoint is 1150, not 1250: at 1250 the page had a second, harder
   discontinuity of its own — at 1249px #panel was a full-width ~1193px form
   row and at 1250px it snapped to 372px. 1150 is where the tab bar already
   becomes the header rail, so the whole layout now changes at one boundary
   instead of two.

   The dial track is `auto`, not `1fr`: the dial is square and sized from the
   HEIGHT budget, so a 1fr track just reserved width it could never use — 994px
   of track around a 686px dial, i.e. 154px of dead background either side. An
   auto track hugs the dial and justify-content:center turns what is left into
   symmetric page margin instead of an internal gutter. The panel is allowed to
   take some of that slack too, capped at 520 so the form does not stretch into
   a banner. */
@media (min-width: 1150px) {
  .main-column {
    display: grid;
    grid-template-columns: minmax(340px, 520px) auto;
    justify-content: center;
    gap: 18px;
    align-items: start;
  }

  #panel {
    grid-column: 1;
    grid-row: 1;
    position: sticky;
    top: 12px;
    margin-top: 0;
  }

  #dashboard-row {
    grid-column: 2;
    grid-row: 1;
  }

  #artbook-history-section {
    grid-column: 1 / -1;
  }
}

/* The vocal picker used to be a standalone pill under the dial; inside the
   card's control box it should be plain, or it reads as a box in a box. */
.control-box #voice-style-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: 10px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font-size: 12px;
}

.control-box #voice-style-row label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
}

.control-box #audio-only-row {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 0;
  line-height: 1.35;
}

.control-box #audio-only-row input {
  margin-top: 2px;
  flex-shrink: 0;
}

#video-toggle-preview {
  display: block;
  width: 100%;
  max-height: 80px;
  object-fit: cover;
  border-radius: 8px;
  margin-top: 8px;
  border: 1px solid var(--line-strong);
}

/* Secondary detail (BPM · key · age) in the player bar — present but never
   competing with the title. */
.radio-meta {
  opacity: 0.6;
  font-size: 11px;
  margin-left: 6px;
  white-space: nowrap;
}

/* Round-5 feedback: the two side blocks should start level, the mascot
   shouldn't dominate the column, and the how-to line belongs at the foot of
   the page rather than shouting from the top. */
.settings-plan-actions {
  margin-top: 8px;
  flex-wrap: wrap;
}

/* The player bar's contents were all packed into the left third of a very
   wide bar, which read as the player having shrunk. The now-playing line
   takes the slack so the volume sits at the right edge where it belongs. */
#radio-now {
  flex: 1;
}

#player-volume {
  margin-left: auto;
}

.radio-bar-progress #radio-seek {
  flex: 1;
  accent-color: var(--cyan);
}

@media (max-width: 700px) {
  /* Phones: cover, title/artist, transport, expand — the wave, volume,
     mini-EQ and time labels go; the seek strip along the top stays. */
  #radio-bar { padding: 0 8px 0 10px !important; }
  .radio-bar-top { gap: 8px; }
  #radio-controls { gap: 2px; }
  #radio-controls .btn { padding: 5px 9px; }
  #radio-prev-btn { display: none; }
  #player-volume, #dock-wave, #radio-icon.mini-eq { display: none; }
  #radio-time-cur, #radio-time-dur { display: none; }
  #radio-now .dock-title { font-size: 12px; }
  .radio-meta { display: none; }
}


/* Amy gets her own lit booth: a square neon frame, cyan into magenta, with
   the glow bleeding both inside and out so it reads as light rather than a
   border. */
.mascot-stage {
  border-radius: 16px;
  border: 1px solid transparent;
  background:
    linear-gradient(var(--ink), var(--ink)) padding-box,
    linear-gradient(135deg, var(--cyan), #7a4dff 55%, var(--magenta)) border-box;
  box-shadow:
    0 0 18px rgba(var(--cyan-rgb), 0.22),
    0 0 30px rgba(var(--magenta-rgb), 0.18),
    inset 0 0 34px rgba(var(--cyan-rgb), 0.06);
  animation: booth-glow 4.5s ease-in-out infinite;
}

@keyframes booth-glow {
  0%, 100% {
    box-shadow:
      0 0 18px rgba(var(--cyan-rgb), 0.22),
      0 0 30px rgba(var(--magenta-rgb), 0.18),
      inset 0 0 34px rgba(var(--cyan-rgb), 0.06);
  }
  50% {
    box-shadow:
      0 0 26px rgba(var(--cyan-rgb), 0.34),
      0 0 44px rgba(var(--magenta-rgb), 0.28),
      inset 0 0 40px rgba(var(--magenta-rgb), 0.08);
  }
}

@media (prefers-reduced-motion: reduce) {
  .mascot-stage { animation: none; }
}

/* Most Played leaderboard — real listens (community radio + public share
   plays), not generation count, so it reflects what people actually chose
   to hear. Sits under the live-rendering queue in the same "Live" tab. */
#trending-section {
  margin-top: 14px;
}

.trending-item {
  display: flex;
  align-items: center;
  gap: 10px;
  border: 1px solid var(--dim);
  border-radius: 6px;
  padding: 8px 10px;
  background: var(--panel-bg);
  margin-bottom: 6px;
  cursor: pointer;
}

.trending-item:hover {
  border-color: var(--cyan);
}

.trending-rank {
  color: var(--magenta);
  font-weight: bold;
  font-size: 12px;
  flex-shrink: 0;
  width: 22px;
}

.trending-info {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.trending-meta {
  color: var(--dim);
  font-size: 11px;
}

.trending-plays {
  color: var(--cyan);
  font-size: 11px;
  flex-shrink: 0;
}

.liked-unlike-btn {
  background: none;
  border: 0;
  cursor: pointer;
  padding: 2px;
  flex-shrink: 0;
}

.liked-unlike-btn img {
  width: 18px;
  height: 18px;
  opacity: 0.85;
}

.liked-unlike-btn:hover img {
  opacity: 1;
  transform: scale(1.1);
}

/* Why a render failed, printed under its row in the admin log — now the
   last few traceback lines, not just one, so it's a <pre> instead of a
   <span> and needs to actually wrap/scroll instead of one flowing line. */
.a-reason {
  display: block;
  grid-column: 1 / -1;
  font-size: 10.5px;
  line-height: 1.5;
  font-family: ui-monospace, Consolas, monospace;
  color: #ff8fa8;
  background: rgba(255, 85, 119, 0.08);
  border-radius: 6px;
  padding: 6px 8px;
  margin: 4px 0 0;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 160px;
  overflow-y: auto;
}

/* The mix panel under the dial: per-stem levels that the renderer actually
   applies when you press Generate (not a live remix of a finished track —
   the audio is synthesised once, with these levels baked in). */
#mix-panel {
  width: 100%;
  margin: 12px 0 0;
  border: 1px solid var(--line-strong);
  border-radius: 16px;
  background: var(--surface-1);
  padding: 16px 18px;
}

.mix-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 10px;
  letter-spacing: 1.6px;
  color: var(--text);
  opacity: 0.55;
  margin-bottom: 12px;
}

#mix-reset {
  background: none;
  border: 0;
  color: var(--cyan);
  font: inherit;
  letter-spacing: 1px;
  cursor: pointer;
  opacity: 0.9;
}

.mix-bars {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 96px;
  padding: 8px;
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.35);
  margin-bottom: 14px;
}

.mix-bars i {
  flex: 1;
  height: 10%;
  border-radius: 3px;
  background: linear-gradient(180deg, var(--cyan), #7a4dff 60%, var(--magenta));
  transition: height 0.08s linear;
}

.mix-rows {
  display: grid;
  gap: 9px;
}

.mix-rows div {
  display: grid;
  grid-template-columns: 74px 1fr 46px;
  align-items: center;
  gap: 12px;
}

.mix-rows span {
  font-size: 10px;
  letter-spacing: 1.4px;
  color: var(--text);
  opacity: 0.6;
}

.mix-rows input[type="range"] {
  width: 100%;
  accent-color: var(--cyan);
}

.mix-rows b {
  font-size: 11px;
  color: var(--cyan);
  text-align: right;
}

.mix-rows b.muted {
  color: #ff5577;
}

/* ---- Desktop dial fit + ultrawide dead space (Luis, 2026-09-02) ----
   Two symptoms, one cause: the dial is square and sized from its column's
   width, so on a wide monitor it inflated past the viewport (endless scroll)
   while the artwork inside still could not fill the column (dead gutters). */

@media (min-width: 1150px) {
  /* Honest budget: #genre-preview 50 + #mix-panel 279 + the 84px radio bar
     body reserves = 377, rounded to 380 so the column lands just inside the
     viewport instead of 207px past it. */
  :root {
    /* header (56) + preview pill (50) + margins, plus the dock. The mix
       panel lives in the Fine-tune drawer now, not under the dial. */
    --dial-chrome: calc(150px + var(--dock-h) + var(--rail-h));
  }

  /* Stop reserving the entire column width for a dial that is capped by
     HEIGHT and can never use it — that gap was the dead space. The column now
     hugs the dial, and #dashboard-row's justify-content:center keeps it
     centred in whatever is left. */
  #dashboard-col {
    width: auto;
    min-width: 0;
  }

  /* With the column hugging its content, a percentage width on the canvas has
     nothing definite to resolve against and collapses to the 640px intrinsic
     attribute. Size it from the height budget directly instead — that is what
     actually governs a square dial — and let flex shrink it if the row is
     narrower than the budget allows. */
  canvas#dashboard {
    width: min(1400px, calc(100vh - var(--dial-chrome)));
    /* The dial now sits in an `auto` grid track, which does not clamp its
       child. On a short-and-narrow desktop (tall viewport, 1150px wide) the
       height budget can exceed the width available and the dial would push
       the page sideways. height:auto on the base rule keeps it square as it
       shrinks. */
    max-width: 100%;
  }
}

/* ---- Song reactions (Luis, 2026-09-02) ----
   Extends the existing like button rather than sitting beside it: the heart is
   just the first entry in the palette, so a plain click still means "like". */
#radio-react {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

#radio-reaction-summary {
  display: flex;
  gap: 5px;
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
}

#radio-reaction-summary .rx {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

/* The picker opens UPWARD — the radio bar is pinned to the bottom of the
   viewport, so a downward menu would be clipped off-screen. */
#reaction-picker {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 2px;
  padding: 6px;
  border-radius: 12px;
  background: rgba(13, 13, 22, 0.97);
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
  z-index: 60;
}

#reaction-picker[hidden] {
  display: none;
}

#reaction-picker button {
  background: none;
  border: 0;
  cursor: pointer;
  font-size: 20px;
  line-height: 1;
  padding: 5px 6px;
  border-radius: 8px;
  transition: transform 0.12s ease, background 0.12s ease;
}

#reaction-picker button:hover,
#reaction-picker button:focus-visible {
  transform: scale(1.25);
  background: rgba(255, 255, 255, 0.1);
}

/* The one you already left, so the picker doubles as a readout. */
#reaction-picker button[aria-pressed="true"] {
  background: rgba(255, 0, 128, 0.28);
}

/* ---- Out-of-credits offer (Luis, 2026-09-02) ----
   Sits inside #status-line, which is where the refusal already appears, so the
   offer lands at the point of intent rather than in a panel the person has to
   go looking for. */
.credits-offer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}

.credits-offer-buy {
  flex: 1 1 auto;
  min-height: 40px;
  padding: 9px 16px;
  border: 0;
  border-radius: 9px;
  cursor: pointer;
  font: inherit;
  font-weight: 700;
  color: #0b0b12;
  background: linear-gradient(135deg, var(--cyan), var(--magenta));
  box-shadow: 0 6px 18px -8px rgba(255, 45, 149, 0.8);
}

.credits-offer-buy:hover {
  filter: brightness(1.08);
}

.credits-offer-more {
  background: none;
  border: 0;
  padding: 6px 4px;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  color: var(--text-dim);
  text-decoration: underline;
}

.credits-offer-more:hover {
  color: var(--cyan);
}

/* ---- Reaction pill on list rows (Luis, 2026-09-02) ----
   Lives inside a row whose own click starts playback, so it has to read as a
   separate control, not as part of the row's text. */
.rx-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 3px 9px;
  cursor: pointer;
  font: inherit;
  font-size: 12px;
  color: var(--text);
  line-height: 1.5;
  transition: background 0.12s ease, transform 0.12s ease;
}

.rx-pill:hover,
.rx-pill:focus-visible {
  background: rgba(255, 255, 255, 0.13);
  transform: scale(1.06);
}

.rx-pill:disabled {
  opacity: 0.55;
  cursor: default;
}

/* Your own reaction is the one you can take back, so it needs to be findable. */
.rx-pill[data-mine="1"] {
  border-color: rgba(255, 45, 149, 0.55);
  background: rgba(255, 45, 149, 0.16);
}

.rx-one {
  white-space: nowrap;
}

.rx-own {
  color: var(--magenta);
  font-weight: 700;
}

/* --- Sign-in: the one reassurance under the card ------------------------- */
#signin-note {
  margin: 10px 0 0;
  font-size: 12px;
  color: var(--text-dim);
  text-align: center;
}

/* --- Artist chip: handle, credits and plan as one thing ------------------ */
#artist-chip-wrap { position: relative; }
#artist-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px;
  border: 1px solid var(--dim);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.04);
  color: inherit;
  font: inherit;
  cursor: pointer;
}
#artist-chip:hover { border-color: var(--cyan); }
#artist-chip .artist-chip-chev { color: var(--text-dim); font-size: 11px; }
#artist-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 60;
  min-width: 210px;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  border: 1px solid var(--dim);
  border-radius: 12px;
  background: var(--surface-1);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
}
#artist-menu #top-bar-account-actions {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
#artist-menu .btn { width: 100%; text-align: left; }

/* --- Create: the first-run reveal ---------------------------------------- */
#more-controls-btn {
  display: block;
  margin: 10px auto 0;
  padding: 6px 12px;
  border: 0;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 13px;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
#more-controls-btn:hover { color: var(--cyan); }

/* --- Floor: your own render ---------------------------------------------- */
#my-render {
  margin-bottom: 18px;
  padding: 14px;
  border: 1px solid var(--dim);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.03);
}
#my-render-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 8px;
}
#my-render-title { font-weight: 600; }
#my-render-pct { color: var(--cyan); font-variant-numeric: tabular-nums; }
#my-render-bar {
  height: 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
#my-render-fill {
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--cyan), var(--magenta));
  transition: width 0.4s ease;
}
/* No percentage yet: a moving sheen says "working", where a 0% bar says
   "stuck". */
#my-render-fill.indeterminate {
  transition: none;
  background: linear-gradient(90deg,
    rgba(49, 232, 255, 0.15) 0%,
    var(--cyan) 50%,
    rgba(49, 232, 255, 0.15) 100%);
  background-size: 200% 100%;
  animation: my-render-sweep 1.4s linear infinite;
}
@keyframes my-render-sweep {
  from { background-position: 200% 0; }
  to { background-position: -200% 0; }
}
#my-render-steps {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
}
#my-render-done {
  display: flex;
  gap: 8px;
  margin-top: 12px;
}

/* --- Vault: one song, opened --------------------------------------------- */
#song-drawer { position: fixed; inset: 0; z-index: 80; }
#song-drawer-scrim {
  position: absolute;
  inset: 0;
  background: rgba(4, 4, 8, 0.6);
  backdrop-filter: blur(2px);
}
#song-drawer-panel {
  position: absolute;
  top: 0;
  right: 0;
  width: min(420px, 100%);
  height: 100%;
  overflow-y: auto;
  padding: 18px;
  border-left: 1px solid var(--dim);
  background: var(--surface-1);
  box-shadow: -20px 0 50px rgba(0, 0, 0, 0.5);
}
#song-drawer-close {
  position: absolute;
  top: 10px;
  right: 12px;
  border: 0;
  background: none;
  color: var(--text-dim);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
}
#song-drawer-close:hover { color: var(--text); }
#song-drawer-cover {
  width: 100%;
  aspect-ratio: 1;
  border-radius: 12px;
  margin-bottom: 12px;
}
#song-drawer-title { margin: 0 0 4px; font-size: 20px; }
#song-drawer-meta { color: var(--text-dim); font-size: 13px; margin-bottom: 12px; }
#song-drawer-files { display: flex; flex-direction: column; gap: 6px; }
.sd-file {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--dim);
  border-radius: 10px;
}
.sd-file-kind {
  flex: 0 0 auto;
  min-width: 58px;
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--cyan);
}
.sd-file-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
}
#song-drawer-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}

/* The drawer covers the phone screen rather than sitting beside a grid that
   is only one column wide there anyway. */
@media (max-width: 700px) {
  #song-drawer-panel {
    width: 100%;
    border-left: 0;
    padding-bottom: calc(var(--dock-h) + var(--tabbar-h) + var(--safe-b) + 20px);
  }
}


/* Most Played / Most Liked rows sit at 48px — slightly tighter than the Just
   Dropped chips, which are the wider card. */
#trending-list .queue-thumb,
#most-liked-list .queue-thumb {
  width: 48px;
  height: 48px;
}
