/* v2 engine chassis: full-bleed scene canvas, loading state, hero overlay.
   Layout only; palette and type come from tokens.css/base.css/instruments.css,
   already shared with the rest of the site. Sections 2 through 9 are added
   in a later pass; #sections-later below is their marked, empty home. */

html, body {
  height: 100%;
  overflow: hidden; /* the virtual scroller in scroll.js owns all motion; the page itself never scrolls */
  overscroll-behavior: none;
}

#scene {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
  outline: none;
}

/* Shown only when WebGL2 is unavailable: the capture as a still image
   behind the same copy, so the page still sells. */
#scene-fallback {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  object-fit: cover;
  display: none;
}
#scene-fallback[data-active] { display: block; }

#loading {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  background: var(--bg-deep);
  transition: opacity 480ms ease;
}
#loading[data-done] {
  opacity: 0;
  pointer-events: none;
}
#loading .engraved { letter-spacing: 0.3em; }
#loading-track {
  width: min(280px, 60vw);
  height: 3px;
  background: var(--seam);
  border-radius: 2px;
  overflow: hidden;
}
#loading-fill {
  height: 100%;
  width: 0%;
  background: var(--accent);
  border-radius: 2px;
  transition: width 120ms linear;
}
#loading-pct {
  font-family: var(--font-num);
  font-size: 12px;
  letter-spacing: 0.12em;
  color: var(--text-dim);
}

/* Hero overlay: sits above the canvas, trail and grain (z-index 6 beats
   trail's 4 and the grain's 3), pointer-events off on the wrapper so the
   canvas underneath still receives pointer moves for parallax/forces, back
   on for the two keys so they stay clickable. */
#hero-overlay {
  position: fixed;
  inset: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 12vh 6vw;
  pointer-events: none;
  opacity: 0;
  transition: opacity 600ms ease;
}
#hero-overlay[data-visible] { opacity: 1; }

#hero-overlay h1 {
  letter-spacing: 0.06em;
  max-width: 100%;
  overflow-wrap: break-word;
  line-height: 0; /* the wordmark image is the whole glyph; no extra box around it */
}

/* Brand mark: sized responsively, never recoloured or filtered, only ever
   faded in as part of the hero overlay's own opacity transition. */
#wordmark {
  width: clamp(220px, 26vw, 420px);
  height: auto;
  display: inline-block;
}

#hero-overlay .lede {
  max-width: 46ch;
  margin-left: auto;
  margin-right: auto;
}

#hero-overlay .key-row {
  display: flex;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
  margin: 28px 0 0;
  pointer-events: auto;
}

#hero-overlay .key-row .key {
  text-decoration: none;
}

#hero-overlay .hero__hint {
  margin-top: 48px;
  animation: hero-hint-breathe 3.2s ease-in-out infinite;
}

@keyframes hero-hint-breathe {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

/* Debug readout, opt-in via ?debug=1. */
#debug-hud {
  position: fixed;
  left: 18px;
  bottom: 18px;
  z-index: 7;
  font-family: var(--font-num);
  font-size: 11px;
  line-height: 1.6;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  background: color-mix(in srgb, var(--chassis) 82%, transparent);
  border-radius: var(--radius);
  padding: 10px 12px;
  pointer-events: none;
  white-space: pre;
}
#debug-hud[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  #hero-overlay .hero__hint { animation: none; }
}

/* Sections 2-9: every one of these is a full-viewport overlay stacked on
   top of the same scene canvas, like #hero-overlay. Only one (during a
   crossfade, two) is ever above zero opacity at a time; opacity and
   pointer-events are both driven by static/js/v2/sections.js's weight, in
   main.js, so a section that has faded out cannot steal a drag from the
   one fading in over it. Copy sits centred exactly like the hero; each
   section adds an instrument panel below it. */
.v2-section {
  position: fixed;
  inset: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  text-align: center;
  padding: 8vh 6vw;
  pointer-events: none;
  opacity: 0;
  /* Hidden outright, not just transparent. main.js flips visibility with
     opacity every frame; this default means the twelve off-screen glass
     panels (each an 18px backdrop-filter over a canvas that repaints every
     frame) cost the compositor nothing during the load screen and in the
     no-WebGL fallback too. Looks identical: opacity 0 was already invisible. */
  visibility: hidden;
  overflow-y: auto;
  /* A section only animates its own opacity on the frame it takes over
     from a hard 0/1 (main.js also writes the value every frame while
     active), so this transition is just insurance against a stray
     rounding step reading as a flicker, never the mechanism itself. */
  /* The glass panel below is tilted in 3D against this; the perspective
     lives on the section (the "room" the card floats in), not the card
     itself, so the vanishing point stays centred on the viewport. */
  perspective: 1400px;
}

.v2-section[data-interactive] { pointer-events: auto; }
/* The one panel whose section is currently the visitor's. If a control is on
   screen it can be used, and if it cannot be used it is not on screen. */
.v2-section[data-interactive] .v2-section__panel { pointer-events: auto; }

.v2-section__copy { max-width: 62ch; }

.v2-section__copy .v2-section__caption {
  color: var(--text-strong);
}

.v2-section__eyebrow {
  font-family: var(--font-num);
  font-size: 12px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 14px;
}

.v2-section__copy h2 { margin-bottom: 14px; }
.v2-section__copy .lede { margin: 0 auto; }

/* Glass panel: a floating instrument card. transform-style/rotateX/rotateY
   are written every active frame by ui.js's createGlassTilt (damped toward
   the pointer plus a lean from scroll velocity); everything else here is
   the frosted-glass material itself, which stays static in CSS. */
.v2-section__panel {
  width: min(720px, 92vw);
  /* NOT unconditionally auto. Every section is a full-viewport layer stacked on
     every other one, so a panel that keeps pointer-events while its section is
     faded out still sits over the visible one and eats the drag: reaching for a
     fader hit an invisible neighbour instead, and the control only started
     responding once the scroll had carried it far enough to be on top.
     `pointer-events: none` on the section cannot prevent this on its own, since
     a child re-enabling itself with `auto` overrides an inherited `none`. So the
     panel stays off until main.js marks its section as the interactive one. */
  pointer-events: none;
  /* NEVER let the panel be the thing that gives.
     It is a flex item in a column that also holds the copy, so its default
     flex-shrink of 1 let the browser crush it to a 2px sliver the moment its
     content outgrew the space left over. Together with `overflow: hidden`
     below, that reads as a panel with NOTHING IN IT: every control present,
     laid out, measured, and clipped away to a hairline. A panel that is too
     tall should overflow visibly, which anyone can see and fix, rather than
     silently emptying itself. Measured at the point mode panel: 876px of
     content inside a 2px box. */
  flex: 0 0 auto;
  position: relative;
  transform-style: preserve-3d;
  will-change: transform;
  background: color-mix(in srgb, var(--canvas) 46%, transparent);
  backdrop-filter: blur(18px) saturate(130%);
  -webkit-backdrop-filter: blur(18px) saturate(130%);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 20px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55), 0 2px 0 rgba(255, 255, 255, 0.04) inset;
  overflow: hidden;
}

/* Soft inner top highlight, the light-catching edge a real pane of glass
   tilted toward the viewer would show. */
.v2-section__panel::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0) 40%);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .v2-section__panel { background: color-mix(in srgb, var(--canvas) 94%, transparent); }
}

.v2-section__panel .band {
  position: relative;
  z-index: 2;
  justify-content: space-between;
  background: color-mix(in srgb, var(--header-band) 62%, transparent);
  border-radius: 0;
}

.v2-section__panel .well {
  position: relative;
  z-index: 2;
  padding: 20px 22px;
  background: color-mix(in srgb, var(--canvas) 38%, transparent);
  box-shadow: inset 0 2px 14px rgba(0, 0, 0, 0.45);
}

/* DENSE PANEL. A panel carrying a dozen controls cannot run them down one
   column: it grows past the viewport, and the copy above it has to fit too.
   Two columns halve the height and, more importantly, group the controls into
   readable bays instead of one long ladder. Group labels span the full width
   so each bay still reads as a heading over its own controls.

   auto-fit rather than a fixed two, so a narrow window collapses it back to a
   single column on its own instead of squeezing two unusable ones. */
.well--grid {
  display: grid;
  /* EXACTLY TWO, not auto-fit. auto-fit made as many 210px columns as the panel
     was wide, so a wide panel got three or four, and since every group label
     spans the full row and most groups hold one or two controls, the extra
     columns were simply empty: a panel half full of nothing. Two columns are
     always filled by a pair, and a lone control spans them (below). */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px 20px;
  align-items: start;
}

/* A control with no partner takes the whole row rather than leaving a gap
   beside it. */
.well--grid > .fader:only-of-type,
.well--grid > .v2-section__row {
  grid-column: 1 / -1;
}
.well--grid .group-label,
.well--grid .v2-section__chips {
  grid-column: 1 / -1;
}
/* A dense panel also gets more width to spread into. Three columns of controls
   inside a wider card is much shorter than two inside a narrow one, and height
   is the dimension that runs out: the copy sits above the panel and both have
   to fit one screen together. */
/* A DENSE SECTION LAYS OUT SIDE BY SIDE. Stacked, the copy and a twelve
   control panel simply do not both fit a laptop screen: the panel's bottom rows
   fall off the bottom edge, which is the worst failure available because the
   visitor cannot tell there was anything down there. Putting the copy beside the
   panel gives the panel the full height of the section, and reads better anyway:
   the text explains what the instrument on the right is doing.

   Falls back to the stacked layout below 1100px, where two columns would leave
   both too narrow to use. */
@media (min-width: 1100px) {
  /* EVERY section, not only the dense ones. This began as a rescue for the one
     panel that would not fit stacked, but reading it beside the instrument is
     better everywhere: the copy explains what the thing on the right is doing
     while you are looking at it, instead of scrolling out of view above it. It
     also gives every panel the section's full height to grow into, which is the
     room the remaining sections need to carry real controls rather than a
     token fader. */
  .v2-section {
    flex-direction: row;
    align-items: center;
    /* THE CENTRE BELONGS TO THE CAPTURE.
       Centring the copy/panel pair put the panel squarely over the middle of
       the frame, which is where the subject is: the reason the room kept
       disappearing behind its own controls. Pushed apart instead, copy hard
       left and panel hard right, the middle third opens up and the capture is
       visible on every section without the panel having to be see-through to
       allow it. */
    justify-content: space-between;
    gap: 32px;
    text-align: left;
  }
  .v2-section .v2-section__copy {
    flex: 0 1 auto;
    /* A COMFORTABLE MEASURE, not whatever is left over.
       At 34ch beside a panel this ran to four or five words a line, which is
       the width of a newspaper column set in a headline face: the eye returns
       to the left margin so often that a two-sentence paragraph feels like
       work. Around 46ch is the low end of the range prose is actually read at,
       and it costs the panel very little. */
    max-width: 46ch;
    min-width: 0;
    flex: 1 1 46ch;
  }
  .v2-section .v2-section__panel {
    flex: 0 0 auto;
    /* Narrower, because it no longer has to be the centrepiece. The app's own
       dock is 340px; this is the floating equivalent, not a copy of it. */
    width: min(430px, 33vw);
  }
  /* A multi-bay panel earns more of the width than a single-column one. */
  .v2-section:has(.well--grid) .v2-section__panel {
    width: min(500px, 38vw);
  }
}
.v2-section:has(.well--grid) .v2-section__panel {
  width: min(1020px, 94vw);
}
/* THE WHOLE PANEL HAS TO FIT ON SCREEN AT ONCE. A control the visitor cannot
   see is not a control, and a panel that runs off the bottom edge is worse than
   one with fewer things in it: they cannot tell what they are missing. Every
   rule below buys vertical space back, in the order that costs the least.
   Measured on the point mode panel: 681px down to comfortably inside a laptop
   viewport with the copy above it. */
.well--grid { padding: 12px 16px; }
/* At four bays the default rhythm alone spent well over a hundred pixels on
   nothing but gaps. */
.well--grid .group-label {
  margin: 4px 0 2px;
  padding-top: 6px;
  font-size: 10px;
  letter-spacing: 0.18em;
}
/* A fader's own stack (name, value, rail) is the single biggest repeated cost
   at nine of them: trimming the margin and the rail height takes about 14px off
   each without making the rail any harder to hit, since the track keeps a
   comfortable target height. */
.well--grid .fader { margin: 2px 0; gap: 1px 10px; }
.well--grid .fader__name { font-size: 11px; letter-spacing: 0.04em; }
.well--grid .fader__value { font-size: 11px; }
.well--grid .fader__track { height: 18px; }
.well--grid .fader__cap { top: 2px; height: 14px; }
/* Chips stay full width so a picker reads as one row of choices, but they do
   not need the same breathing room as a control that gets dragged. */
.well--grid .v2-section__chips { gap: 5px; margin: 0; }
.well--grid .chip { padding: 4px 9px; font-size: 11px; }

.v2-section__row {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
}

.v2-section__row > * { flex: 1 1 220px; min-width: 200px; }

.v2-section__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
}

.v2-section__chips .chip {
  border: none;
  cursor: pointer;
  transition: background 120ms, box-shadow 120ms, color 120ms;
}
.v2-section__chips .chip[aria-pressed="true"] {
  background: var(--accent-dim);
  color: var(--accent-glow);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);
}
/* A chip lit by the section's own auto-narrated demo rather than a click:
   a slower, cooler glow than the deliberate accent-green selection above,
   so "the scroll is doing this" reads differently from "you picked this". */
.v2-section__chips .chip[data-auto-lit="true"] {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--mod) 55%, transparent);
  color: var(--mod);
}

.v2-section__caption {
  font-size: 13px;
  color: var(--text-dim);
  min-height: 1.4em;
  margin: 10px 0 0;
}

.v2-section__readout {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 10px;
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
}
.v2-section__readout .num { font-size: 20px; color: var(--text-strong); }
.v2-section__readout .dim { font-size: 12px; }

@media (max-width: 720px) {
  .v2-section { gap: 18px; padding: 6vh 5vw; }
  .v2-section__row { flex-direction: column; align-items: stretch; }
}

/* Section 2, Channel Masks: the mask-name badge in the panel header. */
.v2-mask__badge { display: inline-flex; align-items: center; gap: 8px; }
.v2-mask__swatch {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--hot);
  box-shadow: 0 0 7px 1px color-mix(in srgb, currentColor 50%, transparent);
  transition: background 160ms;
}

/* --- Section 4, Composer: the node graph ------------------------------- */
.v2-composer__graph {
  display: flex;
  align-items: center;
  gap: 0;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.v2-composer__node {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 11px;
  border-radius: var(--radius);
  background: linear-gradient(180deg, #344840, #405448);
  border: 1px solid rgba(255, 255, 255, 0.08);
  transition: opacity 220ms, transform 220ms, border-color 220ms;
  font-size: 12px;
  white-space: nowrap;
}
.v2-composer__node .num { color: var(--text-strong); }
/* The wire fills from left to right as the node it feeds comes online. */
.v2-composer__wire {
  --wire-fill: 0%;
  flex: 1 1 18px;
  min-width: 18px;
  height: 2px;
  background: linear-gradient(
    90deg,
    var(--accent) 0%,
    var(--accent) var(--wire-fill),
    rgba(255, 255, 255, 0.09) var(--wire-fill),
    rgba(255, 255, 255, 0.09) 100%
  );
  transition: opacity 220ms;
}

/* --- Section 5, Modulation -------------------------------------------- */
.v2-mod__scope {
  display: block;
  width: 100%;
  height: 76px;
  margin-bottom: 12px;
  border-radius: var(--radius);
  background: var(--canvas);
  box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.65);
}
.v2-mod__bands {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  height: 54px;
  margin: 12px 0 8px;
}
.v2-mod__band {
  flex: 1;
  height: 100%;
  display: flex;
  align-items: flex-end;
  background: var(--chassis);
  border-radius: 2px;
  overflow: hidden;
}
.v2-mod__band-fill {
  width: 100%;
  height: 10%;
  background: linear-gradient(180deg, var(--mod), color-mix(in srgb, var(--mod) 45%, transparent));
  transition: height 90ms linear;
}

/* --- Section 7, Drone -------------------------------------------------- */
.v2-drone__sticks {
  display: flex;
  gap: 16px;
  justify-content: center;
  margin-bottom: 12px;
}
.v2-stick {
  position: relative;
  width: 96px;
  height: 96px;
  border-radius: var(--radius);
  background: var(--chassis);
  box-shadow: inset 0 2px 12px rgba(0, 0, 0, 0.7);
}
.v2-stick__knob {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  border-radius: 50%;
  background: linear-gradient(180deg, #C2CBB4, #8A9080);
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.8);
}
.v2-drone__readout,
.v2-pipeline__counts {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 8px;
}
.v2-drone__readout .num,
.v2-pipeline__counts .num {
  color: var(--hot);
  font-size: 17px;
  font-variant-numeric: tabular-nums;
}

/* --- Section 8, Pipeline ---------------------------------------------- */

/* Buy and log in, always reachable.
   These used to be a section at the end of the lap, which meant the price was
   only visible to someone who had already scrolled the whole site. As chrome
   they sit above every section (z-index over the section layer) and stay put,
   so a visitor convinced at Effects does not have to keep scrolling to act on
   it. Not part of the loop, so nothing here is driven by scroll weight. */
#site-chrome {
  position: fixed;
  top: 18px;
  right: 20px;
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 10px;
  pointer-events: auto;
}

#site-chrome .chip,
#site-chrome .key {
  text-decoration: none;
  white-space: nowrap;
}

@media (max-width: 560px) {
  #site-chrome { top: 12px; right: 12px; gap: 7px; }
  #site-chrome .chip { display: none; } /* the price survives; the login link folds away first */
}

/* HIDE: the interface gets out of the way and leaves the capture alone.
   The chrome and the hero fade rather than display:none, so coming back is a
   fade too and never a flash of relayout. Sections are handled in main.js,
   because their opacity is already written every frame from scroll weight and a
   competing CSS rule would fight it. */
body.ui-hidden #site-chrome,
body.ui-hidden #hero-overlay {
  opacity: 0;
  pointer-events: none;
}

#site-chrome,
#hero-overlay {
  transition: opacity 420ms ease;
}

/* The way back. Only says anything while hidden, and sits out of the way at the
   bottom of the frame: with everything else gone, a visitor needs one small
   reminder that this is a mode and not a broken page. */
#hide-hint {
  position: fixed;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  z-index: 9;
  margin: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 420ms ease;
}
body.ui-hidden #hide-hint { opacity: 0.55; }

.hero__shortcut {
  margin-top: 18px;
  opacity: 0.6;
}

/* The automation lane. Full width of the panel because a lane is read
   horizontally: squeezing it into one grid column would make two bars of
   musical time about as legible as a dash. */
.v2-timeline__lane {
  grid-column: 1 / -1;
  width: 100%;
  /* Four lanes need room to be four lanes. At 92px they were stacked ribbons
     with no space to grab a key in. */
  height: 196px;
  cursor: crosshair;
  display: block;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--chassis) 70%, transparent);
  box-shadow: inset 0 1px 10px rgba(0, 0, 0, 0.5);
}

/* The wordmark, doubling as the way home.
   A loop has no top, so "back to the start" is not a scroll position anyone can
   find by scrolling: it is a jump, and the mark everyone already reads as home
   is the obvious thing to hang it on. Sized small enough to sit as chrome rather
   than compete with the hero's own wordmark. */
/* TOP LEFT, AND IT NEVER LEAVES. The wordmark is the one thing on screen that
   should always be there: it is the brand, it is the way home, and with the
   interface hidden it is the only sign you are still on a page rather than
   looking at a broken canvas. So it sits outside the account chrome and is
   deliberately NOT faded by body.ui-hidden. */
#home-mark {
  position: fixed;
  top: 18px;
  left: 20px;
  z-index: 9;
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  line-height: 0;
  opacity: 0.72;
  pointer-events: auto;
  transition: opacity 200ms ease;
}
#home-mark:hover,
#home-mark:focus-visible { opacity: 1; }
#home-mark img {
  width: 78px;
  height: auto;
  display: block;
}

@media (max-width: 560px) {
  #home-mark { top: 12px; left: 12px; }
  #home-mark img { width: 58px; }
}

/* The mask's own readout, inside the Effects panel. Spans the grid so a full
   sentence is not chopped into a column, and sits directly under the group
   label it belongs to. */
#mask-readout {
  grid-column: 1 / -1;
  margin: 0 0 2px;
  font-size: 11px;
  opacity: 0.75;
}

/* THE TARGET PICKERS. One per lane, and the reason this section exists: opening
   one shows every parameter the page drives, grouped, in a list that needs no
   explaining. The breadth IS the argument, so the control is deliberately a
   plain native select rather than something custom that would show four entries
   at a time. */
.v2-timeline__targets {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 6px 10px;
  margin-bottom: 2px;
}

.v2-timeline__target {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
}

/* Ties the row to its lane on the sheet above, which is the only thing that
   says WHICH lane a picker belongs to. */
.v2-timeline__swatch {
  width: 8px;
  height: 8px;
  border-radius: 2px;
  flex: 0 0 auto;
}

.v2-timeline__target select {
  flex: 1 1 auto;
  min-width: 0;
  appearance: none;
  font-family: var(--font-num);
  font-size: 11px;
  letter-spacing: 0.03em;
  color: var(--text-strong);
  background: var(--chassis);
  border: 1px solid var(--seam);
  border-radius: var(--radius);
  padding: 5px 8px;
  cursor: pointer;
}

.v2-timeline__target select:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 1px;
}

/* The dropdown itself is drawn by the OS, which does not inherit the page's
   dark palette, so the options need their own colours or they render as black
   text on black on some platforms. */
.v2-timeline__target option,
.v2-timeline__target optgroup {
  background: var(--chassis);
  color: var(--text-strong);
}

/* The audio note. Only says anything once someone has asked for audio, and then
   it says exactly what happened, because "allow the prompt" and "install a
   loopback device" are different problems with different fixes. */
#mod-audio-note {
  grid-column: 1 / -1;
  margin: 0 0 2px;
  font-size: 11px;
  line-height: 1.4;
  opacity: 0.75;
}


/* THE MACRO KNOBS. Round because a macro is a knob in the program and because a
   performer's hand knows a knob; the site's faders are for setting a value, and
   these are for moving several at once. */
.v2-macros {
  display: flex;
  justify-content: center;
  gap: 34px;
  margin-bottom: 16px;
}

.v2-macro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
}

.v2-macro__dial {
  position: relative;
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: var(--chassis);
  border: 1px solid var(--seam);
  box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.55), 0 1px 0 rgba(255, 255, 255, 0.04);
  cursor: ns-resize;
  touch-action: none;
}

/* The pointer: a spoke from the centre, rotated to the knob's position. */
.v2-macro__fill,
.v2-macro__live {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 2px;
  height: 26px;
  margin-left: -1px;
  transform-origin: 50% 100%;
  margin-top: -26px;
  border-radius: 1px;
  pointer-events: none;
}

.v2-macro__fill { background: var(--accent); }

/* The LIVE position, when a driver is moving the knob away from where the
   performer left it. Same base/live split the modulation fader uses, for the
   same reason: the hand-set position must stay visible underneath. */
.v2-macro__live {
  background: var(--mod);
  opacity: 0;
  transition: opacity 180ms ease;
}

.v2-macro__label { font-size: 10px; letter-spacing: 0.16em; }
.v2-macro__value { font-size: 11px; color: var(--text-strong); }

/* A stored slot is visibly different from an empty one, or a performer cannot
   tell what is safe to press. */
.v2-slot { min-width: 38px; }
.v2-slot--stored { border-color: var(--accent); }

.v2-perform__list {
  margin: 0;
  padding-left: 18px;
  display: grid;
  gap: 6px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text-dim);
}
.v2-perform__list b { color: var(--text-strong); font-weight: 600; }

/* A THIRD COLUMN, for the one panel that genuinely has too much to say.
   Particles carries seventeen faders plus two chip rows, because it is standing
   in for a sixty-parameter system, and in two columns that is 873px against a
   900px viewport: it fits by 27 pixels on a laptop and not at all on anything
   shorter. Three columns brings it back under two thirds of the frame.

   Only this panel gets it. Three columns of controls is denser than the rest of
   the site wants to read, and applying it everywhere would make the sparse
   panels look like spreadsheets. */
@media (min-width: 1100px) {
  .v2-section:has(.well--wide) .v2-section__panel {
    width: min(560px, 42vw);
  }
  .well--wide {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  /* A group label still owns its own row, or the groups stop reading as groups. */
  .well--wide .group-label,
  .well--wide .v2-section__chips {
    grid-column: 1 / -1;
  }
}

/* NARROW SCREENS: THE PANEL SCROLLS ITSELF.
   Measured at 390x844, the Effects panel is 1134px and Particles is 1293px
   against an 844px viewport. Stacked single-column there is no arrangement of
   seventeen faders that fits, and the site's own rule is that a control you
   cannot see is not a control. So on narrow screens the panel becomes its own
   scroll area with a hard height, which is honest: the scrollbar says there is
   more, where an overflowing panel says nothing at all.

   `overscroll-behavior: contain` stops a flick that reaches the end of the panel
   from continuing into the loop, which would otherwise fling the visitor into
   the next section the moment they reached the last fader. */
@media (max-width: 1099px) {
  .v2-section__panel {
    max-height: calc(100vh - 190px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* A visible edge, so "there is more below" is a thing you can see rather than
     a thing you have to try. */
  .v2-section__panel::after {
    content: "";
    position: sticky;
    bottom: 0;
    display: block;
    height: 26px;
    margin-top: -26px;
    background: linear-gradient(to top, color-mix(in srgb, var(--canvas) 85%, transparent), transparent);
    pointer-events: none;
  }
}

/* THE SECTION RAIL. Right edge, vertically centred: clear of the copy (left)
   and the panel (right of centre), and where the eye already goes to ask how
   much is left.

   EVERY TICK IS THE SAME LENGTH. The first version scaled each tick's WIDTH by
   its section's weight, which meant thirteen different lengths flush against the
   right edge and therefore a ragged left edge that changed shape as you
   scrolled. It read as a rendering fault rather than a design, because a ragged
   edge is what broken layout looks like and nothing about it said "on purpose".

   So position is carried by COLOUR alone, on a fixed grid. The active tick is
   accent-coloured and fully opaque, its neighbours dim in proportion during a
   crossfade, and the column stays a clean flush rectangle at every scroll
   position. A ruler, not a waveform. */
#section-rail {
  position: fixed;
  right: 16px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 8;
  display: flex;
  flex-direction: column;
  /* No gap: the spacing lives in each dot's own padding, so the rhythm is one
     number rather than two that have to be kept in agreement. */
  gap: 0;
  pointer-events: auto;
}

.rail__dot {
  appearance: none;
  background: none;
  border: 0;
  position: relative;
  padding: 4px 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
}

/* Fixed 18px. Never animated, so the column's silhouette is constant. */
.rail__tick {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 1px;
  background: color-mix(
    in srgb,
    var(--accent) calc(100% * var(--w, 0)),
    color-mix(in srgb, var(--text-strong) 26%, transparent)
  );
  transition: background 200ms ease;
}

.rail__dot:hover .rail__tick {
  background: color-mix(in srgb, var(--text-strong) 70%, transparent);
}

/* The name only on hover or focus: thirteen labels down the edge of every frame
   would compete with the section copy, which is the thing meant to be read. */
.rail__label {
  /* OUT OF LAYOUT FLOW. Hidden with opacity alone, each label still occupied its
     own width, so every dot sized to its own section name and the ticks landed
     at twelve different x positions: the exact ragged column this was meant to
     fix, just caused one step further up. Absolute keeps the dot's width equal
     to the tick's. */
  position: absolute;
  right: 28px;
  font-family: var(--font-num);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  white-space: nowrap;
  opacity: 0;
  transform: translateX(6px);
  transition: opacity 160ms ease, transform 160ms ease;
  pointer-events: none;
}
.rail__dot:hover .rail__label,
.rail__dot:focus-visible .rail__label {
  opacity: 1;
  transform: translateX(0);
}
.rail__dot:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }

body.ui-hidden #section-rail { opacity: 0; pointer-events: none; }
#section-rail { transition: opacity 420ms ease; }

@media (max-width: 700px) {
  #section-rail { right: 8px; }
  .rail__dot { padding: 3px 0; }
  .rail__tick { width: 12px; }
  .rail__label { display: none; }
}

/* NARROW SCREENS: THE PANEL SCROLLS ITSELF.
   Measured at 390x844, the Effects panel is 1134px and Particles is 1293px
   against an 844px viewport. Stacked single-column there is no arrangement of
   seventeen faders that fits, and the site's own rule is that a control you
   cannot see is not a control. So on narrow screens the panel becomes its own
   scroll area with a hard height, which is honest: the scrollbar says there is
   more, where an overflowing panel says nothing at all.

   `overscroll-behavior: contain` stops a flick that reaches the end of the panel
   from continuing into the loop, which would otherwise fling the visitor into
   the next section the moment they reached the last fader. */
@media (max-width: 1099px) {
  .v2-section__panel {
    max-height: calc(100vh - 190px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* A visible edge, so "there is more below" is a thing you can see rather than
     a thing you have to try. */
  .v2-section__panel::after {
    content: "";
    position: sticky;
    bottom: 0;
    display: block;
    height: 26px;
    margin-top: -26px;
    background: linear-gradient(to top, color-mix(in srgb, var(--canvas) 85%, transparent), transparent);
    pointer-events: none;
  }
}


/* A SCRIM BEHIND THE COPY.
   The capture is bright, busy and MOVING, and the copy sits straight on top of
   it. White text on a field of pale splats is legible in a screenshot and not
   legible while the thing underneath is scrolling: the eye keeps re-acquiring
   the letterforms against a background that will not hold still.

   The panel solves this for itself with backdrop-blur. The copy had nothing, so
   it gets a soft directional scrim: dense behind the text, gone by the time it
   reaches the middle of the frame, so the capture is never boxed in. Sits behind
   the text only (z-index 0 against the copy's own z-index 1), and is
   pointer-transparent so the pointer field still works through it. */
.v2-section__copy {
  position: relative;
  z-index: 1;
}
.v2-section__copy::before {
  content: "";
  position: absolute;
  inset: -32px -80px -32px -60px;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    120% 90% at 22% 50%,
    color-mix(in srgb, var(--bg-deep) 88%, transparent) 0%,
    color-mix(in srgb, var(--bg-deep) 74%, transparent) 42%,
    transparent 78%
  );
}

/* Stacked layout centres the copy, so the scrim centres with it. */
@media (max-width: 1099px) {
  .v2-section__copy::before {
    inset: -24px -28px;
    background: radial-gradient(
      120% 100% at 50% 50%,
      color-mix(in srgb, var(--bg-deep) 88%, transparent) 0%,
      color-mix(in srgb, var(--bg-deep) 72%, transparent) 46%,
      transparent 82%
    );
  }
}
