/* ==========================================================================
   ClipBounce — design system

   Structure over decoration. The rules that matter:
     · full-bleed, never a centred max-width box
     · extreme scale contrast — 10px tracked labels against 200px display
     · the output is the hero, everything else floats around it
     · colour comes from the user's artwork, not from us
     · the page is always alive: ambient motion, light spill, grain
   ========================================================================== */

:root {
  --ink:      #04050a;
  --fg:       #f4f5f9;
  --fg-dim:   #a2a8bd;
  --fg-faint: #5a6076;

  /* Overwritten from the artwork palette at runtime. */
  --art-1: #6c5cff;
  --art-2: #23e8d0;
  --art-3: #ff3d8a;

  --glass:      rgba(255, 255, 255, .045);
  --glass-line: rgba(255, 255, 255, .10);
  --glass-hi:   rgba(255, 255, 255, .16);

  --display: 'Anton', 'Arial Narrow', sans-serif;
  --serif:   'Instrument Serif', Georgia, serif;
  --sans:    'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
  --mono:    'JetBrains Mono', ui-monospace, Menlo, monospace;

  --ease:  cubic-bezier(.22, 1, .36, 1);
  --ease-i: cubic-bezier(.65, 0, .35, 1);
}

* { box-sizing: border-box; }
html, body { height: 100%; }

/* Any class that sets `display` outranks the UA's `[hidden] { display: none }`,
   so toggling `.hidden` in JS silently does nothing. This puts it back. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--fg);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.5;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}
/* Vertical scrolling only. The ambient and spill canvases are deliberately
   drawn wider than the viewport so their glow bleeds off the edges — desktop
   never sees that because the body is `overflow: hidden`, but switching to
   `auto` here let you scroll sideways into 22px of dead space. */
@media (max-width: 1080px) {
  html, body { overflow-x: hidden; }
  body { overflow-y: auto; }
}

button, input { font: inherit; color: inherit; }
button { cursor: pointer; border: 0; background: none; }
::selection { background: var(--art-1); color: #fff; }

:focus-visible {
  outline: 2px solid var(--art-2);
  outline-offset: 3px;
  border-radius: 6px;
}

/* ═════════════════════════ living background ═════════════════════════ */

.ambient {
  position: fixed;
  inset: -10%;
  width: 120%;
  height: 120%;
  z-index: 0;
  /* The canvas is only 260×150, so upscaling already smooths it — the blur is
     for glow, not for hiding pixels. Keeping it modest matters because five
     backdrop-filter panels sit on top and re-blur whenever this changes. */
  filter: blur(52px) saturate(1.6);
  opacity: .9;
  transform: translateZ(0);
  transition: opacity .8s var(--ease);
}
body[data-state="studio"] .ambient { opacity: .55; }

/* Film grain. Without this, large flat gradients read as flat CSS. */
.grain {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  opacity: .16; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
}

.vignette {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% 45%, transparent 35%, rgba(0,0,0,.55) 100%),
    linear-gradient(180deg, rgba(0,0,0,.5), transparent 22%, transparent 70%, rgba(0,0,0,.7));
}

/* ═════════════════════════ cold open ═════════════════════════ */

.cold {
  position: relative;
  z-index: 2;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  padding: 4vh 24px;
  text-align: center;
  transition: opacity .5s var(--ease), transform .6s var(--ease), filter .5s;
}
body[data-state="studio"] .cold {
  opacity: 0; transform: scale(1.08); filter: blur(14px); pointer-events: none;
}

.eyebrow {
  margin: 0 0 clamp(18px, 4vh, 40px);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--fg-faint);
  display: flex; align-items: center; gap: 10px;
  animation: rise .9s var(--ease) both .1s;
}
.dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--art-2);
  box-shadow: 0 0 12px var(--art-2);
  animation: breathe 2.6s ease-in-out infinite;
}
@keyframes breathe { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }

.wordmark {
  margin: 0;
  font-family: var(--display);
  font-weight: 400;
  /* Sized for a 10-letter wordmark. Anton runs ~0.45em per character, so
     CLIPBOUNCE needs roughly 4.5 × the font size in width — 19vw fitted
     BOUNCE but would overflow narrow viewports at this length. */
  font-size: clamp(40px, 15vw, 240px);
  line-height: .82;
  letter-spacing: -.02em;
  display: flex;
  background: linear-gradient(96deg, var(--fg) 18%, var(--art-2) 50%, var(--art-1) 82%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.wordmark span {
  display: inline-block;
  /* `backwards`, never `both`. With `both` the span keeps the final keyframe's
     transform — an identity matrix, but still a transform, which gives it its
     own stacking context and stops the parent's background-clip:text gradient
     painting through. `backwards` fills only before the animation, so the span
     ends on transform:none and the gradient shows. */
  animation: drop 1.1s var(--ease) backwards;
}
.wordmark span:nth-child(1) { animation-delay: .05s; }
.wordmark span:nth-child(2) { animation-delay: .10s; }
.wordmark span:nth-child(3) { animation-delay: .15s; }
.wordmark span:nth-child(4) { animation-delay: .20s; }
.wordmark span:nth-child(5) { animation-delay: .25s; }
.wordmark span:nth-child(6) { animation-delay: .30s; }
.wordmark span:nth-child(7) { animation-delay: .35s; }
.wordmark span:nth-child(8) { animation-delay: .40s; }
.wordmark span:nth-child(9) { animation-delay: .45s; }
.wordmark span:nth-child(10) { animation-delay: .50s; }
/* Transform and opacity only — deliberately no filter. A filter other than
   `none` gives the span its own stacking context, which stops the parent's
   background-clip:text gradient painting through it and renders the wordmark
   invisible. */
@keyframes drop {
  from { transform: translateY(-.38em) rotate(-7deg); opacity: 0; }
}

.lede {
  margin: clamp(14px, 3vh, 30px) 0 clamp(24px, 5vh, 46px);
  font-size: clamp(21px, 3.1vw, 40px);
  line-height: 1.22;
  color: var(--fg-dim);
  animation: rise 1s var(--ease) both .5s;
}
.lede em {
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.18em;
  color: var(--fg);
}

.cta {
  position: relative;
  padding: 19px 46px;
  border-radius: 999px;
  font-size: 17px;
  font-weight: 500;
  letter-spacing: .01em;
  color: #fff;
  isolation: isolate;
  overflow: hidden;
  animation: rise 1s var(--ease) both .62s;
  transition: transform .28s var(--ease), box-shadow .28s var(--ease);
  box-shadow: 0 16px 44px -14px var(--art-1), inset 0 0 0 1px rgba(255,255,255,.18);
}
.cta::before {
  content: ''; position: absolute; inset: 0; z-index: -2;
  background: linear-gradient(110deg, var(--art-1), var(--art-3) 55%, var(--art-2));
  background-size: 220% 100%;
  animation: slide 7s linear infinite;
}
.cta::after {                       /* shine sweep */
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(100deg, transparent 32%, rgba(255,255,255,.42) 50%, transparent 68%);
  transform: translateX(-120%);
  transition: transform .85s var(--ease);
}
.cta:hover { transform: translateY(-3px) scale(1.02); box-shadow: 0 26px 60px -16px var(--art-1), inset 0 0 0 1px rgba(255,255,255,.3); }
.cta:hover::after { transform: translateX(120%); }
@keyframes slide { to { background-position: 220% 0; } }

.hint {
  margin: 16px 0 0;
  font-size: 13px;
  color: var(--fg-faint);
  animation: rise 1s var(--ease) both .72s;
}

.specs {
  position: absolute;
  bottom: clamp(20px, 5vh, 52px);
  display: flex;
  gap: clamp(22px, 5vw, 72px);
  margin: 0; padding: 0; list-style: none;
  animation: rise 1.1s var(--ease) both .85s;
}
.specs li { display: flex; flex-direction: column; gap: 3px; }
.specs b {
  font-family: var(--mono); font-size: 17px; font-weight: 700; color: var(--fg);
}
.specs b i { font-style: normal; font-size: .6em; color: var(--fg-faint); }
.specs span {
  font-size: 10px; letter-spacing: .2em; text-transform: uppercase; color: var(--fg-faint);
}

@keyframes rise { from { opacity: 0; transform: translateY(18px); } }

/* ═════════════════════════ studio ═════════════════════════ */

.studio {
  position: fixed;
  inset: 0;
  z-index: 2;
  display: grid;
  grid-template-columns: minmax(250px, 310px) 1fr minmax(250px, 330px);
  gap: clamp(14px, 1.8vw, 30px);
  padding: clamp(14px, 2vh, 26px) clamp(14px, 2vw, 32px);
  opacity: 0;
  transform: scale(.965);
  pointer-events: none;
  transition: opacity .6s var(--ease) .15s, transform .7s var(--ease) .15s;
}
body[data-state="studio"] .studio { opacity: 1; transform: none; pointer-events: auto; }

/* Staggered entrance so the studio assembles rather than appearing.
   `backwards`, never `both` — see the wordmark note above. */
@keyframes panelIn { from { opacity: 0; transform: translateY(18px); } }

body[data-state="studio"] .rail > * { animation: panelIn .65s var(--ease) backwards; }
body[data-state="studio"] .rail-l > *:nth-child(1) { animation-delay: .30s; }
body[data-state="studio"] .rail-l > *:nth-child(2) { animation-delay: .37s; }
body[data-state="studio"] .rail-l > *:nth-child(3) { animation-delay: .44s; }
body[data-state="studio"] .rail-r > *:nth-child(1) { animation-delay: .34s; }
body[data-state="studio"] .rail-r > *:nth-child(2) { animation-delay: .41s; }
body[data-state="studio"] .rail-r > *:nth-child(3) { animation-delay: .48s; }
body[data-state="studio"] .transport { animation: panelIn .65s var(--ease) backwards .52s; }

/* The rail itself no longer scrolls — `.rail-scroll` does. That keeps the
   export button pinned and fully visible: it is the primary action, and a
   button clipped by the fold reads as broken on both axes no matter how
   precisely its text is centred. It also puts the button outside the scroll
   container entirely, so no scrollbar or gutter can ever shift it. */
.rail {
  display: flex; flex-direction: column; gap: clamp(10px, 1.4vh, 18px);
  min-width: 0; min-height: 0; overflow: hidden;
}

.rail-scroll {
  display: flex; flex-direction: column; gap: clamp(10px, 1.4vh, 18px);
  flex: 1 1 auto;
  /* Floor it so a short window shrinks the thumbnails rather than collapsing
     them to nothing — a scroll area with no height is worse than one that
     scrolls. */
  min-height: 110px;
  min-width: 0; overflow-y: auto; overflow-x: hidden;
  /* No scrollbar, and no gutter.

     Long story, worth recording. The export button's centred text kept reading
     as off-centre, and every geometric fix measured perfect — because the
     geometry was never wrong. `scrollbar-gutter: stable both-edges` does keep
     the content box concentric, but only ONE of the two reserved gutters has
     anything drawn in it. The eye sees [empty][content][scrollbar] and reads
     the content as pushed left, however centred it actually is. That's also why
     it looked correct in an overlay-scrollbar browser and wrong in Brave.

     Removing the scrollbar from layout entirely is the only thing that makes
     the visible column symmetrical. The rail still scrolls by wheel, trackpad
     and keyboard; the mask below fades the top and bottom edges so there's
     still a cue that content continues. */
  scrollbar-width: none;
  -ms-overflow-style: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 14px,
                                      #000 calc(100% - 14px), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 14px,
                              #000 calc(100% - 14px), transparent 100%);
}
.rail-scroll::-webkit-scrollbar { width: 0; height: 0; }

.glass {
  position: relative;
  padding: 15px 16px 16px;
  border-radius: 18px;
  background: var(--glass);
  border: 1px solid var(--glass-line);
  backdrop-filter: blur(18px) saturate(1.3);
  -webkit-backdrop-filter: blur(18px) saturate(1.3);
  box-shadow: inset 0 1px 0 var(--glass-hi), 0 18px 44px -30px #000;
}

.rail-label {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  margin: 0 0 12px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

/* ─── track card ─── */

.track-swap {
  display: grid; gap: 3px; width: 100%; text-align: left;
  padding: 11px 12px; border-radius: 12px;
  background: rgba(255,255,255,.04);
  border: 1px solid transparent;
  transition: .2s var(--ease);
}
.track-swap:hover { border-color: var(--art-1); background: rgba(255,255,255,.07); }
/* Long names scroll rather than being clipped. A producer's export filenames run
   long and the part that identifies the take is usually at the *end*
   ("… v3 master 2"), which is exactly what a truncation hides. The inner span
   must be inline-block for `transform` to apply to it, which is also why
   text-overflow is no longer in play here. */
.tc-name { font-size: 14.5px; font-weight: 500; overflow: hidden; white-space: nowrap; }
.tc-name > span { display: inline-block; }
.tc-name.is-scrolling > span {
  animation: track-marquee var(--marq-dur, 8s) ease-in-out infinite alternate;
}
/* Pauses at both ends so the name is readable at rest, not permanently gliding. */
@keyframes track-marquee {
  0%, 16%   { transform: translateX(0); }
  84%, 100% { transform: translateX(var(--marq-shift, 0)); }
}
.tc-meta { font-family: var(--mono); font-size: 10.5px; color: var(--fg-faint); }
/* Only used when the decoder returned less audio than the file actually holds.
   It has to out-read the artwork palette, so it's a fixed warning colour rather
   than an accent — this is the one line in the card that must not blend in. */
.tc-meta.meta-warn { color: #ff8a5c; text-transform: none; letter-spacing: 0; }
/* Anchored to the track row, not the card. `.glass` is the nearest positioned
   ancestor, so a card-relative hint lands in the top-right corner — which is
   where the New clip button lives, and the two overlapped. Sitting at the right
   of the row itself puts it in empty space, next to what it actually describes.
   `padding-right` on the row keeps a long filename from running underneath it. */
.track-swap { position: relative; padding-right: 74px; }
.swap-hint {
  position: absolute; top: 50%; right: 12px;
  transform: translateY(-50%);
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--fg-faint); opacity: 0; transition: opacity .2s;
  pointer-events: none;
}
.track-card:hover .swap-hint { opacity: 1; }

.analysing { display: flex; align-items: center; gap: 8px; margin-top: 9px; font-size: 12px; color: var(--fg-dim); }
.spinner {
  width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid var(--glass-line); border-top-color: var(--art-2);
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ─── artwork ─── */

.art-drop {
  display: flex; align-items: center; gap: 13px; width: 100%; text-align: left;
  padding: 9px; border-radius: 13px;
  border: 1px solid transparent;
  transition: .2s var(--ease);
}
.art-drop:hover { border-color: var(--art-2); background: rgba(255,255,255,.05); }
.art-thumb {
  flex: none; width: 54px; height: 54px; border-radius: 10px;
  background: rgba(255,255,255,.06) center / cover no-repeat;
  border: 1px solid var(--glass-line);
  display: grid; place-items: center;
}
.art-plus { font-size: 22px; font-weight: 300; color: var(--fg-faint); line-height: 1; }
.art-thumb.has-art .art-plus { display: none; }
.art-copy { display: grid; gap: 2px; min-width: 0; }
.art-copy b { font-weight: 500; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.art-copy i { font-style: normal; font-size: 11.5px; color: var(--fg-faint); }

.palette { display: flex; gap: 5px; margin-top: 12px; }
.palette:empty { display: none; }
.palette span {
  flex: 1; height: 26px; border-radius: 7px;
  border: 1px solid rgba(255,255,255,.12);
  animation: pop .4s var(--ease) backwards;
}
@keyframes pop { from { transform: scale(.4) translateY(6px); opacity: 0; } }

/* Every swatch is editable, so each needs to look pressable. The pencil only
   appears on hover — five permanent icons would read as clutter — but the
   standing hint line below the row is what actually advertises the feature,
   since nobody hovers a swatch they don't already know is interactive. */
.palette span {
  position: relative; cursor: pointer;
  transition: transform .16s var(--ease), border-color .16s, box-shadow .16s;
}
.palette span::after {
  content: '✎';
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-size: 12px; color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,.7);
  opacity: 0; transition: opacity .16s var(--ease);
}
.palette span:hover { transform: translateY(-2px); border-color: rgba(255,255,255,.55); }
.palette span:hover::after { opacity: 1; }
.palette span:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--art-2); }

.palette-hint {
  margin: 7px 0 0;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .06em;
  color: var(--fg-faint); text-align: center;
}

/* ─── colour picker ─── */

.colour-pick {
  display: flex; align-items: center; gap: 10px;
  margin-top: 10px; padding: 8px 10px;
  border: 1px solid var(--glass-line); border-radius: 12px;
  cursor: pointer;
  transition: border-color .18s var(--ease), background .18s var(--ease);
}
.colour-pick:hover { border-color: var(--art-2); background: rgba(255,255,255,.05); }
/* The native swatch is unstyleable across browsers, so it sits at zero size and
   the chip beside it is what the user sees. The label still forwards the click. */
.colour-pick input[type='color'] {
  width: 0; height: 0; padding: 0; border: 0; opacity: 0; position: absolute;
}
.colour-chip {
  width: 26px; height: 26px; border-radius: 7px; flex: none;
  background: var(--art-1);
  border: 1px solid rgba(255,255,255,.22);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.25);
}
.colour-copy { display: grid; line-height: 1.25; }
.colour-copy b { font-size: 12px; font-weight: 500; color: var(--fg); }
.colour-copy i { font-family: var(--mono); font-size: 9.5px; font-style: normal; color: var(--fg-faint); }

/* ─── fields ─── */

.fields { display: grid; gap: 8px; }
.field { display: grid; grid-template-columns: 46px 1fr; align-items: center; gap: 10px; }
.field > span { font-family: var(--mono); font-size: 9.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--fg-faint); }
.field input {
  width: 100%; padding: 9px 11px;
  background: rgba(255,255,255,.04);
  border: 1px solid var(--glass-line);
  border-radius: 9px;
  font-size: 13.5px;
  transition: border-color .18s, background .18s;
}
.field input:focus { outline: none; border-color: var(--art-1); background: rgba(255,255,255,.07); }
.field input::placeholder { color: var(--fg-faint); }

/* ═════════════════════════ stage ═════════════════════════ */

.stage {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(12px, 2vh, 22px);
  min-width: 0; min-height: 0;
}

.stage-frame {
  position: relative;
  height: min(100%, 82vh);
  aspect-ratio: 9 / 16;
  max-width: 100%;
  animation: liftIn 1s var(--ease) backwards .22s;
}
@keyframes liftIn { from { opacity: 0; transform: translateY(28px) scale(.94); } }

/* The screen's own colours, blurred out onto the page. This is what makes the
   preview look lit rather than pasted on. */
.spill {
  position: absolute;
  inset: -14%;
  width: 128%; height: 128%;
  filter: blur(58px) saturate(2.1);
  opacity: .85;
  z-index: -1;
  border-radius: 50%;
  transition: opacity .5s;
}

.screen {
  display: block;
  width: 100%; height: 100%;
  border-radius: clamp(14px, 1.6vw, 26px);
  background: #000;
  border: 1px solid var(--glass-hi);
  box-shadow: 0 44px 90px -34px rgba(0,0,0,.95), inset 0 0 0 1px rgba(255,255,255,.05);
  cursor: pointer;
  transition: transform .3s var(--ease);
}
.screen:hover { transform: scale(1.008); }

.transport {
  display: flex; align-items: center; gap: 13px;
  width: min(100%, 540px);
  padding: 10px 14px;
  border-radius: 999px;
}
.play {
  flex: none; width: 38px; height: 38px;
  display: grid; place-items: center; border-radius: 50%;
  background: var(--fg); color: var(--ink);
  transition: transform .18s var(--ease), opacity .18s;
}
.play:hover:not(:disabled) { transform: scale(1.08); }
.play:disabled { opacity: .28; cursor: not-allowed; }
.play svg { width: 18px; height: 18px; }

/* Icon swapping is done with a class, NOT the `hidden` property.
   `hidden` is defined on HTMLElement and SVGElement does not inherit it, so
   `svg.hidden = true` silently sets a meaningless JS property and the icon
   never changes. Toggling a class on the parent works on any element type. */
.play .i-pause { display: none; }
.play.is-playing .i-play { display: none; }
.play.is-playing .i-pause { display: block; }

/* ─── playback timeline ─── */

.scrub {
  -webkit-appearance: none; appearance: none;
  flex: 1; min-width: 60px; height: 18px;
  background: none; cursor: pointer;
}
.scrub::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px;
  background: linear-gradient(90deg, var(--art-1) 0 var(--scrub-pct, 0%), var(--art-2) var(--scrub-pct, 0%), rgba(255,255,255,.14) var(--scrub-pct, 0%) 100%);
}
.scrub::-moz-range-track {
  height: 4px; border-radius: 2px;
  background: linear-gradient(90deg, var(--art-1) 0 var(--scrub-pct, 0%), rgba(255,255,255,.14) var(--scrub-pct, 0%) 100%);
}
.scrub::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 13px; height: 13px; margin-top: -4.5px;
  border-radius: 50%; background: var(--fg);
  box-shadow: 0 1px 5px rgba(0,0,0,.7);
  transform: scale(.55);
  transition: transform .18s var(--ease);
}
.scrub::-moz-range-thumb {
  width: 13px; height: 13px; border: 0;
  border-radius: 50%; background: var(--fg);
  box-shadow: 0 1px 5px rgba(0,0,0,.7);
}
/* The handle stays discreet until you reach for it. */
.transport:hover .scrub::-webkit-slider-thumb,
.scrub:focus-visible::-webkit-slider-thumb,
.scrub:active::-webkit-slider-thumb { transform: scale(1); }
.scrub:disabled { opacity: .4; cursor: not-allowed; }
.scrub:disabled::-webkit-slider-thumb { transform: scale(0); }

.tc {
  font-family: var(--mono); font-size: 11.5px; color: var(--fg-dim);
  min-width: 74px; text-align: right; flex: none;
  font-variant-numeric: tabular-nums;
}
.tc i { font-style: normal; opacity: .4; margin: 0 3px; }

/* ─── volume ─── */

.vol-group { display: flex; align-items: center; gap: 7px; flex: none; }

.vol-btn {
  display: grid; place-items: center;
  width: 26px; height: 26px; border-radius: 7px;
  color: var(--fg-dim);
  transition: color .16s, background .16s;
}
.vol-btn:hover { color: var(--fg); background: rgba(255,255,255,.08); }
.vol-btn svg { width: 17px; height: 17px; }
.vol-btn.is-muted { color: var(--art-3); }
/* Same SVG-`hidden` trap as the play button — driven by the class instead. */
.vol-btn .i-mute { display: none; }
.vol-btn.is-muted .i-vol { display: none; }
.vol-btn.is-muted .i-mute { display: block; }

.vol {
  -webkit-appearance: none; appearance: none;
  width: 74px; height: 18px;
  background: none; cursor: pointer;
}
.vol::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px;
  background: linear-gradient(90deg, var(--art-2) 0 var(--vol-pct, 70%), rgba(255,255,255,.16) var(--vol-pct, 70%) 100%);
}
.vol::-moz-range-track {
  height: 4px; border-radius: 2px;
  background: linear-gradient(90deg, var(--art-2) 0 var(--vol-pct, 70%), rgba(255,255,255,.16) var(--vol-pct, 70%) 100%);
}
.vol::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 12px; height: 12px; margin-top: -4px;
  border-radius: 50%; background: var(--fg);
  box-shadow: 0 1px 4px rgba(0,0,0,.6);
  transition: transform .16s var(--ease);
}
.vol::-moz-range-thumb {
  width: 12px; height: 12px; border: 0;
  border-radius: 50%; background: var(--fg);
  box-shadow: 0 1px 4px rgba(0,0,0,.6);
}
.vol:hover::-webkit-slider-thumb { transform: scale(1.25); }
.vol:disabled { opacity: .35; cursor: not-allowed; }

/* ═════════════════════════ styles picker ═════════════════════════ */

.styles { display: grid; grid-template-columns: repeat(2, 1fr); gap: 9px; }
/* Odd style count: give the last one its own full-width row rather than
   leaving it stranded next to an empty cell. */
.styles > :last-child:nth-child(odd) { grid-column: 1 / -1; max-width: 220px; margin: 0 auto; }

.style-btn {
  position: relative; padding: 0; border-radius: 13px; overflow: hidden;
  border: 1px solid var(--glass-line);
  background: rgba(0,0,0,.35);
  transition: transform .25s var(--ease), border-color .25s, box-shadow .25s;
}
.style-btn canvas { display: block; width: 100%; height: auto; }
.style-btn span {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 16px 8px 7px;
  font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--fg-dim);
  background: linear-gradient(transparent, rgba(0,0,0,.85));
}
.style-btn:hover { transform: translateY(-3px); border-color: var(--glass-hi); }
.style-btn[aria-checked="true"] {
  border-color: var(--art-2);
  box-shadow: 0 0 0 1px var(--art-2), 0 14px 34px -16px var(--art-2);
}
.style-btn[aria-checked="true"] span { color: #fff; }

/* ═════════════════════════ clip ═════════════════════════ */

.wave-wrap {
  position: relative; height: 74px;
  border-radius: 11px; overflow: hidden;
  background: rgba(0,0,0,.32);
  border: 1px solid var(--glass-line);
  touch-action: none;
}
#waveform { display: block; width: 100%; height: 100%; }

.clip-window {
  position: absolute; top: 0; bottom: 0;
  background: linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.03));
  border-left: 2px solid var(--art-2); border-right: 2px solid var(--art-2);
  box-shadow: 0 0 24px -4px var(--art-2);
  cursor: grab;
}
.clip-window:active { cursor: grabbing; }
.handle { position: absolute; top: 0; bottom: 0; width: 16px; cursor: ew-resize; }
.handle::after {
  content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 3px; height: 24px; border-radius: 2px; background: var(--art-2);
}
.handle-l { left: -9px; } .handle-r { right: -9px; }

/* Playhead: mirrors the preview's position, and clicking the clip window seeks
   to it. Sits above the clip window but ignores pointers so it can't block a
   drag of the window itself. */
.playhead {
  position: absolute; top: 0; bottom: 0; width: 2px;
  background: #fff;
  box-shadow: 0 0 8px rgba(255,255,255,.9), 0 0 16px var(--art-2);
  pointer-events: none;
  z-index: 2;
}
.playhead::before {
  content: ''; position: absolute; top: -1px; left: 50%;
  transform: translateX(-50%);
  border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-top: 5px solid #fff;
}

.clip-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 11px; }
.clip-time { font-family: var(--mono); font-size: 11px; color: var(--fg-dim); }
.chips { display: flex; gap: 5px; }
.chip {
  padding: 5px 9px; font-family: var(--mono); font-size: 11px;
  border: 1px solid var(--glass-line); border-radius: 999px; color: var(--fg-faint);
  transition: .16s var(--ease);
}
.chip:hover { color: var(--fg); }
.chip[aria-pressed="true"] { background: var(--fg); border-color: var(--fg); color: var(--ink); }

/* ═════════════════════════ export ═════════════════════════ */

.export {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  /* Never let the rail compress this. As a flex item it defaults to shrink:1,
     so when the column runs short the button loses height — and because its
     content is laid out from the top padding under `overflow: hidden`, every
     pixel of that squeeze comes off the bottom and shoves the label low. It
     reads as "not centred vertically" and no amount of padding fixes it.
     `.rail-scroll` is the element designated to absorb the shortfall. */
  flex-shrink: 0;
  /* 16/18 rather than a flat 17: the two line boxes reserve descender space
     that these glyphs barely use, so with symmetric padding the *boxes* centre
     perfectly while the *ink* sits ~1.3px low. Measured, not guessed — and it's
     the ink the eye judges. */
  padding: 16px 17px 18px; border-radius: 16px; overflow: hidden;
  color: #fff; font-weight: 500;
  isolation: isolate;
  box-shadow: 0 18px 46px -18px var(--art-1), inset 0 0 0 1px rgba(255,255,255,.18);
  transition: transform .22s var(--ease), box-shadow .22s var(--ease), filter .22s;
}
.export::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(110deg, var(--art-1), var(--art-3) 55%, var(--art-2));
  background-size: 220% 100%;
  animation: slide 7s linear infinite;
}
.export:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 26px 58px -18px var(--art-1), inset 0 0 0 1px rgba(255,255,255,.32); }
.export:disabled { filter: grayscale(.85) brightness(.6); cursor: not-allowed; box-shadow: none; }
/* Full width + text-align, NOT flex centring.
   `align-items: center` positions a shrink-to-fit child at
   (containerWidth − childWidth) / 2 — arithmetic that lands on a fractional
   pixel and gets rounded, which on a scaled display (125% etc.) shifts the text
   a whole device pixel. It measures as perfectly centred and still looks off.
   Letting the text renderer centre within a full-width box removes the
   arithmetic, and it handles subpixel positioning properly. */
.export-label,
.export-sub { display: block; width: 100%; text-align: center; }
.export-label { font-size: 16px; }
.export-sub { font-family: var(--mono); font-size: 10.5px; opacity: .8; }

.mini-btn {
  padding: 4px 10px;
  font-family: var(--sans); font-size: 11px;
  letter-spacing: .04em; text-transform: none;
  color: var(--fg-dim);
  border: 1px solid var(--glass-line); border-radius: 999px;
  transition: .18s var(--ease);
}
.mini-btn:hover { color: #fff; border-color: var(--art-2); background: rgba(255,255,255,.07); }
.mini-btn--wide { padding: 8px 12px; font-size: 12.5px; width: 100%; }

/* ─── iOS silent-switch hint ─── */

/* Sits under the transport rather than over the canvas: the user is looking at
   the play button, having just pressed it and heard nothing. Not an error
   colour — nothing is broken, and the export they came for is unaffected. */
.silent-hint {
  display: flex; align-items: flex-start; gap: 10px;
  width: min(100%, 540px);   /* matches the transport pill above it */
  box-sizing: border-box;
  margin: 10px 0 0;
  padding: 9px 11px;
  font-size: 11.5px; line-height: 1.45;
  color: var(--fg-dim);
  background: rgba(255,255,255,.05);
  border: 1px solid var(--glass-line);
  border-radius: 10px;
}
.silent-hint button {
  flex: none;
  width: 20px; height: 20px;
  font-size: 15px; line-height: 1;
  color: var(--fg-dim);
  border-radius: 6px;
}
.silent-hint button:hover { color: #fff; background: rgba(255,255,255,.09); }

/* ─── locked during an export ─── */

/* An export is a real-time recording of this canvas, so anything still editable
   is being filmed. These read as "not now" rather than as broken: dimmed and
   inert, but still in place, because the panel rearranging itself mid-record
   would look like a fault at exactly the wrong moment.

   The pointer-events rule is the load-bearing one — the drop zones are divs,
   and `disabled` does nothing at all to a div. */
/* Scoped to .is-locked deliberately. A blanket `button:disabled` rule would
   have overridden the four controls that already have tuned disabled states —
   .play, .scrub, .vol and .export each pick their own opacity, and .export
   uses a grayscale filter rather than transparency. */
.is-locked {
  opacity: .42;
  pointer-events: none;
  cursor: default;
}

/* Inert, but keeps its normal appearance — for the waveform and clip window,
   which the playhead crosses during the export. That travelling playhead is
   the clearest sign the render is progressing, so it must not be dimmed. */
.is-inert { pointer-events: none; }

/* The picker's own selected-state glow would otherwise still read as live. */
.styles.is-locked .style-btn[aria-pressed='true'] { box-shadow: none; }

/* ─── Pro styles and the unlock ─── */

/* A quiet mark, not a barrier. Pro styles are fully previewable, so the badge
   is telling you what will happen at export, not forbidding a click. */
.style-btn { position: relative; }
.style-btn[data-pro='true']::after {
  content: 'PRO';
  position: absolute; top: 8px; right: 8px;
  font-family: var(--mono); font-size: 8.5px; font-weight: 700; letter-spacing: .1em;
  padding: 3px 6px; border-radius: 5px;
  color: #05060a; background: var(--art-2);
  box-shadow: 0 2px 8px rgba(0,0,0,.45);
}
.style-btn[data-pro='true'][data-owned='true']::after { display: none; }

.unlock {
  display: grid; gap: 10px;
  padding: 16px; border-radius: 16px;
  border: 1px solid var(--glass-line);
  background: rgba(255,255,255,.04);
}
.unlock-head { margin: 0; font-size: 14px; font-weight: 500; color: var(--fg); }
.unlock-head b { color: var(--art-2); }
.unlock-copy { margin: 0; font-size: 11.5px; line-height: 1.5; color: var(--fg-dim); }
.unlock-copy b { color: var(--fg); }
/* The one filled, full-width control in the panel. Everything else here is
   glass and outline, so weight alone says which action is the point. */
.buy-btn {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 11px 14px;
  font-family: var(--sans); font-size: 13px; font-weight: 700;
  color: #05060a; text-decoration: none;
  background: var(--art-2); border-radius: 11px;
  transition: .18s var(--ease);
}
.buy-btn span {
  font-family: var(--mono); font-size: 11px; font-weight: 700;
  padding: 2px 7px; border-radius: 999px;
  background: rgba(0,0,0,.22);
}
.buy-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }
/* Reduced motion is on for this user, so the lift must not be the only
   feedback — brightness carries it on its own. */
@media (prefers-reduced-motion: reduce) { .buy-btn:hover { transform: none; } }

.unlock-row { display: grid; grid-template-columns: 1fr auto; gap: 7px; }
.unlock-row input {
  min-width: 0;
  padding: 8px 10px;
  font-family: var(--mono); font-size: 11px; letter-spacing: .06em;
  color: var(--fg); background: rgba(0,0,0,.32);
  border: 1px solid var(--glass-line); border-radius: 9px;
}
.unlock-row input:focus { outline: none; border-color: var(--art-2); }
.unlock-msg { margin: 0; font-family: var(--mono); font-size: 10px; color: #ff8a5c; }
.unlock-msg.ok { color: var(--art-2); }

.export-status { display: grid; gap: 7px; font-size: 11.5px; color: var(--fg-dim); }
.bar { height: 3px; border-radius: 2px; background: rgba(255,255,255,.12); overflow: hidden; }
.bar-fill { height: 100%; width: 0; background: var(--art-2); transition: width .2s linear; }

/* ═════════════════════════ drop veil ═════════════════════════ */

.dropveil {
  position: fixed; inset: 0; z-index: 40;
  display: grid; place-items: center;
  background: rgba(4, 5, 10, .82);
  backdrop-filter: blur(14px);
  opacity: 0; pointer-events: none;
  transition: opacity .22s var(--ease);
}
.dropveil.on { opacity: 1; }
.dropveil-inner {
  display: grid; gap: 8px; justify-items: center;
  padding: 52px 74px; border-radius: 26px;
  border: 2px dashed var(--art-2);
  animation: pulse 1.6s ease-in-out infinite;
}
.dropveil-inner b { font-family: var(--display); font-size: 58px; letter-spacing: .01em; }
.dropveil-inner span { font-family: var(--mono); font-size: 11px; letter-spacing: .24em; text-transform: uppercase; color: var(--fg-dim); }
@keyframes pulse { 50% { transform: scale(1.03); } }

/* ═════════════════════════ responsive ═════════════════════════ */

@media (max-width: 1080px) {
  .studio {
    position: static;
    grid-template-columns: 1fr;
    height: auto;
    padding: 20px 16px 60px;
  }
  body[data-state="studio"] .cold { display: none; }

  /* The other half of that swap, and the cause of "scroll down and there's a
     second, broken page". On desktop .studio is taken out of flow, so hiding it
     with opacity alone costs nothing. Here it is `position: static`, and an
     element at `opacity: 0` still occupies its full layout box — 2,391px of it,
     below an 812px screen. It was the workspace all along, laid out and
     invisible. Opacity hides pixels; only display removes the space. */
  body:not([data-state="studio"]) .studio { display: none; }

  .stage-frame { height: auto; width: min(100%, 340px); margin: 0 auto; }
  .rail { overflow: visible; }
  .stage { order: -1; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
  .cta::before, .export::before { animation: none; }

  /* Text that moves on its own is exactly what this preference is for, so the
     track name doesn't auto-scroll here — but the name is still *information*,
     and truncating it with no way to read the rest is a worse outcome than the
     motion. So it scrolls on hover or keyboard focus instead: user-initiated,
     which the preference permits. The !important overrides are needed to beat
     the blanket rule directly above. */
  .tc-name.is-scrolling > span { animation: none; }
  .track-swap:hover .tc-name.is-scrolling > span,
  .track-swap:focus-visible .tc-name.is-scrolling > span {
    animation-name: track-marquee !important;
    animation-duration: var(--marq-dur, 8s) !important;
    animation-timing-function: ease-in-out !important;
    animation-iteration-count: infinite !important;
    animation-direction: alternate !important;
  }
}
