/* Generated from web/style.css by tools/sync-ui.py — do not edit.
   Edit the source and re-run the script; validate_ui.py fails
   on a copy that has drifted from it. */
/* Pitcheon — warm-dark studio instrument, amber-lit like old stereo gear.
   Gold is "on pitch"; sharp runs hot toward red and flat runs cold toward
   blue, so deviation is legible peripherally while you are busy singing and
   not reading numbers.

   Shoelace supplies the controls (focus management, keyboard nav, ARIA);
   everything below re-skins its tokens so the kit disappears into the panel
   instead of announcing itself. */


/* ---------- Shoelace token overrides ---------- */


/* ============================================================================
   A second machine.

   Cold rolled steel and a green phosphor tube, where the default is walnut and
   amber. It shares every line of component CSS with it — nothing below this
   comment is a rule, only bindings — which is the whole claim of the token
   layer and the reason this theme exists at all (DS-503). If a component had
   needed changing to build it, that component would have been the finding.

   Its own primitives first, then the contract bound to them. A theme may read
   a --p-* ; a component may not.
   ========================================================================= */


/* Tooltips here explain controls you already know once you have used them
   twice, so they should only appear on a deliberate rest — at Shoelace's
   150ms they fired while the cursor was merely crossing the fascia. */
sl-tooltip {
  --show-delay: 700ms;
  --hide-delay: 100ms;
  /* A tooltip here names the control, it does not explain it — a sentence over
     a key you already understand is a banner across the fascia. One line. */
  --max-width: 220px;
}
/* Opaque, and said here rather than left to --sl-tooltip-background-color.
   Whatever that variable was resolving to, it was arriving translucent: the
   fascia and the lit display behind a tooltip showed straight through the
   words. A tooltip is a card held in front of the machine, not a filter over
   it — and over a panel that is itself glowing, "mostly opaque" is not opaque.
   Given the panel's own material while we are here: a dark card with the same
   hairline every other raised thing on this fascia carries. */
sl-tooltip::part(body) {
  background-color: #1a1512;
  border: 1px solid rgba(255, 255, 255, .075);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, .04) inset,
    0 10px 24px -10px rgba(0, 0, 0, .95),
    0 2px 6px rgba(0, 0, 0, .5);
  color: var(--ink-dim);
}
/* The arrow is a separate element and takes its own fill, so it has to be told
   the same colour or it reads as a grey pip under a brown card. */
sl-tooltip::part(base__arrow) { background-color: #1a1512; }


* { box-sizing: border-box; }

/* ---- the blip ----
   Randomised in flicker.js and snapped here: a display that flickers does not
   ease into it. steps(1, end) holds each frame flat, so the jump and the surge
   land whole rather than sliding, which is the difference between a fault and
   a transition. Transform, not position, so nothing round it reflows. */
[data-flicker][data-blip] {
  animation: msg-blip var(--blip-ms, 180ms) steps(1, end) both;
}
@keyframes msg-blip {
  0%   { transform: none;                                    filter: none; }
  16%  { transform: translate(var(--blip-x), var(--blip-y)); filter: brightness(var(--blip-up, 1.5)); }
  33%  { transform: none;                                    filter: brightness(var(--blip-down, .7)); }
  52%  { transform: translate(var(--blip-x), 0);             filter: brightness(var(--blip-up, 1.5)); }
  68%  { transform: none;                                    filter: brightness(var(--blip-down, .7)); }
  84%  { transform: translate(0, var(--blip-y));             filter: none; }
  100% { transform: none;                                    filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  [data-flicker][data-blip] { animation: none; }
}

/* The pixel face renders with antialiasing off, and only it. A face drawn as
   squares on a grid wants a hard edge; smoothed, its one-pixel stem becomes a
   grey smear. Everything set in Work Sans must stay smoothed — aliased text at
   reading size is worse than either. The bloom around the letters is a
   text-shadow and is unaffected by this either way. */
/* Only faces that are actually drawn on a grid. .knob-label, .dial-wrap and
   .stat-label were in here and are all set in Work Sans — aliasing an outline
   face does not make it crisp, it makes it ragged. */
.dsp-text, .led, .verdict, .curtain p {
  -webkit-font-smoothing: none;
  font-smooth: never;
}

html, body {
  height: 100%;
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-weight: 400;
  overflow: hidden;
}

/* One column. The library used to open as a second one between the rail and
   the stage; it is a list on the fascia's glass now, so neither is left. */
body {
  display: grid;
  grid-template-columns: 1fr;
  /* A slow warm bloom from the lower left, like a valve amp behind the panel. */
  background-image:
    var(--room-glow);
}

/* Film grain keeps the flat panels from looking like plastic. */
.grain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: var(--layer-grain);
  opacity: var(--dim-ghost);
  mix-blend-mode: overlay;
  background-image: var(--grain-tex);
  background-size: 150px 150px;
}


/* ---------- stage ---------- */

.stage {
  display: grid;
  grid-template-rows: 1fr auto auto;
  min-height: 0;
  /* No padding at all: the fascia runs edge to edge and each band re-adds its
     own inset. */
  padding: 0;
  gap: 0;
}

/* The keys that change what the screen shows, on their own band between the
   screen and the fascia — the same relationship the amp's under-bar has to its
   display. There is no header any more: the song's name is printed on the
   fascia's glass, where the machine already says what is loaded, and a second
   copy of it across the top was a web page's habit rather than an
   instrument's.
   No background of its own. It is the same continuous piece of fascia the
   window is cut into, and a band with its own gradient would put another edge
   back where one has just been taken out.
   Two gaps, and the difference between them says which things belong together:
   12px between keys, 8px between the takes key and its own readout — the same
   split the fascia makes between --key-gap and the tighter spacing that binds
   a mark to the key it labels. */
.stage-keys {
  /* Three tracks rather than a row with gaps: the score has to sit on the
     centre of the app, and between two clusters of different widths it would
     otherwise sit wherever they left it. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-xl);
  /* 16 at the sides, not 22: the same inset the window above keeps, so the
     first key on the ledge starts on the window's own left edge and the last
     ends on its right. The ledge is the sill under that window, and a sill
     that begins six pixels inside it reads as a second, narrower thing. */
  padding: 7px 16px 16px;
  /* A sill rather than more of the same fascia: a step lighter than the panel
     above and below, so the band reads as a ledge the keys are mounted on.
     The line at its foot is an inset shadow, not a border — a border draws
     even at zero height, which would leave a hairline across the fascia with
     no song loaded. */
  background-image:
    var(--grain-tex),
    var(--keys-face);
  background-size: 150px 150px, auto;
  background-blend-mode: multiply, normal;
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, .055);
  /* Closed. There is nothing to edit the lyrics of and no takes to hear until
     a song is chosen, so the ledge is not there — it grows out of the amp's
     top edge when one is. box-sizing is border-box everywhere, so a height of
     zero takes the padding with it. */
  height: 0;
  min-height: 0;
  overflow: hidden;
  transition: height var(--motion-slower) var(--ease);
}
/* Its open height is the padding plus the tallest key, and it has to be a
   number rather than auto because that is what the growth animates against —
   which also means it does not grow when something taller is put on the ledge.
   23px of padding plus a 34px icon key, which is the tallest thing here. */
body[data-song="on"] .stage-keys { height: 57px; }
/* The keys ride up with the ledge rather than being revealed by it, and arrive
   a beat after it has finished opening. */
.stage-keys > * {
  transform: translateY(10px);
  opacity: 0;
  transition: transform var(--motion-slower) var(--ease), opacity var(--motion-slow) var(--ease);
}
body[data-song="on"] .stage-keys > * {
  transform: none;
  opacity: 1;
  transition-delay: .12s;
}
@media (prefers-reduced-motion: reduce) {
  .stage-keys, .stage-keys > * { transition: none; }
}
/* .btn is width:100% by default, because on the fascia every key it is used
   for either sits in a fixed-size slot or is an icon square. Here they are
   loose in a flex row, so they have to be told to hug their legend or the
   first one takes the whole band. */
.stage-keys .btn { width: auto; }
.stage-keys .btn-face { padding: 9px 17px; }
/* Rows, not blocks. These are containers for the two clusters either side of
   the score, and while each held a single child it made no difference — the
   moment the right one held three they stacked down the ledge and were clipped
   by its fixed height. 12px between things, the same as between the tracks. */
.keys-left,
.keys-right {
  display: flex;
  align-items: center;
  gap: var(--space-xl);
}
.keys-left { justify-self: start; }
.keys-right { justify-self: end; }

/* Legend then window, on one line — the ledge is a band and a stacked pair
   would set its height. A step up from the takes count beside it: this is the
   reading the whole run is judged on, and that one is an inventory. */
.scoreboard { display: flex; align-items: center; gap: 22px; }
.stat { display: flex; align-items: center; gap: 9px; }
/* Engraved on the fascia rather than printed on a screen, so it is set in the
   fascia's face — the same as every knob legend beside it. The pixel font is
   for things behind glass; using it out here made the ledge look like a strip
   of display it is not. */
.stat-label {
  font-size: var(--type-sm);
  font-weight: 500;
  letter-spacing: var(--tracking-xl);
  text-transform: uppercase;
  color: var(--ink-faint);
  text-shadow: 0 1px 0 rgba(0, 0, 0, .7);
}
.led--stat {
  min-width: 54px;
  padding: 7px 10px;
  font-size: var(--type-xl);
  text-align: right;
}
.led--stat .led-on { top: 7px; right: 10px; }
.head-takes { display: flex; align-items: center; gap: var(--space-md); }
/* The icon keys on a fascia band are square; only the ones carrying a legend
   hug their text. `.stage-keys .btn` sets width:auto and outranks .btn--icon's
   own sizing, so the square ones have to say so again at the same depth. */
.stage-keys .btn--icon,
.panel-head .btn--icon { width: var(--btn-size); }
/* The amp's key size, not one of their own. Every icon key on the fascia is
   34px; a panel's head is the same piece of casework, and two sizes of the
   same key on one machine reads as two machines.
   On every key in the head, not only the round ones: the lyric editor's head
   carries legend keys, and left at the default 40px they made that panel's
   head six pixels taller than the takes panel's — which puts the two windows
   at different heights, the exact thing min-height was added to stop. */
.panel-head .btn { --btn-size: 34px; height: var(--btn-size); }
.panel-head .btn-face { height: calc(var(--btn-size) - 9px); }
.stage-keys .btn--icon .btn-face,
.panel-head .btn--icon .btn-face { padding: 0; }
#takesBtn[hidden] { display: none; }
#takesCount[hidden] { display: none; }
/* The count in its own window rather than appended to the legend. A key's
   legend is stamped into the cap and does not change — `takes · 3` was a
   readout pretending to be a label. This is the readout. */
.led--count {
  min-width: 40px;
  padding: var(--space-sm) var(--space-md);
  font-size: var(--type-lg);
  text-align: right;
}
.led--count .led-on { top: 6px; right: 8px; }
/* The cap does not light for holding takes — only for being pressed. A lit
   legend on this fascia means "this is on", and the window beside it already
   says how many are held; two signals for one fact leaves neither meaning
   anything. */


/* ---------- glass ---------- */

/* The material has to be declared before anything made of it: `.glass > *`
   and the panels' own rules are all one class deep, so at equal specificity
   source order is the only thing deciding them — and the roll's overlays are
   absolutely positioned, which `.glass > *` would otherwise undo. */

/* Smoked glass over a lamp. One material, two sizes: the fascia's readout and
   the roll above it are the same piece of glass set into the same panel, so
   there is one place to change what glass looks like in this app rather than
   two that drift.
   The layers, outside in: the rim, the lamp behind (::before), whatever is
   printed on it (z-index 1), the scan lines (2), and the room reflected in
   the front surface (3). */
.glass {
  /* What the glass is lit in. Everything printed on it — the text, the pointer
     in the margin, the pixel wash — takes its colour from here, so a panel
     that means something different can say so with one declaration rather than
     an override per element. */
  --dsp-lit: var(--tune);
  --dsp-lit-dim: var(--lit-dim);
  --dsp-lit-rgb: var(--lit-rgb);
  /* and what the reader's mark is printed in over it. Rebinding the property
     rather than writing a second ::selection rule: a pseudo-element inherits
     custom properties from the element it belongs to, so the one rule near the
     top of this file serves the fascia and the glass both. */
  --select-ink: var(--glass-ink);
  /* The lamp behind the glass: where it sits, how far it throws, how hard. A
     readout is lit from a strip along its foot; a panel the size of the roll
     wants a pool in the middle of it instead, and at the same settings the
     first arrangement would be a sunrise across the bottom edge. */
  --lamp-at: 50% 128%;
  --lamp-wide: 72%;
  --lamp-reach: 118%;
  --lamp-strength: 1;
  /* The unlit glass itself, warming toward the foot where the lamp is. Held as
     variables because how dark it needs to be depends on how much fascia is
     around it: a readout ringed by panel on all four sides reads as a hole at
     one value, and a panel the width of the window needs to be darker before
     its bottom edge stops dissolving into the fascia under it. */
  --glass-top: var(--glass-face-top);
  --glass-mid: var(--glass-face-mid);
  --glass-foot: var(--glass-face-foot);
  /* How far the frame's shade reaches in from the rim. A proportion of the
     panel, so a large one needs a much larger figure than a readout does. */
  --vignette: 34px;
  position: relative;
  border-radius: var(--well-radius);
  /* The lamp is a background layer rather than a pseudo-element: it belongs
     behind everything printed on the glass, which is exactly what a background
     layer is, and it frees ::before for the rays. */
  background:
    /* Driven from --dsp-lit-rgb rather than a fixed amber, so a panel that
       changes what it is lit in changes the lamp with it. A display asking
       whether to destroy something prints in red on a glass still glowing
       amber, which reads as red text on somebody else's screen — the light
       behind it is the part that makes it the machine's own alarm. */
    radial-gradient(ellipse var(--lamp-wide) var(--lamp-reach) at var(--lamp-at),
      rgba(var(--dsp-lit-rgb), calc(.26 * var(--lamp-strength))) 0%,
      rgba(var(--dsp-lit-rgb), calc(.11 * var(--lamp-strength))) 34%,
      rgba(var(--dsp-lit-rgb), calc(.035 * var(--lamp-strength))) 58%,
      rgba(var(--dsp-lit-rgb), 0) 78%),
    linear-gradient(180deg,
      var(--glass-top) 0%, var(--glass-mid) 52%, var(--glass-foot) 100%);
  /* Both edge lines are spread rings, not offset shadows. --well-out is a pair
     of shadows nudged up and down by 1px, which along a straight edge reads as
     a hairline parallel to the glass but around a corner pinches to nothing on
     one side and doubles on the other — an offset cannot hold a constant
     distance from a curve. A spread ring is concentric by construction, so the
     gap stays 1px the whole way round. */
  /* Only the outer rims here. The bright ring inside the glass is drawn on
     ::after instead — see there for why.
     Two rings, 1px each, and the order is the point: the dark one is listed
     first so it paints over the light one, leaving the light one showing as a
     single pixel *outside* it. That is the edge of the cut catching the
     overhead lamp, with the shadow of the cut under it — which is what a hole
     machined through a panel actually looks like from the front.
     The dark ring was 2px on its own, because at 1px it vanished into a fascia
     nearly as dark as itself and the glass floated. It does not need the second
     pixel now: the light ring gives the edge somewhere to end. */
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, .92),
    0 0 0 2px rgba(206, 190, 166, .045);
  overflow: hidden;
}

/* The beam.
   A wedge of light thrown from the fixed source toward the pointer, drawn as
   the panel's own ruling in light rather than shadow — a raking light does not
   wash a ridged surface evenly, it catches the ridges.
   Two masks intersected. The conic one cuts the wedge, centred on the angle
   glass.js measures from the source to the pointer. The radial one fades it
   with distance, and its radius is the distance to the pointer — so the light
   is strongest as it leaves the lamp and has died by the time it gets there,
   which is what stops it reading as something attached to the cursor. */
/* The #000 stops below are a mask, not a colour: in -webkit-mask-image black
   means opaque and transparent means cut away, so they are opacity written in
   hex. Nothing here is themeable and none of it should be tokenised — a theme
   that changed them would not recolour the beam, it would reshape it. */
.glass::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  z-index: var(--layer-veil);
  background: repeating-linear-gradient(180deg,
      rgba(255, 255, 255, .1) 0px,
      rgba(255, 255, 255, .1) 1px,
      rgba(255, 255, 255, 0) 1px,
      rgba(255, 255, 255, 0) 2px);
  /* One wedge, written twice for the prefixed property. The half-width is a
     variable so the spread can be tuned without touching the arithmetic: the
     cone runs from --beam-deg minus that to plus it, with a soft third at each
     edge. Wide, because from a source this far off the panel a narrow cone
     arrives as a stripe. */
  --beam-half: 34deg;
  -webkit-mask-image:
    conic-gradient(from calc(var(--beam-deg, 180deg) - var(--beam-half))
        at var(--sx, 135%) var(--sy, -90%),
      transparent 0deg,
      #000 calc(var(--beam-half) * .6),
      #000 calc(var(--beam-half) * 1.4),
      transparent calc(var(--beam-half) * 2)),
    radial-gradient(circle var(--beam-len, 900px) at var(--sx, 135%) var(--sy, -90%),
      #000 0%, #000 38%, rgba(0, 0, 0, .45) 70%, transparent 100%);
  mask-image:
    conic-gradient(from calc(var(--beam-deg, 180deg) - var(--beam-half))
        at var(--sx, 135%) var(--sy, -90%),
      transparent 0deg,
      #000 calc(var(--beam-half) * .6),
      #000 calc(var(--beam-half) * 1.4),
      transparent calc(var(--beam-half) * 2)),
    radial-gradient(circle var(--beam-len, 900px) at var(--sx, 135%) var(--sy, -90%),
      #000 0%, #000 38%, rgba(0, 0, 0, .45) 70%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  opacity: 0;
  transition: opacity var(--motion-slowest) var(--ease);
  pointer-events: none;
}
/* Only while there is a hand near it. Otherwise the lamp is on and nothing on
   the panel is moving. */
.glass[data-lit]::before { opacity: 1; }

/* The room, on the front of the glass — and fixed. It used to slide with the
   pointer, which made the panel look lit by whoever was looking at it. Light
   in a room comes from where it comes from; what moves when you move is which
   part of the surface catches it, and that is the rays above. */
.glass::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Inherited, and not optional. An inset ring on a square pseudo-element
     clipped by a rounded parent draws a rectangle with its corners cut off —
     which is what a crisp 1px line makes visible and a diffuse vignette hides.
     The rings only followed the curve while they were on the element itself. */
  border-radius: inherit;
  z-index: var(--layer-edge);
  background:
    /* Not the lamp — that is off the panel and cannot be drawn. This is the
       light spilling in over the corner nearest it, which is the only part of
       a source outside the frame you ever actually see. */
    radial-gradient(ellipse 58% 46% at 96% -6%,
      rgba(255, 255, 255, .085) 0%,
      rgba(255, 255, 255, .03) 42%,
      rgba(255, 255, 255, 0) 76%),
    linear-gradient(180deg,
      rgba(255, 255, 255, .05) 0%,
      rgba(255, 255, 255, .01) 14%,
      rgba(255, 255, 255, 0) 30%);
  /* Three rings, outside in, and all of them here rather than on the element.
     ::after is the topmost layer of the panel, and the vignette is strong
     enough — 120px of it on the roll — to wash out anything drawn beneath it.
     The bright line was on the element itself and the roll's vignette was
     simply painting over it.
     Order matters: the first inset shadow is on top, so the dark pixel at the
     very rim covers the outer pixel of the light ring beneath it, leaving that
     ring reading as a bright line one pixel in from the edge. */
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, .5),
    inset 0 0 0 2px rgba(255, 255, 255, .1),
    inset 0 0 var(--vignette) rgba(0, 0, 0, .55);
  pointer-events: none;
}

/* Scan lines. Two device pixels on, two off — any finer and it moirés against
   the pixel font; any coarser and it reads as stripes rather than as a lit
   panel. Kept under the reflection so the glass sits over them. */
.glass .dsp-scan {
  position: absolute;
  inset: 0;
  z-index: var(--layer-scan);
  background: repeating-linear-gradient(180deg,
      rgba(0, 0, 0, .22) 0px,
      rgba(0, 0, 0, .22) 1px,
      rgba(255, 255, 255, .012) 1px,
      rgba(255, 255, 255, .012) 2px);
  pointer-events: none;
  mix-blend-mode: multiply;
  opacity: var(--dim-mid);
}

.glass > * { position: relative; z-index: var(--layer-content); }

/* Nothing on a fascia is text you can drag a highlight across. Every legend,
   readout, list row and lit key here is a painted or machined surface, and one
   with half a sentence selected across it stops looking like an object.
   Given back where there is genuinely text to take: the lyric editor, and
   anything you might want to copy out of the machine rather than read off it. */
body {
  -webkit-user-select: none;
  user-select: none;
}
input,
textarea,
[contenteditable],
.lyrics,
.curtain-say {
  -webkit-user-select: text;
  user-select: text;
}

::selection {
  background: var(--select-bg);
  color: var(--select-ink);
}

/* ---------- roll ---------- */

/* The middle band of the stage, between header and transport. Takes open as a
   column here rather than as a sheet over the whole app, so the roll stays
   visible while you read the results. */
.stage-body {
  display: grid;
  /* Two tracks: the window, and the panel that pushes it. 0px rather than 0fr,
     because grid-template-columns only interpolates when each pair of tracks
     is interpolable and a <flex> does not interpolate with a <length> — that
     swap was discrete, which is why the column used to appear at full width in
     one frame. */
  grid-template-columns: 0px 1fr 0px;
  min-height: 0;
  /* Hides the part of the panel that has not arrived yet — it keeps its full
     width and overflows its track while the track is opening.
     A clip rather than overflow:hidden, and open at the foot on purpose. The
     window below is flush with the bottom of this box and its outer rim is a
     2px ring drawn *outside* its border box, so anything clipping tight to the
     box shaves that ring off along the whole bottom edge. */
  clip-path: inset(0 0 -6px 0);
  transition: grid-template-columns var(--motion-slower) var(--ease);
  /* Fascia, continuing the header's gradient down into the amp's. The body's
     own cool bloom used to show through here, so the roll sat in a bluish
     surround with warm panel above and below it — one machine with a window
     cut in it reads better than three surfaces that happen to be stacked. */
  background-image:
    var(--grain-tex),
    var(--stage-face);
  background-size: 150px 150px, auto;
  background-blend-mode: multiply, normal;
}


/* The same fascia as the amp face, in the same gradient — this is another
   panel of the machine, not a drawer bolted to the side of it. Set into the
   box the way the screens are: held off the top and the right edge, corners
   rounded, with a hairline ring rather than a border, so the edge lives inside
   the box and cannot draw while the column is closed.
   Flush at the foot, where the key ledge carries on beneath it. */

/* Each panel watches its own state. Without the modifier this matched both of
   them, since they are the same component — opening takes would have made the
   lyric editor visible in a track still nought pixels wide. */
body[data-lyrics="open"] #lyricsPanel,
body[data-help="open"] #helpPanel {
  visibility: visible;
  transition: visibility 0s;
}

/* Which side it comes in from. Not a preference: the key that opens it says
   where it belongs — takes is at the left end of the ledge and "edit lyrics"
   is at the right, so each panel arrives from under its own key rather than
   both from the same edge with one of them having crossed the window to get
   there. The two changed sides together: moving a panel without its key would
   have it arrive from the far edge of the window, which is the one thing this
   arrangement exists to prevent.
   Everything reverses together: which track the panel sits in, which edge of
   that track it is anchored to (it has to overflow away from the window, or
   it is uncovered rather than sliding), and which way it casts its shadow. */

/* Full width from the first frame, anchored to the left of its track, and it
   overflows to the right until the track has finished opening.
   That is what makes this a panel sliding in rather than a panel being
   uncovered. Stretched to the track it would have grown from nothing, and its
   own edge and corner would have travelled across its contents; anchored to
   the track's right edge it would have sat still while a hole widened over it.
   Anchored left at a fixed width, the whole thing — frame, contents and all —
   translates leftward as one piece, already laid out, already complete. */
.takes-panel {
  position: relative;
  width: var(--takes-w);
  justify-self: start;
  margin: var(--takes-inset) 0 0;
  /* Rounder than the window it holds, not tighter. A nested pair wants the
     outer curve to be the larger one — the eye reads the inner shape as sitting
     inside the outer, and a tight outer corner around a soft inner one inverts
     that. It is also simply what a case edge does: the panel is moulded, the
     window in it is cut. */
  border-radius: var(--radius-3xl);
  overflow: hidden;
  transition: visibility 0s var(--motion-slower);
  /* Not the fascia's ramp. These are the machine's side cheeks rather than its
     front face, and carrying the amp's exact gradient made a panel standing
     against it read as more of the same piece rather than as another one. They
     run darker and settle sooner — the fascia's three stops stretched over a
     panel this tall arrive as one flat wash anyway. */
  background-image:
    var(--grain-tex),
    var(--panel-face);
  background-size: 150px 150px, auto;
  background-blend-mode: multiply, normal;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, .05),
    var(--cast-panel-flip);
  /* Held visible until the column has finished closing, or the panel blinks
     away on the first frame and the slide plays against nothing. */
  visibility: hidden;
 grid-column: 3; grid-row: 1;
}
/* The left-hand panel is the same panel, mirrored. It anchors to the far edge
   of its track rather than the near one — a panel has to overflow *away* from
   the window while its track is opening, or what you see is a hole widening
   over something that was already there rather than a panel arriving. Its
   shadow turns round with it. */
.panel--left {
  grid-column: 1;
  justify-self: end;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, .05),
    var(--cast-panel);
}
body[data-takes="open"] .panel--left {
  visibility: visible;
  transition: visibility 0s;
}
/* The column carries the panel plus its gap to the edge of the app, so the
   panel itself stays exactly --takes-w.
   Four states written out rather than composed from two variables: a track
   list only interpolates when it is the same list with different lengths in
   it, and writing all four is the only way to be sure of that whichever pair
   of panels happens to be open. */
body[data-takes="open"] .stage-body {
  grid-template-columns: var(--panel-col) 1fr 0px;
}
body[data-lyrics="open"] .stage-body,
body[data-help="open"] .stage-body {
  grid-template-columns: 0px 1fr var(--panel-col);
}
body[data-lyrics="open"][data-takes="open"] .stage-body,
body[data-help="open"][data-takes="open"] .stage-body {
  grid-template-columns: var(--panel-col) 1fr var(--panel-col);
}

/* The panel already has the width, and it never changes — so the contents are
   laid out once, when the panel is built, and are simply carried in with it. */
/* The panel's own margin, all of it, in one place. The head, the window and the
   mix carried their own side padding and their own idea of the foot, which is
   how the window ended up sitting 14px off the bottom edge while the head sat
   18px in from the sides. Even the whole way round: the panel is a frame, and a
   frame with a heavier foot than sides reads as a mistake unless something is
   standing in it. */
.takes-inner {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: var(--space-2xl);
  overflow: hidden;
}

/* The window in the panel.
   This started as a small pane inside the take's view holding two sentences,
   with the list, the trouble spots and the lyric editor around it on bare
   fascia — which had the machine speaking through a letterbox and printing
   everything else on its own case. It is all one screen now: the head above
   it and the mix below it are the panel, and everything between them is lit
   from behind. Same glass as the roll and the fascia's readout, a third hole
   in the same box. */
.panel-screen {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  /* The panel's own inset is the window's inset — see .takes-inner. What is
     left here is the gap to whatever stands under it, and that belongs to the
     thing standing there rather than to the window. */
  margin: 0;
  /* Between the fascia's readout and the roll. The vignette is a proportion of
     the panel, and this one is a tall narrow pane rather than either. */
  --vignette: 44px;
  /* Lit from below, the way the readout is. A pool in the middle — which is
     what the roll uses — puts the brightest part of the glass across the
     middle of the list, and a list is read from the top. */
  --lamp-at: 50% 118%;
  --lamp-wide: 96%;
  --lamp-reach: 74%;
  --lamp-strength: .78;
  /* Everything printed here is lit unless it says otherwise, so a paragraph
     that inherits its colour inherits the right one. */
  color: var(--dsp-lit-dim);
}

/* ---------- what is printed on the panel's screen ---------- */

/* Two classes deep throughout, and deliberately: every one of these components
   has a base rule further down the file, and at equal specificity the later
   one would win whatever this block said. */

/* The heading and the take's label on one line, at the two ends of it — the
   top of the screen, read left to right: what this list is, and what it is a
   list about. */
.panel-screen .screen-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  margin: var(--space-2xs) 0 var(--space-xl);
}
/* Small print, not a second heading: it names what is on the glass, and the
   size is what keeps that clear. */
.panel-screen .screen-tag { font-size: var(--type-2xs); letter-spacing: var(--tracking-md); }

/* A heading on glass is stencilled, not typeset — the same small print the
   machine puts round the edge of any of its screens, one size up because it
   is naming the list under it rather than annotating something. The standing
   instruction that used to sit above it, and the rule under that, were a
   paragraph and a line where a heading and a list would do. */
.panel-screen .screen-head {
  margin: 0;
  font-family: var(--stencil);
  font-size: var(--type-md);
  font-weight: 400;
  letter-spacing: var(--tracking-md);
  text-transform: uppercase;
  color: var(--dsp-lit-dim);
  text-shadow: 0 0 7px rgba(var(--dsp-lit-rgb), .24);
}

/* How to work it, said once at the top of the panel. Small print: it is an
   instruction you read on arriving and never again, so it sits below the
   words it is about rather than competing with them. */
.panel-screen .screen-hint {
  margin: 0 0 var(--space-2xl);
  line-height: var(--leading-relaxed);
}

/* Nothing here yet, said on the glass rather than on the case. Work Sans, not
   the pixel face: this is a sentence to read, and the stencil is for labels. */
.panel-screen .empty {
  color: rgba(var(--dsp-lit-rgb), .5);
  text-shadow: 0 0 8px rgba(var(--dsp-lit-rgb), .16);
}

/* Haze at the top and foot of a screen.
   A line scrolling out of the window used to be cut by the rim — half a row of
   letters sitting on the edge of the glass, which reads as a rendering fault
   rather than as more text above. It blurs and fades into the rim instead.
   Progressive, in the sense that the blur is masked to strengthen toward the
   edge: one blurred layer whose mask ramps reads the same as a stack of them
   at this height, and costs one element instead of six.
   It has to sit below `.glass > *` in the file — that rule sets every direct
   child of a panel to position:relative and z-index 1, and at equal specificity
   source order is all that keeps this one where it belongs, which is above
   what is printed on the glass and below the frame drawn over everything. */
/* The screen's scrolling area. It exists so the haze has something to be the
   edges of: hung on the screen itself, the haze at the foot landed on the
   takes panel's message line — which does not scroll — and left the bottom of
   the list, the edge actually cutting text, untouched. */
.screen-body {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}


/* The panel's message line, along the foot of the screen. Bigger than the
   small print at 8px because it is the one thing on this panel you are being
   asked to read rather than scan, and a notice set at footnote size is a
   notice written to be skipped.
   Caps and the stencil like everything else lit behind this glass — 11px is
   the size at which that stays comfortable to read a sentence of. */
.panel-screen .screen-note {
  flex: none;
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 11px 18px 13px;
  border-top: 1px solid rgba(var(--dsp-lit-rgb), .13);
}
.panel-screen .screen-note p {
  margin: 0;
  font-size: var(--type-md);
  letter-spacing: var(--tracking-sm);
  line-height: var(--leading-relaxed);
}
.panel-screen .screen-note[hidden] { display: none; }

/* The words, on the glass they are sung against. The editor keeps its ruled
   lines and its gutter of times; what changes is that both are now drawn in
   light rather than in grey. */
.panel-screen .lyric-area { color: var(--dsp-lit); }
/* The times are not lit. Everything amber on this glass is something the
   machine is telling you; the gutter is a ruler beside the words, and in the
   display's own colour it competed with them for exactly the attention it
   should not have. Neutral, and it warms only under the pointer, where it is
   about to become a place to play from. */
.panel-screen .lyric-at-line { color: rgba(232, 226, 214, .62); }
.panel-screen .lyric-at:hover,
.panel-screen .lyric-stamp:hover { color: rgba(246, 242, 234, .95); }
.panel-screen .lyric-at-line--none .lyric-at:hover { color: inherit; }
.panel-screen .mix-block[hidden] { display: none; }
/* Clear of the window above it. The mix is fascia and the screen is a hole cut
   in that fascia — with the keys crowding the rim they read as part of what is
   behind the glass rather than as controls screwed to the panel. */
.mix-block { padding-top: var(--space-2xl); }
.mix-head {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  /* Room for the engraved value, which is drawn above the top of its own knob
     and so reaches up into this gap. */
  margin-bottom: var(--space-4xl);
}
/* Engraved, like every legend out here — this is fascia, not screen. */
.mix-legend {
  font-size: var(--type-lg);
  font-weight: 500;
  letter-spacing: var(--tracking-xl);
  text-transform: uppercase;
  color: var(--ink-faint);
  text-shadow: 0 1px 0 rgba(0, 0, 0, .7);
}
/* The switch at the right end of the row. It had a "sync with mic" key beside
   it and was sized to match — that key has gone to the screen the reading it
   answers is printed on, and the switch keeps the height it was given, since
   38px is what the row wants rather than what the pair needed. */
.mix-keys { display: flex; align-items: center; gap: var(--space-md); margin-left: auto; }
/* The lamp reads with the key it belongs to, not with the switch beyond it. */
.mix-keys .mix-lamp { margin-right: -1px; }
.mix-keys .btn { --btn-size: 38px; height: var(--btn-size); }
.mix-keys .btn--icon { width: var(--btn-size); }
.mix-keys .btn--icon .btn-face { padding: 0; height: calc(var(--btn-size) - 9px); }
/* Three across, sharing the width evenly. The base .knob puts its dial in a
   130px gutter to the left of a block of text; there is no text here and no
   gutter to spare, so that is undone. */
.mix-knobs { display: flex; align-items: flex-start; gap: var(--space-md); }
/* Two classes deep, and it has to be. The base .knob sits 20,000 characters
   further down the file and puts its dial in a 130px gutter to the left of a
   block of text — at equal specificity that padding wins on source order
   alone, which pushed every module a gutter's width to the right, overflowed
   the row, cut the third knob off and squeezed the legends into two lines. */
.knob.knob--mix {
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  padding-left: 0;
  /* A grid, so the legend can sit in the cap's column alone. Nested in a row
     with the readout, it centred on the pair — which puts it off to the left
     of the thing it names. */
  display: grid;
  grid-template-columns: var(--dial) auto;
  justify-content: center;
  align-items: center;
  column-gap: var(--space-md);
  row-gap: var(--space-lg);
  padding-bottom: var(--space-xs);
  --dial: 76px;
}
.knob--mix .dial-wrap { grid-column: 1; grid-row: 1; }
.knob--mix .led--mini { grid-column: 2; grid-row: 1; }
.knob--mix .knob-label { grid-column: 1; grid-row: 2; }
.knob--mix .dial-wrap {
  position: relative;
  top: auto;
  left: auto;
  transform: none;
}
.knob--mix .knob-label {
  font-size: var(--type-xs);
  letter-spacing: var(--tracking-lg);
  line-height: var(--leading-tight);
  text-align: center;
  /* One line. These are two short words each and they read as a name; broken
     over two they read as a sentence that ran out of room. */
  white-space: nowrap;
}
.knob--mix .led--mini { min-width: 44px; padding: var(--space-xs) var(--space-sm); font-size: var(--type-md); }
.knob--mix .led--mini .led-on { top: 4px; right: 6px; }
/* One engraved value, not three. On a 76px cap the pair at the foot of the
   sweep are two more things to read on a knob you glance at, and the legend
   under it already says which knob this is — the top figure is the only one
   that tells you anything at a glance. Stood off the ring, so it is not
   reading as part of the scale. */
.knob--mix .dial-min,
.knob--mix .dial-max { display: none; }
.knob--mix .dial-mid { top: -13px; }


.knob[data-dark] .led-on {
  color: var(--led-off);
  text-shadow: none;
}

.knob[data-lamp="off"] .dial-mark::after {
  background: var(--lens-face);
  box-shadow: var(--lens-edge);
}
.knob[data-lamp="locked"] .dial-mark::after {
  --lamp-rgb: 207, 47, 24;
  background: var(--lens-catch), var(--hot);
  box-shadow:
    inset 0 -1px 0 rgba(255, 255, 255, .2),
    0 0 7px rgba(207, 47, 24, .6),
    0 0 14px rgba(207, 47, 24, .28);
}

/* Which stem is loaded, beside the MIX legend. Mark and lamp, the same pair
   the fascia uses for its stem keys — read-only here, because this is the
   consequence of a choice made out there rather than the choice itself. */
.mix-stems { display: flex; align-items: center; gap: var(--space-xl); margin-left: var(--space-xl); }
.mix-stem { display: flex; align-items: center; gap: 7px; }
/* The mark stays put. It names which stem this row is about and that does not
   change — the lamp beside it is what says whether it is the one loaded. A
   mark that lights as well is the same fact told twice, and the second telling
   costs the first its meaning. */
.mix-stem sl-icon { font-size: var(--icon-sm); color: var(--stem-ink); }
/* The same lamp the keys carry: a 7px round recess that lights. Built here
   rather than borrowed, because that one is a pseudo-element of a button face
   and this is not a button — but the geometry and both states are its. */
.mix-lamp {
  --lamp: var(--lit);
  --lamp-rgb: var(--lit-rgb);
  /* Never the thing that gives way. A flex item's automatic minimum size is
     the smaller of its specified width and its content width — and this has no
     content, so that minimum is nought and the lamp is the first thing a tight
     row squeezes. Squeezed on one axis only, a circle becomes an ellipse. */
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--lens-face);
  box-shadow: var(--lens-edge);
  transition: background var(--motion-fast) var(--ease), box-shadow var(--motion-fast) var(--ease);
}
/* What a lamp is lit in. Amber is the machine working; red is something owing;
   green is a thing that has just gone right — and green never stands, because
   nothing on this fascia is permanently pleased with itself. */
.mix-lamp[data-tone="late"] { --lamp: var(--hot); --lamp-rgb: 207, 47, 24; }
.mix-lamp[data-tone="done"] { --lamp: var(--ok); --lamp-rgb: var(--ok-rgb); }

.mix-stem[data-on] .mix-lamp,
.mix-lamp[data-on] {
  background: var(--lens-catch), var(--lamp);
  box-shadow:
    inset 0 -1px 0 rgba(255, 255, 255, .2),
    0 0 7px rgba(var(--lamp-rgb), .55),
    0 0 14px rgba(var(--lamp-rgb), .25);
}

.ledge-voice { display: flex; align-items: center; gap: 5px; }
.ledge-voice .btn--radio { --lamp: var(--tune); --lamp-rgb: var(--lit-rgb); }

/* The way back to the list, ahead of the title it replaced. */
.panel-back { flex: none; }
.panel-back[hidden] { display: none; }

/* Deleting the open take, at the far end of the head from the way back into
   it. Amber like every other key on this fascia at rest: a key that is red
   before you have touched it is shouting, and this one is one confirmation
   away from doing anything. It goes red under the pointer. */
.panel-kill { flex: none; margin-left: auto; }
.panel-kill[hidden] { display: none; }
.panel-kill .btn-face { transition: color var(--motion-fast) var(--ease); }
.panel-kill:hover .btn-face { color: var(--hot); }

/* The panel changing what it is showing, above and below the window as well as
   in it. The screen's own contents arrive by lighting — see dsp-in — and these
   do the same with a little travel under it, because a legend on fascia that
   changes has to change like a mechanism rather than simply be a different
   word on the next frame.
   In stages, in the order the panel would have them: the title says where you
   have arrived, the keys that act on it follow, and the mix comes up last
   under the take it belongs to. */
@keyframes panel-swap {
  0%   { opacity: 0; transform: translateY(7px); }
  62%  { opacity: 1; }
  100% { opacity: 1; transform: none; }
}
.takes-inner[data-swap] .panel-head h2 {
  animation: panel-swap 320ms var(--ease) both;
}
.takes-inner[data-swap] .panel-back,
.takes-inner[data-swap] .panel-kill,
.takes-inner[data-swap] .head-keys {
  animation: panel-swap 290ms 150ms var(--ease) both;
}
.takes-inner[data-swap] .mix-block {
  animation: panel-swap 360ms 250ms var(--ease) both;
}

/* How to work it, on the glass. Numbered because the order is the part that
   is hard to find out — the steps are each obvious once seen and useless in
   the wrong sequence. The numerals are lit and the prose is dim, so the shape
   of the list reads before any of it is read. */
.panel-view--help { padding: var(--space-xs) var(--space-2xs) var(--space-lg); }
/* Stencil for the heading, the reading face for the prose. The machine
   stencils its own legends; this is several paragraphs to be read, and the
   lyrics beside it are set in the reading face for the same reason. Set in
   Silkscreen it was legible and nobody would have read it. */
.panel-view--help p {
  font-family: var(--font);
  font-size: var(--type-lg);
  letter-spacing: var(--tracking-none);
  line-height: var(--leading-normal);
  /* Sentence case, like the lyrics on this same glass. Uppercase is how the
     machine stamps a legend, and a legend is three words; five paragraphs of
     it is a thing you decide not to read. */
  text-transform: none;
}
.panel-view--help p { margin: 0 0 var(--space-xl); }
.panel-view--help .dsp-confirm-title { display: block; margin-bottom: var(--space-lg); }
/* The lead of each step, stamped rather than written: this is the machine
   labelling a part of itself, which is what the stencil is for, and it is the
   one thing on the panel that should be findable without reading. Numbered
   steps said the order mattered more than it does — the last two are a loop,
   not a fifth and sixth thing to do. */
.help-lead {
  margin: 18px 0 6px;
  font-family: var(--stencil);
  font-size: var(--type-sm);
  letter-spacing: var(--tracking-lg);
  text-transform: uppercase;
  font-weight: 400;
  color: var(--lit);
}
.panel-view--help > p:first-child { margin-bottom: var(--space-2xs); }

/* The foot of a panel: where the work ends.
   Saving was at the top, above the words it keeps, which is where the reading
   starts rather than where it finishes. The lamp sits eight pixels from the
   key — near enough to be read as that key's light rather than as another
   indicator on the panel, which is what the gap across the head made it. */
.panel-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-md);
  padding: var(--space-lg) var(--space-2xs) 0;
  flex: 0 0 auto;
}
/* The key is as wide as its legend, not as wide as the panel it sits on. A
   key filling its own row does not read as a key, it reads as a bar.
   On .btn rather than on the flex children, which was the first attempt and
   did nothing: a tooltip is display:contents, so the button is the flex item
   and the element the selector reached was not. The width comes from .btn's
   own `width: 100%`, and the ledge already has this same exception. */
.panel-foot .btn { width: auto; }
.panel-foot .btn { --btn-size: 34px; height: var(--btn-size); }
.panel-foot .btn-face { height: calc(var(--btn-size) - 9px); }

/* Saving, as the lamp doing something rather than a second thing appearing.
   Nothing moves; the light does. The first version faded the element's
   opacity, which takes the lens down with the light — the glass, its edge and
   its catch are casework and are no more allowed to fade than a key is
   allowed to slide. So the geometry, the rim and the highlight are held
   exactly as the lit lamp has them, and what travels is the colour under the
   glass and the glow it throws.
   Dipping and returning rather than going out: a lamp that switches off reads
   as a fault, and this is the machine working. Slow enough to be a heartbeat
   and not an alarm. */
.mix-lamp[data-saving] {
  background: var(--lens-catch), var(--lamp);
  box-shadow:
    inset 0 -1px 0 rgba(255, 255, 255, .2),
    0 0 7px rgba(var(--lamp-rgb), .55),
    0 0 14px rgba(var(--lamp-rgb), .25);
  animation: lamp-save 0.9s ease-in-out infinite;
}
@keyframes lamp-save {
  0%, 100% {
    background: var(--lens-catch), rgba(var(--lamp-rgb), 1);
    box-shadow:
      inset 0 -1px 0 rgba(255, 255, 255, .2),
      0 0 7px rgba(var(--lamp-rgb), .55),
      0 0 14px rgba(var(--lamp-rgb), .25);
  }
  50% {
    background: var(--lens-catch), rgba(var(--lamp-rgb), .22);
    box-shadow:
      inset 0 -1px 0 rgba(255, 255, 255, .2),
      0 0 7px rgba(var(--lamp-rgb), .10),
      0 0 14px rgba(var(--lamp-rgb), .04);
  }
}

.panel-head {
  /* Two more than the window below it: the lettering is engraved on the panel
     and the window is a hole cut in it, so they do not share an edge. */
  padding: 0 var(--space-2xs);
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  margin-bottom: 14px;
  /* One key tall, whether or not there are any keys in it. Its height came
     from whatever happened to be inside — 34px with the take's keys up, half
     that on the list — so the screen below, which fills what is left, changed
     height and moved every time you opened or left a take. A head that is a
     fixed piece of casework is what makes the window under it stay still. */
  min-height: 34px;
}
/* The editing keys ride the right end of the head; Save stays beside the title
   because it is about the state of what you are looking at, not an action you
   go hunting for. */
/* An indicator ahead of the keys, then the keys. A little more air in front of
   the lamp than between the keys, so it reads as belonging to the row rather
   than as a very small key at the start of it. */
.head-keys { display: flex; align-items: center; gap: var(--space-md); margin-left: auto; }
.head-keys .mix-lamp { margin-right: var(--space-xs); }
.head-keys[hidden] { display: none; }
/* Engraved on the fascia, like every other legend out here — the same caps,
   tracking and cut shadow as a knob label, one size up because it names the
   whole panel rather than one control on it. */
.panel-head h2 {
  margin: 0;
  font-family: var(--font);
  font-weight: 500;
  font-size: var(--type-lg);
  letter-spacing: var(--tracking-xl);
  text-transform: uppercase;
  color: var(--ink-dim);
  text-shadow: 0 1px 0 rgba(0, 0, 0, .7);
}

/* The row is explicit on all three, and it has to be. With columns named but
   rows left to auto-placement, a panel in column 1 that comes *after* the
   window in the markup is behind the placement cursor, so the grid gives it a
   row of its own — which is how the lyric editor ended up under the window
   instead of beside it. */
/* The roll is the big display. Everything about the material comes from
   .glass — it is the same smoked panel as the readout on the fascia, in a
   different hole in the same box. What is left here is the hole. */
.roll-wrap { grid-column: 2; grid-row: 1;

  min-height: 0;
  min-width: 0;
  /* One window cut into one continuous piece of fascia: an even inset on the
     three sides that meet the edge of the app, and flush at the foot where the
     key strip carries on as panel rather than as another edge. */
  margin: var(--space-2xl) var(--space-2xl) 0;
}
/* Everything here follows from this panel being ten times the height of the
   other one.
   The lamp is pulled in and taken down, or a bloom sized for a 172px readout
   becomes a sunrise across the roll — and the part of it that landed on the
   bottom edge was lifting exactly the pixels that have to stay dark.
   The glass goes darker with it. At the fascia's value its foot arrived at
   very nearly the colour of the panel beneath, so the bottom rim read as a
   stray dark line across the window rather than as the edge of it — the seam
   was the two surfaces meeting at the same brightness, not the rim itself.
   The vignette is opened out because it is a proportion of the glass rather
   than a fixed inset. */
.roll-wrap.glass {
  /* A pool in the middle rather than a strip along the foot. Wide and soft:
     the whole point is that it never resolves into an edge you could point at,
     it just makes the centre of the panel warmer than its corners. */
  --lamp-at: 50% 48%;
  --lamp-wide: 62%;
  --lamp-reach: 78%;
  --lamp-strength: .62;
  --glass-top: #060505;
  --glass-mid: #070606;
  --glass-foot: #0a0908;
  --vignette: 120px;
}
/* Pinned to the width of an unpushed window, not to the frame around it.
   The frame narrows as the panel comes in; if the canvas narrowed with it, it
   would be reallocated and repainted at a new size on every frame of the
   slide — the notes squeezing, the playhead walking left, the whole picture
   rebuilding while you watch. Held at full width it is simply covered, which
   is what being pushed off the edge of a window looks like.
   The window has no rail or gutter beside it, so its unpushed width is the
   viewport less its own two margins. */
#roll {
  display: block;
  width: calc(100vw - var(--takes-inset) * 2);
  height: 100%;
}

/* Half the panel's column, in whichever direction the panel is not.
   The canvas and the stack below it keep the width of the whole window and are
   simply covered when the panel opens — that is what stops the trace redrawing
   and the lyric rewrapping on every frame of the slide. But covered is not
   centred: what was in the middle of the window ends up in the middle of the
   part the panel is now standing on. Shifting them by half of what was taken
   puts the middle of the drawing back in the middle of what you can see, and
   as a transform it costs no layout — the geometry never changes, only where
   it is painted. */
/* Always leftward, and by half of everything that is covered — the canvas and
   the stack are laid out from the left edge of the window and overflow to the
   right of it, so what is hidden is always the right-hand end however many
   panels are open and whichever sides they are on. */
body { --stage-shift: 0px; }
body[data-lyrics="open"],
body[data-takes="open"] { --stage-shift: calc(var(--panel-col) / -2); }
body[data-lyrics="open"][data-takes="open"] {
  --stage-shift: calc(var(--panel-col) * -1);
}
#roll,
.stage-foot {
  transform: translateX(var(--stage-shift, 0px));
  /* The panel's own duration, so the two move as one thing. */
  transition: transform var(--motion-slower) var(--ease);
}


/* The readout overlaps the canvas rather than sitting beside it — the number
   belongs in your eyeline, not in a sidebar. */
/* ---- the foot ----
   One stack: what to aim at, what you are making, the verdict, the words. They
   used to be gathered from three corners of the panel, which meant reading the
   note at the top, the meter on the right and the lyric at the bottom while
   singing. Insets rather than a centring transform, so it can never run into
   the corner blocks however narrow the window gets. */
.stage-foot {
  position: absolute;
  left: 26px;
  /* Anchored left at a fixed width rather than stretched between two edges —
     the same reasoning as the canvas above it. Stretched, the readout
     recentres and the lyric rewraps on every frame while the frame narrows;
     pinned, the stack simply gets covered. */
  width: calc(100vw - var(--takes-inset) * 2 - 52px);
  bottom: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Row, verdict line, words: 23px from the readings to the lyric, measured
     in the page (was 39 — the singer asked for the two brought together).
     The verdict keeps its 16px slot so the words never jump. */
  gap: 2px;
  pointer-events: none;
}

/* Printed on the glass, not a card floating over it. The border and the
   frosted backing were a UI panel — the one thing here that was a window
   rather than something lit behind it. */
.readout {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  transition: filter var(--motion-slower) var(--ease);
}
/* On pitch, the whole reading blooms — the glow belongs to the figures, which
   are lit, rather than to a rectangle around them, which is not. */
.readout.on { filter: drop-shadow(0 0 16px rgba(var(--lit-rgb), .32)); }

/* The one line that answers the question you are actually asking. It used to
   be a number and a sentence in separate readouts, which left you assembling
   the answer yourself mid-phrase. Stencilled, because it is the machine's
   judgement rather than something you read for meaning. */
.verdict {
  margin: 0;
  /* Held open. This line is empty until there is something to judge, and the
     stack under it must not step as it fills and clears. */
  min-height: 1em;
  font-family: var(--stencil);
  font-size: var(--type-xl);
  letter-spacing: var(--tracking-lg);
  text-transform: uppercase;
  color: var(--dsp-lit-dim, #a08a52);
  text-shadow: 0 0 8px rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .22);
  -webkit-font-smoothing: none;
  transition: color var(--motion-base) var(--ease), text-shadow var(--motion-base) var(--ease);
}
.readout.on .verdict {
  color: var(--tune);
  text-shadow: 0 0 10px rgba(var(--lit-rgb), .55), 0 0 24px rgba(var(--lit-rgb), .25);
}
.readout.high .verdict,
.readout.low .verdict {
  color: var(--lit-dim);
  text-shadow: 0 0 12px rgba(var(--lit-rgb), .2);
}

/* Karaoke line, parked at the foot of the roll so it never crosses the pitch
   you are trying to read. */
.lyrics {
  width: 100%;
  text-align: center;
}
.lyrics[hidden] { display: none; }

/* A line you have to read while singing it, so it is set for reading. This is
   the one place on the glass where clarity beats the material: a lyric missed
   because the face was interesting is a lyric missed. */
.lyric-now {
  margin: 0;
  font-family: var(--font);
  font-weight: 500;
  font-size: var(--type-2xl);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--lyric-now);
  text-wrap: balance;
}
/* Quick, because this is a cue. A slow ramp means the word only looks lit
   once the fade has finished, which puts the signal after the moment it is
   signalling — the lead time in front of it is then spent on the fade. */
.lyric-now .word { transition: color var(--motion-flick) linear, text-shadow var(--motion-flick) linear; }
/* Sung words light up as the playhead passes them — lit rather than merely
   whiter, since everything else on this glass that means "now" is lit. */
.lyric-now .word.sung {
  color: var(--dsp-lit, var(--lit));
  text-shadow:
    0 0 8px rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .45),
    0 0 18px rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .2);
}

.lyric-next {
  margin: 9px 0 0;
  font-family: var(--font);
  font-size: var(--type-lg);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-none);
  color: var(--lyric-next);
  text-wrap: balance;
}

/* The lamp with nothing in front of it: the panel is on, and the only thing
   on it is the message. Not quite opaque, so the glass behind still reads as
   glass rather than as a grey sheet dropped over the roll. */
.curtain {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 40px;
  background: rgba(5, 4, 4, .94);
  text-align: center;
  transition: opacity var(--motion-slowest) var(--ease), visibility var(--motion-slowest);
}
.curtain[hidden] { opacity: 0; visibility: hidden; display: grid; }
/* The pigeon, pixelated: build/icon/icon.svg rasterised onto the display's
   grid and lit from behind the glass like everything else on it. Box-shadow
   pixels, not an <img> or an icon font — a smooth vector curve is the one
   thing that could not be made of lit dots. The offsets carry no colour, so
   each one is currentColor: one place to theme it, and short entries rather
   than 271 long ones. 6px cells, not 3: at half this size the wing's dotted
   streaks read as noise rather than feathers. Regenerate from the icon if
   the mark ever changes. */
.curtain-bird {
  position: relative;
  display: block;
  width: 156px;
  height: 150px;
  margin: 0 auto var(--space-xl);
  color: var(--dsp-lit-dim);
}
.curtain-bird::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 6px;
  height: 6px;
  box-shadow:
    6px 0px 0, 6px 6px 0, 12px 6px 0, 0px 12px 0, 12px 12px 0, 18px 12px 0,
    6px 18px 0, 18px 18px 0, 24px 18px 0, 30px 18px 0, 120px 18px 0,
    126px 18px 0, 0px 24px 0, 12px 24px 0, 18px 24px 0, 30px 24px 0,
    36px 24px 0, 42px 24px 0, 108px 24px 0, 114px 24px 0, 120px 24px 0,
    126px 24px 0, 132px 24px 0, 6px 30px 0, 18px 30px 0, 24px 30px 0,
    36px 30px 0, 42px 30px 0, 48px 30px 0, 54px 30px 0, 60px 30px 0,
    102px 30px 0, 108px 30px 0, 114px 30px 0, 120px 30px 0, 132px 30px 0,
    138px 30px 0, 6px 36px 0, 12px 36px 0, 24px 36px 0, 30px 36px 0,
    36px 36px 0, 48px 36px 0, 54px 36px 0, 60px 36px 0, 66px 36px 0,
    72px 36px 0, 102px 36px 0, 108px 36px 0, 114px 36px 0, 120px 36px 0,
    126px 36px 0, 132px 36px 0, 138px 36px 0, 144px 36px 0, 150px 36px 0,
    6px 42px 0, 12px 42px 0, 18px 42px 0, 24px 42px 0, 36px 42px 0,
    42px 42px 0, 48px 42px 0, 60px 42px 0, 66px 42px 0, 72px 42px 0,
    78px 42px 0, 84px 42px 0, 102px 42px 0, 108px 42px 0, 114px 42px 0,
    120px 42px 0, 126px 42px 0, 132px 42px 0, 138px 42px 0, 6px 48px 0,
    12px 48px 0, 24px 48px 0, 30px 48px 0, 48px 48px 0, 54px 48px 0,
    60px 48px 0, 78px 48px 0, 84px 48px 0, 90px 48px 0, 102px 48px 0,
    108px 48px 0, 114px 48px 0, 120px 48px 0, 126px 48px 0, 132px 48px 0,
    12px 54px 0, 18px 54px 0, 24px 54px 0, 30px 54px 0, 36px 54px 0,
    42px 54px 0, 48px 54px 0, 60px 54px 0, 66px 54px 0, 72px 54px 0,
    78px 54px 0, 84px 54px 0, 102px 54px 0, 108px 54px 0, 114px 54px 0,
    120px 54px 0, 126px 54px 0, 132px 54px 0, 12px 60px 0, 24px 60px 0,
    30px 60px 0, 36px 60px 0, 48px 60px 0, 54px 60px 0, 60px 60px 0,
    66px 60px 0, 78px 60px 0, 84px 60px 0, 90px 60px 0, 102px 60px 0,
    108px 60px 0, 114px 60px 0, 120px 60px 0, 126px 60px 0, 18px 66px 0,
    24px 66px 0, 36px 66px 0, 42px 66px 0, 48px 66px 0, 60px 66px 0,
    66px 66px 0, 72px 66px 0, 78px 66px 0, 84px 66px 0, 90px 66px 0,
    102px 66px 0, 108px 66px 0, 114px 66px 0, 120px 66px 0, 126px 66px 0,
    18px 72px 0, 24px 72px 0, 30px 72px 0, 36px 72px 0, 48px 72px 0,
    54px 72px 0, 60px 72px 0, 66px 72px 0, 78px 72px 0, 84px 72px 0,
    90px 72px 0, 102px 72px 0, 108px 72px 0, 114px 72px 0, 120px 72px 0,
    126px 72px 0, 30px 78px 0, 36px 78px 0, 42px 78px 0, 48px 78px 0,
    60px 78px 0, 66px 78px 0, 72px 78px 0, 78px 78px 0, 84px 78px 0,
    90px 78px 0, 102px 78px 0, 108px 78px 0, 114px 78px 0, 120px 78px 0,
    126px 78px 0, 36px 84px 0, 42px 84px 0, 48px 84px 0, 54px 84px 0,
    60px 84px 0, 66px 84px 0, 102px 84px 0, 108px 84px 0, 114px 84px 0,
    120px 84px 0, 126px 84px 0, 54px 90px 0, 60px 90px 0, 66px 90px 0,
    102px 90px 0, 108px 90px 0, 114px 90px 0, 120px 90px 0, 126px 90px 0,
    54px 96px 0, 60px 96px 0, 66px 96px 0, 102px 96px 0, 108px 96px 0,
    114px 96px 0, 120px 96px 0, 48px 102px 0, 54px 102px 0, 60px 102px 0,
    66px 102px 0, 96px 102px 0, 102px 102px 0, 108px 102px 0, 114px 102px 0,
    120px 102px 0, 42px 108px 0, 48px 108px 0, 54px 108px 0, 60px 108px 0,
    66px 108px 0, 72px 108px 0, 78px 108px 0, 84px 108px 0, 90px 108px 0,
    96px 108px 0, 102px 108px 0, 108px 108px 0, 114px 108px 0, 30px 114px 0,
    36px 114px 0, 42px 114px 0, 48px 114px 0, 54px 114px 0, 60px 114px 0,
    66px 114px 0, 72px 114px 0, 78px 114px 0, 84px 114px 0, 90px 114px 0,
    96px 114px 0, 102px 114px 0, 108px 114px 0, 18px 120px 0, 24px 120px 0,
    30px 120px 0, 36px 120px 0, 42px 120px 0, 48px 120px 0, 54px 120px 0,
    60px 120px 0, 66px 120px 0, 72px 120px 0, 78px 120px 0, 84px 120px 0,
    90px 120px 0, 96px 120px 0, 6px 126px 0, 12px 126px 0, 18px 126px 0,
    24px 126px 0, 30px 126px 0, 36px 126px 0, 42px 126px 0, 48px 126px 0,
    6px 132px 0, 12px 132px 0, 18px 132px 0, 24px 132px 0, 30px 132px 0,
    36px 132px 0, 42px 132px 0, 12px 138px 0, 18px 138px 0, 24px 138px 0,
    30px 138px 0, 36px 138px 0, 18px 144px 0, 24px 144px 0, 30px 144px 0;
}
.curtain p {
  /* Stencilled. This is the machine talking — one short instruction at a time,
     in the face the rest of its legends are cut in. The lyric stays in Work
     Sans, because that is words you follow while singing rather than a message
     you read once.
     Plain ch on the width, no percentage: .curtain-say is a centred grid item,
     so its width is shrink-to-fit, and a percentage max-width inside one
     resolves against a containing block whose width is not known yet — which
     Chrome settles by wrapping at about ten characters. */
  max-width: 44ch;
  margin: 0;
  font-family: var(--stencil);
  font-size: var(--type-xl);
  font-weight: 400;
  line-height: var(--leading-loose);
  letter-spacing: var(--tracking-md);
  color: var(--dsp-lit-dim, #a08a52);
  text-shadow: 0 0 9px rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .22);
}

/* The roll's own contents arrive with the sweep, the way the fascia's rows do:
   the readout first, the words a beat behind it. Without this the wash passes
   over a panel that was already showing everything, which is a lighting effect
   rather than a redraw. */
.roll-wrap[data-wash] .readout { animation: dsp-in 460ms var(--in-trace, 90ms) both; }
.roll-wrap[data-wash] .lyrics  { animation: dsp-in 440ms var(--in-rows, 270ms) both; }
@media (prefers-reduced-motion: reduce) {
  .roll-wrap[data-wash] .readout,
  .roll-wrap[data-wash] .lyrics { animation: none; }
}

/* The block the message sits in, and the thing the wash is sized to: a sweep
   across the whole roll would be weather, not a panel redrawing. */
.curtain-say {
  position: relative;
  padding: var(--space-3xl) var(--space-4xl);
}
.curtain[data-wash] p { animation: dsp-in 460ms var(--in-trace, 90ms) both; }
@media (prefers-reduced-motion: reduce) {
  .curtain[data-wash] p { animation: none; }
}

/* ---------- lit readouts and form controls ---------- */

/* A recessed LED window. The unlit "88:88" sits behind the live digits, which
   is what makes a real seven-segment display legible as one — without it the
   digits just look like text in an odd font. */
.led {
  position: relative;
  display: inline-block;
  padding: 6px 11px;
  border-radius: var(--radius-md);
  /* Flat black and sunk, not the big panels' construction shrunk down. Those
     carry a lit rim because they are large enough to read as a sheet of glass
     set into a frame; at window size the same rings only crowd the digits, and
     the thing this needs to look like is a hole with a lamp behind it. */
  background: var(--led-ground);
  box-shadow:
    inset 0 2px 4px rgba(0, 0, 0, .85),
    inset 0 0 0 1px rgba(0, 0, 0, .6),
    0 1px 0 rgba(255, 255, 255, .04);
  font-family: var(--face-seg);
  font-size: var(--type-lg);
  font-weight: 700;
  line-height: var(--leading-flush);
  letter-spacing: var(--tracking-sm);
}
.led-off { color: rgba(var(--lit-rgb), .085); }
/* A channel with nothing coming through it goes dark, it does not go faint.
   Nothing here is dimmed and nothing is disabled: the knob turns exactly as it
   did, because it can — what has stopped is the reading, and a window with its
   segments out is how an instrument says so. The fade is on the segments only,
   and it follows the -- that app.js writes first, so the reading drops before
   the light does rather than both going at once. */
/* Right-aligned over the ghost, the way a real display fills from the units
   digit up. Left-aligned, a short value reads as part of the unlit segments. */
.led-on {
  transition: color var(--motion-slower) var(--ease), text-shadow var(--motion-slower) var(--ease);

  position: absolute;
  top: 6px;
  right: 11px;
  text-align: right;
  color: var(--tune);
  text-shadow: 0 0 7px rgba(var(--lit-rgb), .5), 0 0 14px rgba(var(--lit-rgb), .22);
}

/* Nothing here yet — the takes list and the lyric editor both say so in the
   same voice. */
.empty {
  margin: var(--space-lg) var(--space-xs);
  font-size: var(--type-md);
  color: var(--glass-ink-faint);
  line-height: var(--leading-normal);
}

sl-range {
  --track-color-active: var(--tune);
  --track-color-inactive: var(--line);
  --track-height: 3px;
  --thumb-size: 13px;
  width: 100%;
}
sl-range::part(form-control-label),
sl-select::part(form-control-label) {
  font-size: var(--type-xs);
  letter-spacing: var(--tracking-lg);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--space-xs);
}
sl-range::part(form-control-help-text) {
  font-size: var(--type-sm);
  color: var(--ink-dim);
  margin-top: var(--space-2xs);
  font-variant-numeric: tabular-nums;
}


/* Nothing below here matches anything. There has been no <sl-select> in this
   app or on the site for some time, and these rules outlived it.

   Kept, for one reason: they are the only worked example of styling a
   Shoelace component from outside its shadow root, ::part by ::part, and the
   next kit component to arrive will need exactly that. Read them as a recipe,
   not as a description of the running app.

   One thing in them is no longer true. They were written against a 27-colour
   ramp bound on .sl-theme-dark, which remapped the kit's neutral and primary
   scales to the fascia's warm palette — the rebinds on sl-option below exist
   to undo that for the open list. That ramp is gone (see ui/tokens.css), so
   an sl-option added today would take the kit's stock colours, and these
   rebinds would be correcting something nobody did. */
sl-select::part(listbox) {
  /* Stand off the trigger so the panel and the button read as separate
     objects rather than bleeding into one another. */
  margin-block: var(--space-md);
  padding: var(--space-xs);
  border: 1px solid var(--line);
  border-radius: var(--radius-xl);
  background: var(--panel);
  box-shadow: var(--cast-float);
}

sl-option::part(base) {
  padding: 7px 9px;
  border-radius: var(--radius-md);
  font-size: var(--type-md);
  line-height: var(--leading-snug);
  white-space: nowrap;
  transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
sl-option::part(label) { white-space: nowrap; }
sl-option::part(checked-icon) {
  width: 13px;
  font-size: var(--icon-sm);
  color: var(--tune);
  padding-inline-end: 11px;   /* the tick was crowding the word */
}
sl-option:not([aria-selected="true"])::part(checked-icon) { visibility: hidden; }

/* The keyboard/hover "current" row is an internal class, not a reflected
   attribute, so it cannot be selected from out here. Rebinding the token the
   internal rule reads is the way in, and that technique is the part worth
   keeping; the specific values are the correction described above. */
sl-option {
  --sl-color-primary-600: rgba(245, 181, 42, .18);  /* current row background */
  --sl-color-neutral-0: var(--ink);                  /* current row text */
  --sl-color-neutral-700: var(--ink-dim);            /* every other row */
}

sl-option::part(base):hover { color: var(--ink); background: var(--hover-veil); }
sl-option[aria-selected="true"]::part(base) {
  background: rgba(var(--lit-rgb), 0.08);
  color: var(--ink);
}

/* ---------- recording & review ---------- */

/* The two badges that used to sit in this corner are gone: what they said —
   recording, and which take is playing back — is on the fascia's status line
   now, where every other piece of machine state already lived. */

/* Takes and trouble spots are lists on a lit panel, and this machine already
   has a way of drawing one of those: the song list on the fascia. Same pixel
   face, same rows with a title on the left and an aside on the right, same
   pixel arrow in the margin to mark the one in hand.
   What went, and why: the borders, the rounded corners, the fill and the
   coloured left edge. All of it was a card — a shape laid over the display —
   and four of them stacked up read as a form rather than as a readout. A lit
   panel says "this row" by lighting it, not by drawing a box round it. */
.takes-list,
.trouble { display: flex; flex-direction: column; }

.take,
.spot {
  position: relative;
  width: 100%;
  /* The song list's gutter, so the arrow lands in the same place here as it
     does on the fascia. */
  padding: 9px 10px 9px 22px;
  border: 0;
  background: none;
  font-family: var(--stencil);
  /* The fascia's list size. These rows follow the song list in every other
     respect and were a size under it, which had the panel reading as the small
     print of a display rather than as one. */
  font-size: var(--type-lg);
  font-weight: 400;
  letter-spacing: var(--tracking-sm);
  text-transform: uppercase;
  text-align: left;
  color: var(--dsp-lit-dim);
  cursor: pointer;
  transition: color var(--motion-fast) var(--ease);
}
/* Reached with the arrows is the same as reached with the cursor, so the row
   lights the same way. The outline goes: on a printed row a browser's focus
   ring is a rectangle from another interface, and the pointer in the margin
   already says which one is in hand. */
.take:hover,
.spot:hover,
.take:focus-visible,
.spot:focus-visible { color: var(--dsp-lit); outline: none; }

/* A take carries more than a song does — a score, what it cost you and when —
   so it is two of the list's rows stacked rather than one. */
.take { display: flex; flex-direction: column; gap: 3px; }
.row-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
}
.row-line--dim { font-size: var(--type-xs); opacity: var(--dim-mid); }
.take-score {
  color: var(--dsp-lit);
  text-shadow: 0 0 8px rgba(var(--dsp-lit-rgb), .3);
}
.take[aria-current="true"] {
  color: var(--dsp-lit);
  text-shadow: 0 0 7px rgba(var(--dsp-lit-rgb), .3);
}


/* A spot is one row: when, what happened, by how much. */
.spot { display: flex; align-items: baseline; gap: 11px; }
.spot-time { min-width: 34px; opacity: var(--dim-mid); }
.spot-what { flex: 1; }
.spot-what b { font-weight: 400; color: var(--dsp-lit); }
.spot-amt { opacity: var(--dim-mid); }


/* ---------- knobs ---------- */

/* A fader module off an audio interface: engraved label, lit readout, and a
   travelling cap in a recessed slot. */
/* Dial on the left, its legend and lit readout to the right. A hardware panel
   puts the label under the knob, but ours carry a sentence of explanation
   each, so the column goes beside the dial rather than below it.
   The dial is taken out of the flow rather than made a grid cell: as a row-
   spanning grid item its 116px was shared out between the two rows, which
   pushed the hint half a knob's height away from the legend it explains. */
.knob {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  min-height: 116px;
  padding-left: 130px;
}


/* The knob sits in a dished-out area of the panel, inside a collar, with a
   raised handle across the cap. Four layers, and they only read as hardware
   together:
     .dial-wrap::before  the dish — panel bevelled down toward the knob
     .dial::after        the collar the cap turns inside
     .dial               the cap
     .dial-mark          the handle and its lit bead
   The dish and the collar are lit from the BOTTOM, not the top: in a
   depression the near wall is shadowed and the far wall catches the light.
   Getting that backwards is what made the first attempt read as a button
   sitting on the surface rather than set into it. */


/* Ticks across the sweep. Each is placed by rotating from centre and pushing
   out, so the ring follows the pointer's travel exactly. They sit outside the
   dish, engraved on flat panel. */
.dial-scale i {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 2px;
  height: 2px;
  margin: -1px;
  border-radius: 50%;
  background: var(--dial-tick);
  box-shadow: 0 1px 0 rgba(0, 0, 0, .7);
  transform: rotate(var(--a)) translateY(calc(var(--tick-r) * -1));
}
.dial-scale i:first-child,
.dial-scale i:last-child { background: var(--dial-tick-lit); }

/* Ends at the foot of the sweep, midpoint above the top tick — clear of it,
   because a tick landing between two digits turns 28 into 2.8. */


/* Under your hand, or under the keyboard's. The cap does not change shape —
   it is the same raised cap either way — it simply gathers a little more
   shadow and takes a ring, which is what says "this one is live" without
   implying it has moved. */
.knob:focus-within .dial {
  /* What the comment above promised and no rule delivered. Three selectors had
     been left dangling into .knob-head's braces, so the cap was being given
     display:flex and no ring at all; git says the body was never written —
     58a13e4 introduced the selector and the comment together. The knob has
     therefore never shown focus. It does now. */
  outline: var(--focus-ring);
  outline-offset: var(--focus-outset);
}

/* The bead on the cap is styled with the rest of the lamp states, further
   down. This comment sat above a selector with no body of its own. */

.knob-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
}
.knob-label {
  font-size: var(--type-sm);
  font-weight: 500;
  letter-spacing: var(--tracking-xl);
  text-transform: uppercase;
  color: var(--ink-faint);
  text-shadow: 0 1px 0 rgba(0, 0, 0, .7);
}
/* The slot the cap runs in. */
.knob sl-range::part(base) { padding: var(--space-2xs) 0; }
.knob sl-range {
  --track-height: 7px;
  --track-color-inactive: #0b0908;
  --track-color-active: var(--tune);
  --thumb-size: 19px;
  width: 100%;
}
sl-range::part(input) { cursor: grab; }
sl-range::part(input):active { cursor: grabbing; }

/* Recessed slot, so the cap looks like it travels inside the panel. */
sl-range::part(input)::-webkit-slider-runnable-track {
  border-radius: var(--radius-lg);
  box-shadow:
    inset 0 2px 3px rgba(0, 0, 0, .9),
    inset 0 -1px 0 rgba(255, 255, 255, .05);
}

/* A machined cap: brushed face, bright top lip, dark underside, and a knurl
   line across the middle so you can see it move. */
sl-range::part(input)::-webkit-slider-thumb {
  background:
    linear-gradient(180deg, rgba(255,255,255,.16) 0 42%, rgba(0,0,0,.28) 42% 46%, rgba(255,255,255,.05) 46%),
    var(--track-face);
  border: 1px solid var(--control-edge);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .3),
    inset 0 -2px 3px rgba(0, 0, 0, .6),
    0 2px 5px rgba(0, 0, 0, .4);
}

/* A small lit window for the value. */
/* One width for every mini readout. Left to size themselves, the ghost digits
   behind them differ — "88" against "-888" — and a column of knobs ends up
   with a ragged right edge. */
.led--mini {
  min-width: 52px;
  padding: 4px 7px;
  font-size: var(--type-md);
  letter-spacing: var(--tracking-sm);
  text-align: right;
}
.led--mini .led-on { top: 4px; right: 7px; }

/* ---------- lyrics editor ---------- */

/* The scrollport is the view rather than .takes-inner, so the head stays put
   and a sticky shelf can pin to the top of the list at top:0. */
/* The scrollport is the view rather than the screen it sits in, so a sticky
   shelf can pin to the top of the list at top:0 — and so the screen's rim and
   vignette stay put while its contents move under them.
   18px at the sides, because the re-transcribe drawer bleeds to the edge of
   the glass with a matching negative margin. */
.panel-view {
  flex: 1;
  min-height: 0;
  /* 12 and 6 rather than 18 and 0, which come to the same 18px of air on the
     right — but the bar renders at this element's edge, so the 6px has to be
     margin for the bar to stand off the glass rather than against it. */
  padding: 15px 12px 18px 18px;
  margin-right: var(--space-sm);
  overflow-y: auto;
}
/* The song list's scrollbar, brought over whole — this screen is the same
   material and had no business drawing a different one.
   Without the negative margin that comes with it on the fascia. There, the
   list bleeds into the display's own padding so the bar ends up near the
   glass; here the scrolling element *is* the padded one, so the same margin
   pushed its right-hand padding off the edge of the screen and the words with
   it. The view is inset by a margin instead: the bar stands off the glass by
   the same 6px it does on the fascia, and the text keeps all 18px.
   No scrollbar-width or scrollbar-color anywhere near this: setting either
   makes Chrome render the standard bar and ignore ::-webkit-scrollbar
   entirely, which is what the panel was doing before. */
.panel-view::-webkit-scrollbar { width: 3px; }
/* The track keeps the view's own top and bottom padding: a bar running the
   full height of the glass reads as part of the frame rather than as something
   belonging to what is scrolling inside it. */
.panel-view::-webkit-scrollbar-track { background: transparent; margin: 15px 0 18px; }
.panel-view::-webkit-scrollbar-thumb {
  /* A stack of lit dots, not a bar — the same 3px pitch the wash uses. */
  background-image: repeating-linear-gradient(180deg,
      rgba(var(--lit-rgb), .3) 0 1px, transparent 1px 3px);
}
.panel-view:hover::-webkit-scrollbar-thumb {
  background-image: repeating-linear-gradient(180deg,
      rgba(var(--lit-rgb), .5) 0 1px, transparent 1px 3px);
}
.panel-view[hidden] { display: none; }
/* The question takes the whole window and is centred in it, so this one view
   is a column rather than a scrollport. Nothing in it scrolls: a question you
   have to scroll to read all of is a question that will be answered unread. */
.panel-view--ask {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.lyric-rows { display: flex; flex-direction: column; gap: var(--space-xs); }

.lyric-row {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: var(--space-xs) 0;
  border-bottom: 1px solid var(--line);
}
.lyric-row.active { background: rgba(var(--lit-rgb), 0.04); }


.lyric-at:hover { color: var(--glass-ink); }

.lyric-text {
  flex: 1;
  min-width: 0;
  padding: 5px 7px;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--glass-ink);
  font: inherit;
  font-size: var(--type-lg);
}
.lyric-text:hover { border-color: var(--line); }
.lyric-text:focus {
  outline: none;
  border-color: rgba(var(--lit-rgb), 0.40);
  background: var(--bg);
}

.lyric-row .kill {
  flex: none;
  width: 22px;
  padding: 3px;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--glass-ink-faint);
  font: inherit;
  font-size: var(--type-lg);
  line-height: var(--leading-flush);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--motion-fast), color var(--motion-fast);
}
.lyric-row:hover .kill { opacity: 1; }
.lyric-row .kill:hover { color: var(--hot); }

#lyricsBtn[hidden] { display: none; }

/* The dialog and the toasts that used to live here are both gone. The machine
   asks on its own display, with the arrows and OK, and it answers there too:
   a notice floating in the top-right corner was a second place to look, over
   the app rather than on it, in the one corner nobody watches while singing. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}


/* ==========================================================================
   Hardware buttons
   --------------------------------------------------------------------------
   Old stereo gear, not flat UI. The key keeps a solid 3px side under its face
   in every state, so its silhouette never shifts: a latched key sitting beside
   a resting one lines up exactly. The press is sold by the *face* instead —
   the top-lit gradient inverts to lit-from-below and the inner highlight
   becomes a shadow cast down into a recess. Nothing moves; the light does.
   ========================================================================== */


/* The Shoelace button styling that stood here is gone with the last sl-button.
   Every key on this machine is a .btn now — one cap, built from the panel's own
   parts — and a second implementation of the same object, reached through
   ::part() on a component from another kit, was the thing that made "fix the
   buttons and use ours" a recurring note rather than a one-off. */

/* ==========================================================================
   Primary button
   --------------------------------------------------------------------------
   The knob, squared off. Same layers in the same order, same light, so a
   button and a knob on one panel read as the same machining:

     .btn                the dish — panel bevelled down toward the button
     .btn::after         the collar: flat, and the darkest element here
     .btn-face           the cap, top dark to bottom bright
     .btn-face::after    a 2px lit edge along the cap's top, fading by alpha
     .btn-face::before   a 1px line just inside it, showing only at the foot

   The hairline that sat at the dish's inner edge is out; git has it.

   The dish is written in alpha rather than the knob's hex, because this sits
   on three different panel browns and has to seat itself on each.
   ========================================================================== */

.btn {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: var(--radius-key);
  /* The base is split across two layers on purpose. Fully opaque, the ring
     stopped reacting to whatever it was screwed onto; fully transparent, the
     panel's grain read straight through the face, and a machined cap does not
     carry the tooth of the fascia. So the ring takes the base at about half
     strength and lets the surface through, and the cap (below) takes it
     solid. One variable still moves both.
     Above it, top dark to bottom light: the near wall of the dish shaded, the
     far wall catching the light. */
  --btn-base: var(--key-face);
  background:
    linear-gradient(180deg,
      rgba(0, 0, 0, .11) 0%,
      rgba(0, 0, 0, .03) 46%,
      rgba(255, 255, 255, .05) 100%),
    linear-gradient(color-mix(in srgb, var(--btn-base) 52%, transparent) 0 0);
  box-shadow:
    inset 0 2px 5px rgba(0, 0, 0, .04),
    inset 0 -2px 4px rgba(255, 255, 255, .04);
  color: var(--key-ink);
  font: inherit;
  font-size: var(--type-sm);
  font-weight: 500;
  letter-spacing: var(--tracking-lg);
  text-transform: uppercase;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  transition: color var(--motion-fast) var(--ease);
}

/* The collar: one flat value the whole way round, and the darkest thing on the
   button. Grading it made it read as a second, brighter ring. */
.btn::after {
  content: "";
  position: absolute;
  inset: 2.5px;
  /* Concentric with the 11px outer corner: outer radius minus the inset. Set
     by eye they drifted apart, and the corners went lumpy where the cap's
     curve crossed the collar's. */
  border-radius: var(--radius-key-ring);
  border: 2px solid rgba(0, 0, 0, .6);
  pointer-events: none;
}

/* The cap. */
.btn-face {
  position: relative;
  /* Above .btn::after. The collar is a pseudo-element of .btn, so it paints
     after this element's box — and the cap's cast lands in exactly the band
     the collar occupies, which swallowed it whole. */
  z-index: var(--layer-content);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  margin: 4.5px;
  padding: 9px 14px;
  border-radius: var(--radius-key-face);
  background:
    linear-gradient(180deg,
      rgba(0, 0, 0, .17) 0%,
      rgba(0, 0, 0, .09) 32%,
      rgba(255, 255, 255, .025) 66%,
      rgba(255, 255, 255, .055) 100%),
    var(--btn-base);
  box-shadow:
    inset 0 -3px 7px rgba(0, 0, 0, .45),
    /* A pool under the middle rather than a band across the foot: the spread
       is well past half the blur, so the sides are pulled in further than the
       blur pushes them out and the corners round away to nothing.
       Two casts rather than one — alpha was already at .95, so the only way
       left to get darker was to stack a tight core inside the wide pool and
       let them compound. Both keep spread past half their blur, so neither
       leaks out to the sides. */
    var(--cast-key);
  text-shadow: 0 1px 0 rgba(0, 0, 0, .7);
  transition: background var(--motion-base) var(--ease);
}

/* A 1px line just inside the 2px edge. Outside the cap it did not render at
   all — the collar behind it is near-black and the line only carries .07 at
   its strongest, so there was nothing for it to show against. Transparent for
   its whole top half and only surfacing along the foot. */
.btn-face::before {
  content: "";
  position: absolute;
  inset: 2px;
  border-radius: var(--radius-key-lip);
  border: 1px solid transparent;
  background: linear-gradient(180deg,
      rgba(255, 255, 255, 0) 0%,
      rgba(255, 255, 255, 0) 48%,
      rgba(255, 255, 255, .028) 74%,
      rgba(255, 255, 255, .07) 100%) border-box;
  -webkit-mask:
    linear-gradient(#000 0 0) padding-box exclude,
    linear-gradient(#000 0 0);
  mask:
    linear-gradient(#000 0 0) padding-box exclude,
    linear-gradient(#000 0 0);
  pointer-events: none;
}

/* A 2px edge all the way round the cap, bright along the top and very faint
   everywhere else — it never reaches zero, so the container keeps an edge on
   its sides and foot. Fades by alpha, not by tapering: a masked gradient
   border holds its width the whole way round, where an offset inset shadow
   would thin as it goes. */
.btn-face::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 2px solid transparent;
  background: linear-gradient(180deg,
      rgba(255, 255, 255, .18) 0%,
      rgba(255, 255, 255, .07) 28%,
      rgba(255, 255, 255, .04) 56%,
      rgba(255, 255, 255, .03) 100%) border-box;
  -webkit-mask:
    linear-gradient(#000 0 0) padding-box exclude,
    linear-gradient(#000 0 0);
  mask:
    linear-gradient(#000 0 0) padding-box exclude,
    linear-gradient(#000 0 0);
  pointer-events: none;
}

/* Hover leaves the legend alone entirely. Brightening it spent the same move
   the lit state uses, so by the time the button was actually on the glow had
   nothing left to say. The cap shifting under the cursor is the whole of it. */
.btn:hover .btn-face {
  background: linear-gradient(180deg,
      rgba(0, 0, 0, .155) 0%,
      rgba(0, 0, 0, .08) 32%,
      rgba(255, 255, 255, .032) 66%,
      rgba(255, 255, 255, .066) 100%);
}

/* Pressed, and latched-on: the cap turns over into the recess. The rake runs
   the other way and the light that was on its face falls to the floor. */
.btn:active .btn-face,
.btn[aria-pressed="true"] .btn-face,
.btn[data-state="on"] .btn-face {
  background: linear-gradient(180deg,
      rgba(255, 255, 255, .035) 0%,
      rgba(0, 0, 0, .2) 52%,
      rgba(0, 0, 0, .34) 100%);
  box-shadow:
    inset 0 3px 7px rgba(0, 0, 0, .7),
    inset 0 -1px 0 rgba(255, 255, 255, .05);
}

.btn:active .btn-face::before,
.btn[aria-pressed="true"] .btn-face::before,
.btn[data-state="on"] .btn-face::before { display: none; }

/* On backlights the legend: the engraving does not change, the lamp behind it
   comes up. Same two-stop bloom as the seven-segment readouts, so everything
   lit on the fascia glows alike. */
.btn[data-state="on"] { color: var(--tune); }
.btn[data-state="on"] sl-icon {
  color: var(--tune);
  filter: drop-shadow(0 0 5px rgba(var(--lit-rgb), .55));
}
.btn[data-state="on"] .btn-face {
  text-shadow: 0 0 7px rgba(var(--lit-rgb), .5), 0 0 14px rgba(var(--lit-rgb), .22);
}
.btn[data-state="err"] { color: var(--hot); }
.btn[data-state="err"] sl-icon { color: var(--hot); }

/* Secondary is the same button with nothing in front of the legend. There is
   no modifier for it: adding a class that changes no declaration would be a
   hook pretending to be a variant. Drop the sl-icon and it is secondary.

   Icon keys are square, with the mark where the legend would be. Sized off
   --btn-size, so one rule covers a 36px key and a 56px one — the margin below
   is the cap's own, doubled, which is what keeps the face square inside a
   square button. */
.btn--icon {
  --btn-size: 40px;
  width: var(--btn-size);
  height: var(--btn-size);
  flex: none;
}
.btn--icon .btn-face {
  height: calc(var(--btn-size) - 9px);
  padding: 0;
  gap: 0;
}
.btn--icon sl-icon { font-size: var(--icon-md); }
/* A square key with a word in it rather than a mark: the icon key zeroes the
   label padding, so the legend needs its own size to sit right. */
.btn--icon .btn-face { font-size: var(--type-xs); letter-spacing: var(--tracking-md); }

/* On the face, not on the key. Inset it has to be, because outset it crosses
   the key's 3px side and touches its neighbour. But .btn-face is opaque, sits
   5px inside the key and paints at z-index 1, so a ring drawn on .btn at -8px
   landed underneath it: the outline was there on every key and visible on none.
   It read as "focus only works on the selected one", because a latched face is
   the one state that changes enough to notice. */
.btn:focus-visible { outline: none; }
.btn:focus-visible .btn-face {
  outline: var(--focus-ring);
  outline-offset: var(--focus-inset);
}
.btn sl-icon {
  font-size: var(--icon-sm);
  /* text-shadow does not reach an sl-icon, so the same engraving arrives as
     a drop-shadow filter. */
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .7));
}


/* ==========================================================================
   Amp face — new elements
   --------------------------------------------------------------------------
   Built for the redesigned footer, where the transport becomes the whole
   instrument. Each of these stands on the button and knob foundations above
   rather than inventing its own material.
   ========================================================================== */

/* ---------- stem selector: latching keys, one down at a time ---------- */


/* Mark over its key. In a row the marks read as a legend strip above the
   switches; beside them they would push the row past the play key's width. */
.stem-opt { position: relative; display: flex; flex-direction: column; align-items: center; gap: var(--space-sm); }

/* In the stem row the mark is lifted out of the flow. In the flow it adds its
   own height to the top of each module, so the row centres mark-plus-key and
   the keys themselves end up sitting below the select-song key they should
   line up with. Out of flow, the module is exactly one key tall and the row
   aligns on the keys. There is room above because the display makes that grid
   row taller than the play key. */
.stem-select .stem-opt { gap: 0; }
.stem-select .stem-mark {
  position: absolute;
  bottom: calc(100% + 7px);
  left: 50%;
  transform: translateX(-50%);
}

/* The legend is a mark rather than a word — engraved over the key the way a
   stamped symbol sits over a switch on a real fascia. It is engraved, so it
   never changes: the lamp in the middle of the cap is what carries the state,
   and lighting the mark as well would say the same thing twice. */
.stem-mark {
  font-size: var(--type-xl);
  color: var(--mark-ink);
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .75));
}
/* The mark on a stem key is the state itself, so it needs no icon: a bead in
   the middle of the cap, dark until the key is down. Its colour is a property
   of the key, so a lamp that means something other than "selected" can say so
   without a duplicate rule. */
.btn--radio { --lamp: var(--tune); --lamp-rgb: var(--lit-rgb); }
/* .btn-face::before is the cap's hairline everywhere else, and it carries a
   mask that excludes its own padding box. Inherited here with border:0 that
   mask excludes the whole element, which is why the lamp was invisible rather
   than merely subtle — it has to be cleared, not just overridden. */
.btn--radio .btn-face::before {
  content: "";
  position: absolute;
  -webkit-mask: none;
  mask: none;
  background-clip: border-box;
  inset: auto;
  top: 50%;
  left: 50%;
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border: 0;
  border-radius: 50%;
  background: var(--lens-face);
  box-shadow: var(--lens-edge);
  transition: background var(--motion-fast) var(--ease), box-shadow var(--motion-fast) var(--ease);
}
/* The pressed cap hides .btn-face::before because on every other key that
   pseudo is the hairline. Here it is the lamp, so it has to be put back —
   a lamp that goes out when you press the key is the one thing this control
   must never do. */
.btn--radio[data-state="on"] .btn-face::before {
  display: block;
  background: var(--lens-catch), var(--lamp);
  box-shadow:
    inset 0 -1px 0 rgba(255, 255, 255, .2),
    0 0 7px rgba(var(--lamp-rgb), .55),
    0 0 14px rgba(var(--lamp-rgb), .25);
}

/* ---------- play key ---------- */

/* The big one on the fascia. Round, and deliberately not a push key: it does
   not travel and has no pressed face, because it is a latch you read at a
   glance rather than a control you feel. The glyph lights; nothing moves. */
/* The fallback is for anything that uses this key outside the fascia; on the
   amp the size comes from --amp-key, so the knob at the far end can be given
   the same figure rather than a copy of it. */
.btn--play {
  --btn-size: var(--amp-key, 96px);
  width: var(--btn-size);
  height: var(--btn-size);
  border-radius: 50%;
  flex: none;
}
.btn--play::before,
.btn--play::after { border-radius: 50%; }
.btn--play .btn-face {
  height: calc(var(--btn-size) - 9px);
  padding: 0;
  border-radius: 50%;
}
.btn--play .btn-face::before,
.btn--play .btn-face::after { border-radius: 50%; }
/* No press: the cap keeps its idle face however hard you click it. */
.btn--play:active .btn-face,
.btn--play[data-state="on"] .btn-face {
  background:
    linear-gradient(180deg,
      rgba(0, 0, 0, .17) 0%,
      rgba(0, 0, 0, .09) 32%,
      rgba(255, 255, 255, .025) 66%,
      rgba(255, 255, 255, .055) 100%),
    var(--btn-base);
  box-shadow:
    inset 0 -3px 7px rgba(0, 0, 0, .45),
    var(--cast-key);
}
/* The glyph is the whole legend on this key, so it is sized off the key
   rather than pinned at a fixed 30px on a 96px cap. */
.btn--play .play-glyph {
  position: relative;
  display: block;
  width: calc(var(--btn-size) * .3);
  height: calc(var(--btn-size) * .3);
}

/* Light on the cap, not an outline on the legend. A lamp behind a cut-out does
   not only light its own shape — the cap around it warms, and that halo is
   most of what says the thing is lit rather than merely a different colour.
   Round, because the source is a bulb, and sized to reach about as far as the
   cap's own face so the warmth stops where the cap does. */
.btn--play .play-glyph::before {
  content: "";
  position: absolute;
  inset: -100%;
  border-radius: 50%;
  background: radial-gradient(closest-side circle,
    rgba(var(--lit-rgb), .17) 0%,
    rgba(var(--lit-rgb), .07) 42%,
    rgba(var(--lit-rgb), 0) 74%);
  opacity: 0;
  transition: opacity var(--motion-slow) var(--ease);
  pointer-events: none;
}
.btn--play[data-state="on"] .play-glyph::before { opacity: 1; }

/* No display here: the element selector makes this (0,2,1), which outranks
   the (0,2,0) rule that hides the inactive glyph — and both marks rendered at
   once. Sizing only; display is decided by the .glyph rules below.
   position:relative so the mark paints over the halo behind it: the halo is
   absolutely positioned, and without this it would sit on top of the thing it
   is supposed to be light from. */
.btn--play .play-glyph svg {
  position: relative;
  width: 100%;
  height: 100%;
  /* Engraved while dark, the way every legend on this fascia is: a cut with
     the panel's own shadow under its lower edge. */
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .55));
  transition: filter var(--motion-base) var(--ease);
}
.btn--play .play-glyph path { fill: var(--glyph-ink); transition: fill var(--motion-base) var(--ease); }
.btn--play[data-state="on"] .play-glyph path { fill: var(--tune); }

/* The bloom, on the svg rather than on the path inside it — which is the whole
   bug. A CSS filter on an SVG child element is clipped to that element's
   filter region, and the region defaults to its bounding box plus a tenth: a
   6px blur on a 29px triangle overran it and came back cropped to a rectangle.
   That was the square. On the element itself there is no such region and the
   bloom follows the triangle's own alpha.
   Three shadows rather than one, because light is layered: a tight core at the
   filament, the spread through the lens, and a wide haze that is nearly gone
   by the time it reaches the rim. */
.btn--play[data-state="on"] .play-glyph svg {
  filter:
    drop-shadow(0 0 2px rgba(var(--lit-rgb), .85))
    drop-shadow(0 0 7px rgba(var(--lit-rgb), .5))
    drop-shadow(0 0 16px rgba(var(--lit-rgb), .25));
}
/* One mark, always the same mark. A stamped triangle cannot become a pair of
   bars because the transport started moving — the legend is cut into the cap,
   and only the lamp behind it changes. The display carries the word. */
.btn--play .glyph { width: 100%; height: 100%; }
/* The triangle is optically left-heavy, so it sits back toward centre. */
.btn--play .glyph--play { transform: translateX(4%); }

/* ---------- the display ---------- */

/* The readout on the fascia: the glass above, at fascia size.
   A fixed height, not a minimum. This is a piece of hardware, and a panel that
   grows when you open the song list is a web page — every mode has to fit this
   box or scroll inside it. */
.display {
  position: relative;      /* the notice line is laid over the glass */
  height: 172px;
  padding: 14px 18px 12px;
  display: flex;
  flex-direction: column;
}
/* display:flex outranks the UA's [hidden] rule, so the panel has to say it. */
.display[hidden] { display: none; }
/* The wash: the panel's own pixels lighting in a band as it redraws, the way a
   segment display flickers through everything it can show before settling.
   The band moves and the dot grid does not — the grid is a static mask on a
   sweeping gradient, so the pixels light where they are rather than sliding
   across the glass. Built that way round because the alternative, animating
   mask-position over an oversized mask, resolves its percentages against a
   negative and spends most of the animation off-screen.
   Not scoped to the display any more: the message on the roll redraws for the
   same reason a readout does, and it should settle the same way. Anything with
   a .dsp-wash layer inside it washes when data-wash goes on.
   It has to stay below `.glass > *` in the file — that rule sets every direct
   child of a panel to position:relative, and at equal specificity source order
   is all that keeps this one absolute. */
.dsp-wash {
  position: absolute;
  inset: 0;
  z-index: var(--layer-scan);
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}
.dsp-wash::before {
  content: "";
  position: absolute;
  inset: -20% -70%;
  background: linear-gradient(var(--wash-tilt, 104deg),
      transparent 0%,
      rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .6) var(--wash-lead, 20%),
      rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .6) var(--wash-trail, 34%),
      transparent calc(var(--wash-trail, 34%) + 20%));
  /* Three rulings intersected. Two on the axes give the dot grid; the third
     is off-axis and on a coprime pitch, so where it lands between the dots
     varies across the panel and the band lights unevenly. A grid alone lights
     every dot the same and reads as a swept rectangle. */
  -webkit-mask-image:
    repeating-linear-gradient(90deg, #000 0 1px, transparent 1px 3px),
    repeating-linear-gradient(180deg, #000 0 1px, transparent 1px 3px),
    repeating-linear-gradient(var(--wash-grain, 24deg), #000 0 4px, transparent 4px 7px);
  mask-image:
    repeating-linear-gradient(90deg, #000 0 1px, transparent 1px 3px),
    repeating-linear-gradient(180deg, #000 0 1px, transparent 1px 3px),
    repeating-linear-gradient(var(--wash-grain, 24deg), #000 0 4px, transparent 4px 7px);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  transform: translateX(var(--wash-from, -55%));
}
[data-wash] .dsp-wash {
  animation: dsp-wash-fade var(--wash-ms, 520ms) linear both;
}
[data-wash] .dsp-wash::before {
  animation: dsp-wash var(--wash-ms, 520ms) cubic-bezier(.3, .05, .3, 1) both;
}
@keyframes dsp-wash {
  from { transform: translateX(var(--wash-from, -55%)); }
  to   { transform: translateX(var(--wash-to, 55%)); }
}
@keyframes dsp-wash-fade {
  0%   { opacity: 0; }
  16%  { opacity: .62; }
  70%  { opacity: .45; }
  100% { opacity: 0; }
}

/* Coming up in stages, in the order the panel would have them: the trace is
   the thing being looked at, the engraved readings frame it, and the clocks
   settle last. A display does not slide its contents in — everything arrives
   by lighting, with a moment of overshoot as the drive settles. */
@keyframes dsp-in {
  0%   { opacity: 0; }
  55%  { opacity: 1; filter: brightness(1.18); }
  100% { opacity: 1; filter: none; }
}
/* Every delay is a variable, redrawn per wash. Fixed numbers make the second
   viewing identical to the first, which is the point at which an animation
   stops being the panel waking up and starts being a jingle. */
.display[data-wash] .dsp-trace { animation: dsp-in 460ms var(--in-trace, 90ms) both; }
.display[data-wash] .dsp-row   { animation: dsp-in 440ms var(--in-rows, 270ms) both; }
.display[data-wash] .dsp-time  { animation: dsp-in 400ms var(--in-times, 460ms) both; }
.display[data-wash] .dsp-empty,
.display[data-wash] .dsp-confirm-say,
.display[data-wash] .dsp-upload { animation: dsp-in 480ms var(--in-trace, 90ms) both; }
/* The answers land after the question, the way the list's rows land after its
   frame — you are given the thing to decide before the way to decide it. */
.display[data-wash] .dsp-choice {
  animation: dsp-in 340ms both;
  animation-delay: calc(var(--in-rows, 270ms) + var(--row, 0) * var(--in-step, 60ms));
}
/* One row at a time, --row set on each when the list is wired. */
.display[data-wash] .dsp-song {
  animation: dsp-in 340ms both;
  animation-delay: calc(var(--in-rows, 270ms) + var(--row, 0) * var(--in-step, 60ms));
}

/* The takes panel's screen, on the same drive. What it is showing arrives the
   way the fascia's list does: the frame first, then the rows, one after the
   other — a screen that changes mode does not slide the new mode in. */
.panel-screen[data-wash] .screen-top,
.panel-screen[data-wash] .empty,
.panel-screen[data-wash] .lyric-edit,
.panel-screen[data-wash] .screen-note,
.panel-screen[data-wash] .dsp-confirm-say { animation: dsp-in 460ms var(--in-trace, 90ms) both; }
.panel-screen[data-wash] .dsp-choice {
  animation: dsp-in 340ms both;
  animation-delay: calc(var(--in-rows, 270ms) + var(--row, 0) * var(--in-step, 60ms));
}
.panel-screen[data-wash] .take,
.panel-screen[data-wash] .spot {
  animation: dsp-in 340ms both;
  animation-delay: calc(var(--in-rows, 270ms) + var(--row, 0) * var(--in-step, 60ms));
}

@media (prefers-reduced-motion: reduce) {
  [data-wash] .dsp-wash,
  [data-wash] .dsp-wash::before,
  .display[data-wash] .dsp-trace,
  .display[data-wash] .dsp-row,
  .display[data-wash] .dsp-time,
  .display[data-wash] .dsp-empty,
  .display[data-wash] .dsp-upload,
  .display[data-wash] .dsp-song,
  .display[data-wash] .dsp-confirm-say,
  .display[data-wash] .dsp-choice { animation: none; }
  .panel-screen[data-wash] .take,
  .panel-screen[data-wash] .spot,
  .panel-screen[data-wash] .screen-top,
  .panel-screen[data-wash] .empty,
  .panel-screen[data-wash] .lyric-edit,
  .panel-screen[data-wash] .screen-note,
  .panel-screen[data-wash] .dsp-confirm-say,
  .panel-screen[data-wash] .dsp-choice,
  .takes-inner[data-swap] .panel-head h2,
  .takes-inner[data-swap] .panel-back,
  .takes-inner[data-swap] .panel-kill,
  .takes-inner[data-swap] .head-keys,
  .takes-inner[data-swap] .mix-block { animation: none; }
}


/* Everything printed on the glass is lit from behind, so it all carries the
   same bloom the seven-segment readouts do. */
/* Silkscreen: a bitmap face, so it must land on whole pixels — fractional
   sizes turn the stems to mush. 8px and 16px only. */
.dsp-text {
  font-family: var(--stencil);
  font-size: var(--type-2xs);
  font-weight: 400;
  letter-spacing: var(--tracking-md);
  text-transform: uppercase;
  color: var(--dsp-lit, var(--lit));
  text-shadow:
    0 0 7px rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .45),
    0 0 15px rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .2);
}
/* Secondary readings on the glass — still lit, just further back. */
.dsp-text--dim {
  color: var(--dsp-lit-dim, #a08a52);
  text-shadow: 0 0 6px rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .22);
}
/* The four corner readings are the song's label. 8px was a footnote and 16px
   was a headline; these are neither — they are the small print a machine
   stencils round the edge of its screen, and 11px is the size that reads at a
   glance without competing with the trace between them. */
.dsp-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xl);
  line-height: var(--leading-flush);
}
.dsp-row .dsp-text { font-size: var(--type-md); }
/* Three tracks, not space-between: the status has to sit on the centre of the
   glass, and with two flexible neighbours of different widths it would sit
   wherever they left it. */
.dsp-row--head {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: baseline;
}
.dsp-row--head > :last-child { justify-self: end; text-align: right; }
/* What the machine is doing, in its own voice — dimmer than the song's title
   beside it, because it is a state and not a name. */
.dsp-status { color: var(--dsp-lit-dim, #a08a52); }
/* Breathing, slowly. These are the states that are a continuous act rather
   than a condition — listening, playing something back — and they are the only
   things on this fascia that move while you are singing at it. */
.dsp-status[data-tone="live"] { animation: dsp-breathe var(--breathe-live) ease-in-out infinite; }
/* Recording is the one status that has to be unmistakable at a glance, so it
   takes the tally colour and a pulse you would notice from across the room.
   Red, because on anything that records you that is what the light has always
   meant — and it is the one colour this glass never uses for anything else. */
.dsp-status[data-tone="rec"] {
  color: var(--warn);
  text-shadow: 0 0 9px rgba(207, 47, 24, .55);
  animation: dsp-breathe var(--breathe-rec) ease-in-out infinite;
}
@keyframes dsp-breathe { 50% { opacity: .32; } }
@media (prefers-reduced-motion: reduce) {
  .dsp-status[data-tone] { animation: none; }
}
.dsp-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A fixed height, not flex:1. The canvas inside is sized 100% of this row
   while this row's height would otherwise come from the canvas's own content
   — and Timeline writes canvas.width/height on every resize, so the two feed
   each other and the display grows without limit. */
.dsp-body { flex: none; height: 108px; display: flex; align-items: center; gap: var(--space-2xl); }
/* The one exception to the display's pixel face: big numerals stay
   seven-segment. A readout that large is a numeric register, and the two
   faces sit together the way they do on real gear — pixel legends around a
   segment counter. */
.dsp-time {
  flex: none;
  font-family: var(--face-seg);
  font-size: var(--type-xl);
  font-weight: 700;
  letter-spacing: var(--tracking-sm);
  color: var(--tune);
  text-shadow: 0 0 8px rgba(var(--lit-rgb), .5), 0 0 16px rgba(var(--lit-rgb), .22);
}
.dsp-trace { flex: 1; min-width: 0; align-self: stretch; position: relative; }
.dsp-trace canvas { display: block; width: 100%; height: 100%; }

/* Empty: no song, so the display carries the instructions the header used to. */
/* One mode is on at a time, and each panel is hidden unless it is the mode.
   Written as :not() on the panel rather than a list of modes on the wrapper,
   so adding a fourth mode does not mean editing three other selectors. */
/* space-between, so the title sits at the top of the glass, the readings at
   the bottom, and the trace takes the middle — rather than all three stacking
   under the top edge with the rest of the panel empty. */
.dsp-loaded {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  flex: 1;
  min-height: 0;
}
/* The notice line. Laid over the glass rather than in the flow: it appears for
   a few seconds and goes again, and reserving a row for it would leave a gap in
   the layout for the whole of the time it has nothing to say. Bottom edge
   because the top of the glass already carries the title and the reading. */
.dsp-say {
  position: absolute;
  /* As wide as what it says, centred, rather than a band across the glass:
     the foot of the loaded view carries a reading at each end, and a full
     width plate sat on top of both of them with the panel showing faintly
     through. Centred, it takes the empty middle and leaves them alone. */
  left: 50%;
  transform: translateX(-50%);
  max-width: calc(100% - 36px);
  width: max-content;
  bottom: 10px;
  z-index: var(--layer-veil);
  margin: 0;
  padding: 5px 10px;
  border-radius: var(--radius-md);
  background: var(--panel);
  border: 1px solid var(--line);
  color: var(--dsp-lit, #e8c877);
  font-size: var(--type-md);
  line-height: var(--leading-tight);
  text-align: center;
  animation: dsp-say-in .18s ease-out;
}
.dsp-say[hidden] { display: none; }
/* The one message that has to be told apart at a glance: something failed. */
.dsp-say[data-tone="bad"] {
  color: var(--hot);
  border-color: color-mix(in srgb, var(--hot) 45%, var(--line));
}

/* translateX is doing the centring, so the entrance has to carry it too. */
@keyframes dsp-say-in {
  from { opacity: 0; transform: translate(-50%, 3px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .dsp-say { animation: none; }
}

.display:not([data-mode="loaded"]) .dsp-loaded { display: none; }
.display:not([data-mode="empty"]) .dsp-empty { display: none; }
.display:not([data-mode="list"]) .dsp-list { display: none; }
.display:not([data-mode="upload"]) .dsp-upload { display: none; }
.display:not([data-mode="confirm"]) .dsp-confirm { display: none; }
.display:not([data-mode="input"]) .dsp-inputs { display: none; }
/* Asking whether to destroy something is the one thing this display does that
   is not amber. Red is the colour it has never used for "on", which is exactly
   why it can be spent here — including on the wash, so the panel arrives red
   rather than turning red once it has settled. */
.display[data-mode="confirm"],
.panel-screen[data-view="confirm"] {
  --dsp-lit: var(--warn);
  --dsp-lit-dim: var(--warn-dim);
  --dsp-lit-rgb: 207, 47, 24;
  /* The unlit glass with it. The lamp alone left the corners of the panel
     sitting in the warm brown they are lit in the rest of the time, which put
     a seam across the glass where the two colours met. */
  --glass-top: #0a0605;
  --glass-mid: #0e0806;
  --glass-foot: #170c08;
}

/* Mic setup is not a display. It is a row of physical controls that occupies
   the panel where the display sits — bare fascia, no glass, no frame, and the
   fascia's own engraved legends rather than lit pixels. Matched to the
   display's height so nothing jumps when it takes over. */
/* The knob zone occupies exactly the display's slot — same grid column, so its
   left edge lines up with the select-song key and its right edge with the
   settings key without either being positioned against the other. A subtle
   border makes the section read as one enclosure rather than four loose
   controls on the fascia. */
.amp-settings {
  position: relative;
  height: 172px;
  display: flex;
  align-items: center;
  padding: 0 var(--space-2xl);
  border: 1px solid rgba(255, 255, 255, .055);
  border-radius: var(--well-radius);
}
/* On the amp face the cap fills more of its wrap than it does in the popover.
   The wrap has to hold the tick ring and its numbers, but at .5 nearly half of
   every knob's width was empty margin — which is where the fascia ran out of
   room at 1264px. At .62 the caps are the same size on screen and each module
   is a fifth narrower. Scoped to .amp so the popover keeps its own proportions. */
.amp .dial-wrap {
  --cap: calc(var(--dial, 116px) * .62);
}
/* The ring sits lower now, so its end numbers move down out of its way. */
.amp .dial-min,
.amp .dial-max { bottom: 4px; }

/* The master's scale is placed off the ring rather than off the corners of its
   box: each figure sits just outside the tick it belongs to, at the angle that
   tick is at. Sine and cosine of 135 degrees, which is where the two ends of a
   270-degree sweep land.
   That is what puts 0 and 100 beside their own end of the travel rather than
   down in the corners, and it holds at any --dial. */
/* The master's bead, in proportion to a cap nearly twice the size of any
   other on the panel. */
.amp-volume .dial-wrap {
  --bead: 7px;
  --bead-inset: 13px;
}

.amp-volume .dial-min,
.amp-volume .dial-max {
  /* How far out the figures sit from the centre. Off the tick ring rather than
     off the cap, so they keep their distance from the marks they label at any
     size. 13px crowded the dots and read as a scale that had run out of room;
     16px clears the marks and, with only the two end figures left on the ring,
     keeps the whole scale inside the slot it is engraved in. */
  --ring: calc(var(--tick-r) + 16px);
  left: auto;
  right: auto;
  bottom: auto;
  transform: translate(-50%, -50%);
}
.amp-volume .dial-min {
  left: calc(50% - var(--ring) * 0.707);
  top: calc(50% + var(--ring) * 0.707);
}
.amp-volume .dial-max {
  left: calc(50% + var(--ring) * 0.707);
  top: calc(50% + var(--ring) * 0.707);
}
/* No middle figure and no quarters: 0 and 100 at the ends of the travel, and
   the ticks between them for everything else. The quarters went because they
   sit almost due east and west and so set the module's width single-handed —
   the knob had to give up more size to contain them than the two figures were
   worth. */
.amp-volume .dial-mid { display: none; }

/* Knobs only now — the mic key moved out to the fascia — so the bank takes the
   whole container and spreads across it. */
/* Capped, then centred. The bank spreads its knobs across whatever it is
   given, and on a wide screen that pushed the four caps to the far corners of
   the fascia with a metre of empty panel between them — a bank of controls is
   a group, and a group that wide stops reading as one. Past the cap the extra
   width becomes margin on both sides rather than more air between the knobs. */
.amp-settings-knobs {
  flex: 1;
  max-width: 940px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-xl);
}
/* Equal shares, not space-between. Spreading the leftover space equalises the
   gaps between modules, but the modules are different widths because the
   legends are — so the knobs themselves end up unevenly spaced. Giving each
   module the same width puts the caps on a regular pitch whatever the words
   beside them do.
   The size lives here rather than on the elements: as an inline style no media
   query could reach it, and the cap is the one thing in the module that cannot
   shrink on its own. */
.amp-settings-knobs .knob--tight {
  flex: 1 1 0;
  min-width: 0;
  --dial: 120px;
}

/* The lamp selector, at the right-hand end of the settings panel. Legend above
   the keys rather than beside them, so the row of three reads as one control
   with a name rather than as three keys that happen to be near a word. */
/* In the shell the lamp is a menu (Lamp, in the menu bar), so the keys
   in the settings bank would be a second control for one thing. */
html[data-shell] .amp-lamps { display: none; }
.amp-lamps {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-lg);
  margin-left: var(--space-3xl);
  padding-left: var(--space-3xl);
  border-left: 1px solid rgba(255, 255, 255, .05);
}
.amp-lamps-keys { display: flex; align-items: center; gap: var(--key-gap); }
.amp-lamps .btn { --btn-size: 34px; }
/* Each key lit in the colour it fits, whether or not it is the one in use —
   this is a swatch, and a swatch that only shows its colour once you have
   chosen it is no help at all. The unlit ones are the same colour behind the
   glass, which is what --lens-face already does with --lamp-rgb. */
.amp-lamps [data-theme="amber"] { --lamp: #f5b52a; --lamp-rgb: 245, 181, 42; }
.amp-lamps [data-theme="green"] { --lamp: #3fdd88; --lamp-rgb: 63, 221, 136; }
.amp-lamps [data-theme="blue"]  { --lamp: #4aa8ff; --lamp-rgb: 74, 168, 255; }
.amp-lamps [data-theme="white"] { --lamp: #eef2f6; --lamp-rgb: 238, 242, 246; }

/* A module's floor is cap + gap + readout, and below roughly 1400px four of
   those stop fitting the display's slot — so they were overrunning into each
   other rather than the row admitting it had no room. The cap comes down
   instead, which is the only part with any to give. */
@media (max-width: 1400px) {
  .amp-settings-knobs { gap: var(--key-gap); }
  /* Four swatches and four knobs do not both fit at full size. The swatches
     give way first: they are read, not aimed at. */
  .amp-lamps { margin-left: var(--space-xl); padding-left: var(--space-xl); gap: var(--space-md); }
  .amp-lamps .btn { --btn-size: 28px; }
  .amp-lamps-keys { gap: var(--space-sm); }
  .amp-settings-knobs .knob--tight { --dial: 98px; gap: 9px; }
  .amp-settings-knobs .led--mini { min-width: 44px; padding: 4px 5px; }
}
/* The lyric editor: a gutter of times beside one field holding the whole
   lyric. The two scroll together and share a line box exactly — the times
   have to sit on their lines, and a half-pixel of drift over forty lines is
   a whole line by the bottom. So the line-height is a variable both read,
   rather than a number written twice. */
/* The panel it sits in already scrolls, so the field grows to fit the whole
   lyric rather than scrolling inside itself. One scrollbar instead of two,
   and the gutter cannot drift out of step with the text because there is no
   second scroll position to keep in step with. */
.lyric-edit {
  --lyric-line: 26px;
  /* Held here for the same reason the line height is: the mirror has to be set
     in exactly the same face at exactly the same size, or the row it reports a
     line landing on is not the row the line is on. Two literals would drift
     the first time one of them was changed alone. */
  --lyric-size: 14px;
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-lg);
}
.lyric-edit[hidden] { display: none; }

/* A line long enough to wrap takes two rows on screen but is still one line
   of lyric, so the times cannot simply be stacked at a fixed pitch — they are
   placed at the height the text actually reached. */
.lyric-gutter {
  position: relative;
  padding: var(--space-lg) 0;
  /* Room for the stamp as well as the time. */
  min-width: 56px;
  text-align: right;
}
/* One row: the mark that brings a line to the playhead, then the time that
   takes you to the line. Two verbs, two targets. */
.lyric-at-line {
  position: absolute;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 5px;
  height: var(--lyric-line);
  white-space: nowrap;
  color: rgba(255, 255, 255, .3);
}
.lyric-at {
  flex: none;
  width: 46px;
  padding: 4px 5px;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--glass-ink-faint);
  font: inherit;
  font-size: var(--type-sm);
  font-variant-numeric: tabular-nums;
  text-align: left;
  cursor: pointer;

  padding: 0 var(--space-2xs);
  border: 0;
  background: none;
  /* The display's face, not the page's. A clock printed on glass is a machine
     reading — and Silkscreen is fixed-pitch, so the columns line up without
     asking for tabular figures. */
  font-family: var(--stencil);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: var(--tracking-sm);
  line-height: var(--lyric-line);
  color: inherit;
  cursor: pointer;
}
/* A line you have typed has no time yet — say so with a placeholder rather
   than a stale number from whatever used to be on that row. Dim, and not a
   target: there is nowhere to play from. */
.lyric-at-line--none .lyric-at { cursor: default; opacity: var(--dim-half); }

/* The stamp: a playhead in miniature, because that is exactly what it puts
   there. Out of the way until the row is under the pointer — except on a line
   with no time at all, where it is the only thing to do and says so. */
.lyric-stamp {
  position: relative;
  flex: none;
  width: 11px;
  height: var(--lyric-line);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.lyric-stamp::before,
.lyric-stamp::after {
  content: "";
  position: absolute;
  left: 50%;
  background: currentColor;
}
.lyric-stamp::before { top: 50%; width: 1px; height: 11px; margin: -6px 0 0 -1px; }
.lyric-stamp::after  { top: 50%; width: 5px; height: 1px; margin: -1px 0 0 -3px; }
.lyric-at-line:hover .lyric-stamp,
.lyric-at-line--none .lyric-stamp,
.lyric-stamp:focus-visible { opacity: var(--dim-most); }
.lyric-stamp:hover { opacity: 1; }

.lyric-area,
.lyric-mirror {
  min-width: 0;
  padding: var(--space-lg) var(--space-xl) var(--space-lg) 0;
  border: 0;
  background: none;
  resize: none;
  overflow: hidden;   /* height is set to the content; the panel does the scrolling */
  color: inherit;
  font: inherit;
  font-size: var(--lyric-size);
  line-height: var(--lyric-line);
  white-space: pre-wrap;
  overflow-wrap: break-word;
  outline: none;
}
/* Measured, never seen. Out of flow so it cannot affect the layout it is
   measuring, and out of the accessibility tree so it is not read twice. */
.lyric-mirror {
  position: absolute;
  visibility: hidden;
  pointer-events: none;
  top: 0;
  left: -9999px;
  font-size: var(--lyric-size);
  line-height: var(--lyric-line);
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

/* Unsaved: the gutter is showing times from before the edit, so it stops
   claiming to describe what is on screen. */
.lyric-edit.dirty .lyric-gutter { opacity: var(--dim-half); }

/* The lamp on the scoreboard. Same column as the readings beside it so the
   labels line up, but a key instead of a window — the one thing on the ledge
   you set rather than watch. */
.stat--switch { justify-content: flex-start; }
/* The fascia's key size, like every other key on the machine. It was 30px as
   an inline style — small enough beside a 34px key to read as a different
   kind of control rather than the same one, and out of reach of any rule that
   might have told it otherwise. */
.stat--switch .btn { --btn-size: 34px; margin-top: var(--space-2xs); }

/* How long to hold a word, drawn as a bar beneath it. The width is the
   duration, not the word: "mean" held for a second and a half earns a longer
   mark than "complicatedness" sung quickly, which is the whole point of
   showing it. Off by default — it is another thing to read while singing,
   and the words themselves come first. */
/* How long to hold a word, as a bar that empties while you hold it.
 *
 * Two layers: an unlit track the length of the note, and a lit part that fills
 * across it as the word is sung. The track only exists for words still to
 * come — once a word is done its bar goes, so what is left on screen is the
 * road ahead rather than a record of the road behind.
 *
 * Length is the duration up to the width of the word, and no further. A mark
 * that outran its word read as belonging to the one after it, which is the
 * one thing it must never say. Past that point a longer hold shows as a
 * slower crossing rather than a longer mark: the lit part is a fraction of
 * the word's own duration either way, so nothing is lost by capping it.
 */
.lyric-now .word { position: relative; }
.lyric-now .word::before,
.lyric-now .word::after {
  content: "";
  position: absolute;
  bottom: -6px;
  height: 2px;
  border-radius: var(--radius-hair);
  opacity: 0;
  pointer-events: none;
  /* 40px a second up to the word's own width, and never below the floor — a
     sustained "I" is narrower than any mark that could be read as one, so
     that is the single case allowed past the word's edge. */
  --track: clamp(22px, calc(var(--hold, 0) * 40px), 100%);
}
/* The track, centred under its word. Left-aligned it read as belonging to
   whatever came after it, because a word's box carries its trailing space. */
.lyric-now .word::before {
  left: 50%;
  transform: translateX(-50%);
  width: var(--track);
  background: currentColor;
  transition: opacity var(--motion-base) linear;
}
/* What has been sung of it — growing from the track's left edge, not from the
   middle, so it drains the way it is read. No transition: this is driven
   every frame, and a transition would make it lag behind the voice it is
   reporting. */
.lyric-now .word::after {
  left: calc(50% - var(--track) / 2);
  width: calc(var(--track) * var(--fill, 0));
  background: var(--dsp-lit, var(--lit));
  box-shadow: 0 0 6px rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .5);
}
body[data-holds="on"] .lyric-now .word.ahead::before { opacity: var(--dim-faint); }
body[data-holds="on"] .lyric-now .word.ahead::after { opacity: var(--dim-near); }

.amp-settings[hidden] { display: none; }

.dsp-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 9px;
  text-align: center;
}
.dsp-empty p { margin: 0; max-width: 52ch; line-height: var(--leading-loose); }
.dsp-empty > .dsp-text:first-child { font-size: var(--type-xl); letter-spacing: var(--tracking-lg); }


/* Drawn from pixels, not set in an icon font. Everything else behind this
   glass is on a 1px grid; a vector cloud with smooth curves is the one thing
   on the panel that could not be made of lit dots.

   Two elements, and it has to be two. The glyph is entirely box-shadows, and a
   shadow takes no layout space — so a bare 3px element reserved 3px in the
   flex column while the arrow drew 15x21 over whatever came next. It cannot
   simply be sized up either: box-shadow copies the element's own box, so a
   15x21 element would stamp 15x21 rectangles instead of pixels. The outer box
   holds the glyph's real size for layout; the inner one stays a single pixel
   and does the drawing. */
.dsp-upload-mark {
  position: relative;
  flex: none;
  width: 15px;
  height: 21px;
}
.dsp-upload-mark::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 3px;
  height: 3px;
  box-shadow:
    /* the head */
                     6px 0    0 var(--tune),
    3px 3px  0 var(--tune),  6px 3px  0 var(--tune),  9px 3px  0 var(--tune),
    0 6px    0 var(--tune),  3px 6px  0 var(--tune),  6px 6px  0 var(--tune),
    9px 6px  0 var(--tune), 12px 6px  0 var(--tune),
    /* the shaft */
                             6px 9px  0 var(--tune),
                             6px 12px 0 var(--tune),
    /* the floor it lifts from */
    0 18px   0 var(--tune),  3px 18px 0 var(--tune),  6px 18px 0 var(--tune),
    9px 18px 0 var(--tune), 12px 18px 0 var(--tune);
  filter: drop-shadow(0 0 6px rgba(var(--lit-rgb), .45));
}

/* While it is working, the arrow is wrong: nothing is being uploaded any more,
   the file is already here and the machine is listening to it. Same 3px grid
   and the same floor the arrow lifted from, so it reads as the same panel
   having changed its mind rather than a different one — but bars standing on
   that floor instead of something leaving it, and moving, because separating,
   tracking and transcribing a song takes minutes and a still glyph looks
   stuck. Drawn on ::after, so the arrow's own rule is left as it was. */
.dsp-upload[data-busy] .dsp-upload-mark::before { content: none; }

.dsp-upload[data-busy] .dsp-upload-mark::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 3px;
  height: 3px;
  filter: drop-shadow(0 0 6px rgba(var(--lit-rgb), .45));
  /* Stepped, not eased: these are lit cells on a grid, and a meter that slid
     between positions would be a different kind of object altogether. */
  animation: dsp-work 1.05s steps(1, end) infinite;
}

@keyframes dsp-work {
  0% {
    box-shadow:
      0 12px   0 var(--tune),   0 15px 0 var(--tune),
      6px 3px  0 var(--tune),  6px 6px  0 var(--tune),  6px 9px  0 var(--tune),
      6px 12px 0 var(--tune),  6px 15px 0 var(--tune),
      12px 9px 0 var(--tune), 12px 12px 0 var(--tune), 12px 15px 0 var(--tune),
      /* the floor it lifted from, kept */
      0 18px   0 var(--tune),  3px 18px 0 var(--tune),  6px 18px 0 var(--tune),
      9px 18px 0 var(--tune), 12px 18px 0 var(--tune);
  }
  33% {
    box-shadow:
      0 6px    0 var(--tune),   0 9px  0 var(--tune),
      0 12px   0 var(--tune),   0 15px 0 var(--tune),
      6px 12px 0 var(--tune),  6px 15px 0 var(--tune),
      12px 3px 0 var(--tune), 12px 6px  0 var(--tune),  12px 9px 0 var(--tune),
      12px 12px 0 var(--tune), 12px 15px 0 var(--tune),
      0 18px   0 var(--tune),  3px 18px 0 var(--tune),  6px 18px 0 var(--tune),
      9px 18px 0 var(--tune), 12px 18px 0 var(--tune);
  }
  66% {
    box-shadow:
      0 3px    0 var(--tune),   0 6px  0 var(--tune),   0 9px 0 var(--tune),
      0 12px   0 var(--tune),   0 15px 0 var(--tune),
      6px 6px  0 var(--tune),  6px 9px  0 var(--tune),
      6px 12px 0 var(--tune),  6px 15px 0 var(--tune),
      12px 12px 0 var(--tune), 12px 15px 0 var(--tune),
      0 18px   0 var(--tune),  3px 18px 0 var(--tune),  6px 18px 0 var(--tune),
      9px 18px 0 var(--tune), 12px 18px 0 var(--tune);
  }
}

/* Still a meter, just not a moving one. */
@media (prefers-reduced-motion: reduce) {
  .dsp-upload[data-busy] .dsp-upload-mark::after { animation: none; }
}


/* Drawn from pixels, not set in an icon font. Everything else behind this
   glass is on a 1px grid; a vector cloud with smooth curves is the one thing
   on the panel that could not be made of lit dots.
   The element itself is transparent — the glyph is entirely box-shadows on a
   3px pitch, so the shape needs no pixel at its own origin. */
.dsp-upload .dsp-text { font-size: var(--type-xl); }
.dsp-upload .dsp-text--dim { font-size: var(--type-2xs); letter-spacing: var(--tracking-xl); }
/* Adding a song, on the glass. The dashed rule is the one place on this fascia
   where a border does UI work rather than describing the edge of something
   physical — still worth revisiting. */
/* Adding a song, on the glass. The dashed rule is the one place on this fascia
   where a border does UI work rather than describing the edge of something
   physical — still worth revisiting. */
.dsp-upload {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
  margin: var(--space-2xs) 0 var(--space-xs);
  border: 1px dashed rgba(var(--lit-rgb), .28);
  border-radius: var(--radius-md);
  text-align: center;

  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
  margin: var(--space-2xs) 0 var(--space-xs);
  border: 1px dashed rgba(var(--lit-rgb), .28);
  border-radius: 6px;
  text-align: center;
 transition: border-color var(--motion-fast) var(--ease), background var(--motion-fast) var(--ease);
}
/* Mid-drag: unmistakable, because the cursor is already moving. */
.dsp-upload[data-over] {
  border-color: rgba(var(--lit-rgb), .7);
  border-style: solid;
  background: rgba(var(--lit-rgb), .05);
}
.dsp-upload[data-busy] { border-style: solid; border-color: rgba(var(--lit-rgb), .3); }

/* A run of lit dots on the same 3px pitch as the wash, travelling while the
   job runs. Indeterminate on purpose: separation reports stages, not a
   percentage, and a bar that invents one is lying. */
.dsp-bar {
  /* Block, or the height below is ignored and the track is as tall as the
     line box it sits in: this was a span, so a 3px rail rendered 49px deep
     and the lit part read as a moving slab rather than a bar. Centred by its
     margins, under text that is centred too — align-self was doing nothing
     here, because the parent is not a flex container. */
  display: block;
  margin-inline: auto;
  width: 62%;
  height: 3px;
  overflow: hidden;
  /* The panel's own light, so a bar on a screen that has gone red for a
     destructive errand runs red with it rather than staying amber. */
  background-image: repeating-linear-gradient(90deg,
      rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .12) 0 1px, transparent 1px 3px);
}
.dsp-bar[hidden] { display: none; }
.dsp-bar i {
  display: block;
  width: 34%;
  height: 100%;
  background-image: repeating-linear-gradient(90deg,
      rgba(var(--dsp-lit-rgb, var(--lit-rgb)), .9) 0 1px, transparent 1px 3px);
  animation: dsp-bar 1.5s linear infinite;
}
/* Once the figure is known the block stops travelling and becomes the figure:
   the same lit dots, as much of the track as is done. Sliding is what a
   machine does when it cannot tell you — it should not go on saying that
   after it can. */
.dsp-bar[data-known] i {
  width: var(--at, 0%);
  animation: none;
  transition: width var(--motion-slower) linear;
}

@keyframes dsp-bar {
  from { transform: translateX(-110%); }
  to   { transform: translateX(320%); }
}

/* List: the library, on the glass.
   No scrollbar-width or scrollbar-color here on purpose: setting either makes
   Chrome render the standard scrollbar and ignore ::-webkit-scrollbar
   entirely, and the bar has to be drawn from pixels like everything else on
   this panel. Pulled right by the display's own padding so the bar sits near
   the glass edge rather than a whole gutter in from it. */
.dsp-list,
.dsp-inputs-rows {
  flex: 1;
  min-height: 0;
  margin-right: -12px;
  padding-right: var(--space-xs);
  overflow-y: auto;
}

/* The inputs screen is the library screen with different rows in it, so the
   scroller and the rows above are shared. Only the line underneath is new:
   the audio path's delay, reported where the thing it describes is chosen. */
.dsp-inputs {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}
.dsp-inputs-note {
  flex: 0 0 auto;
  font-size: var(--type-md);
  letter-spacing: var(--tracking-sm);
  opacity: var(--dim-mid);
}
.dsp-list::-webkit-scrollbar,
.dsp-inputs-rows::-webkit-scrollbar { width: 3px; }
.dsp-list::-webkit-scrollbar-track,
.dsp-inputs-rows::-webkit-scrollbar-track { background: transparent; }
.dsp-list::-webkit-scrollbar-thumb,
.dsp-inputs-rows::-webkit-scrollbar-thumb {
  /* A stack of lit dots, not a bar — the same 3px pitch the wash uses. */
  background-image: repeating-linear-gradient(180deg,
      rgba(var(--lit-rgb), .3) 0 1px, transparent 1px 3px);
}
.dsp-list:hover::-webkit-scrollbar-thumb,
.dsp-inputs-rows:hover::-webkit-scrollbar-thumb {
  background-image: repeating-linear-gradient(180deg,
      rgba(var(--lit-rgb), .5) 0 1px, transparent 1px 3px);
}
/* The list is the thing you read from across a room, so it is set at the
   display's large size rather than its label size. */
.dsp-list .dsp-song,
.dsp-inputs-rows .dsp-song { font-size: var(--type-lg); letter-spacing: var(--tracking-sm); }
.dsp-song {
  position: relative;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2xl);
  padding: 5px 10px 5px 22px;
  cursor: pointer;
}

/* The mark of selection is a pointer in the margin, not a panel behind the
   row. A filled block is a shape drawn over the display; an arrow is
   something the display itself can say. Built from one 2px pixel and its
   shadows, so it is the same resolution as the rest of the glass. */
.dsp-song::before,
.take::before,
.spot::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 50%;
  width: 2px;
  height: 2px;
  margin-top: -5px;
  background: var(--dsp-lit, var(--lit));
  box-shadow:
    0 2px 0 var(--dsp-lit, var(--lit)),  2px 2px 0 var(--dsp-lit, var(--lit)),
    0 4px 0 var(--dsp-lit, var(--lit)),  2px 4px 0 var(--dsp-lit, var(--lit)), 4px 4px 0 var(--dsp-lit, var(--lit)),
    0 6px 0 var(--dsp-lit, var(--lit)),  2px 6px 0 var(--dsp-lit, var(--lit)),
    0 8px 0 var(--dsp-lit, var(--lit));
  opacity: 0;
  transition: opacity var(--motion-fast) var(--ease);
}
/* The pointer marks whichever row is in hand, and a row reached with the
   arrows is as much in hand as one under the cursor. */
.dsp-song[aria-selected="true"]::before,
.take[aria-current="true"]::before,
.take:hover::before,
.take:focus-visible::before,
.spot:hover::before,
.spot:focus-visible::before { opacity: 1; }
/* Two lines tall, so the arrow goes beside the first of them rather than
   floating in the gap between. */
.take::before { top: 11px; margin-top: 0; }
.dsp-song span:last-child { opacity: var(--dim-mid); }

/* Asking, on the glass. The question sits where the song's title would, and
   the answers below it are the list's rows in every respect — same pointer in
   the margin, same arrows to walk them, same OK to commit. Nothing new to
   learn, which is the whole argument for putting it here instead of in a
   window over the app. */
.dsp-confirm {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin: var(--space-2xs) 0 var(--space-xs);
  /* Room between the words and the rim. A proportion rather than a fixed
     figure, because this same panel is drawn at 340px in the takes panel and
     at the width of the whole fascia on the amp — 26px is a margin in one and
     a hairline in the other. Clamped at both ends so the narrow one keeps a
     usable measure and the wide one does not run the text to its edges. */
  /* Vertical padding as well as horizontal. This used to be 0 top and bottom
     and took all its room from the display being 172px tall with the confirm
     stretched to fill it. That works on the amp and nowhere else: drawn in any
     box that sizes to its content, the red frame closed on the words with
     nothing above or below them and read as broken. Its own padding means the
     panel holds its shape wherever it is drawn. */
  padding: var(--space-2xl) clamp(24px, 8%, 64px);
  /* The dropzone's geometry, in the dropzone's place — this is the other panel
     that takes the whole glass for one errand. Solid and red rather than
     dashed and amber: a dashed rim invites a thing to be dropped on it, and
     the one colour on this fascia that has never meant "on" is the one a
     question about destroying something should be framed in. */
  border: 1px solid rgba(var(--dsp-lit-rgb), .34);
  border-radius: var(--radius-md);
}
.dsp-confirm-say { text-align: center; }
/* The one line on this panel that has to be read before anything is pressed,
   so it gets the display's large face — the same 16px the empty panel gives
   its heading. */
.dsp-confirm-title { font-size: var(--type-xl); letter-spacing: var(--tracking-lg); }
/* Bigger than the display's small print. This is the sentence that tells you
   what you are about to lose, and at 8px it was set as a footnote under a
   16px heading — the one line here that has to be read, in the size the panel
   uses for things it does not expect you to. */
.dsp-confirm p {
  margin: 9px 0 0;
  font-size: var(--type-md);
  letter-spacing: var(--tracking-sm);
  line-height: var(--leading-relaxed);
}
/* The bar, when a question turns into a job. Centred under the words it
   replaced, and the only part of this panel that moves. */
.dsp-confirm .dsp-bar { align-self: center; margin-top: var(--space-xl); }
/* Nothing to choose while it runs, and nothing but the way out once it has
   failed — a choice you cannot act on is a key that does nothing. */
.dsp-choices[hidden] { display: none; }
.dsp-choice[hidden] { display: none; }

/* Centred as a block but left-aligned within it, so the pointer has a common
   margin to sit in — two centred rows would put the arrow at a different
   distance from each answer. */
.dsp-choices {
  align-self: center;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}
/* Printed on the glass, in the glass's own face — not a control. The pointer
   in the margin is what says which one is selected; the keys under the display
   are what move it and commit it. */
.dsp-choice {
  position: relative;
  display: block;
  padding: 4px 10px 4px 22px;
  font-size: var(--type-xl);
  letter-spacing: var(--tracking-md);
  text-align: left;
}
/* Second in the stack, so it arrives one step after the first. */
.dsp-choices .dsp-choice:last-child { --row: 1; }
/* The same pixel pointer the library rows use, at the same offset — it is the
   one mark on this display that means "this is the one". */
.dsp-choice::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 50%;
  width: 2px;
  height: 2px;
  margin-top: -5px;
  background: var(--dsp-lit, var(--lit));
  box-shadow:
    0 2px 0 var(--dsp-lit, var(--lit)),  2px 2px 0 var(--dsp-lit, var(--lit)),
    0 4px 0 var(--dsp-lit, var(--lit)),  2px 4px 0 var(--dsp-lit, var(--lit)), 4px 4px 0 var(--dsp-lit, var(--lit)),
    0 6px 0 var(--dsp-lit, var(--lit)),  2px 6px 0 var(--dsp-lit, var(--lit)),
    0 8px 0 var(--dsp-lit, var(--lit));
  opacity: 0;
  transition: opacity var(--motion-fast) var(--ease);
}
.dsp-choice[aria-selected="true"]::before { opacity: 1; }

/* On the takes panel the two answers are pressed, so they are buttons — and a
   button brings a border, a background and a font of its own that all have to
   go before it is printing on the glass again. */
button.dsp-choice {
  border: 0;
  background: none;
  cursor: pointer;
}
/* The pointer marks the resting answer until you are over one of them, and
   then it marks the one you are over. Scoped to this panel: on the fascia the
   arrow means the keyboard selection, and lighting it under the cursor would
   be the display claiming to be clickable when its answers are the keys
   beneath it. */
.panel-screen .dsp-choices:not(:hover):not(:focus-within) .dsp-choice[data-default]::before,
.panel-screen .dsp-choice:hover::before,
.panel-screen .dsp-choice:focus-visible::before { opacity: 1; }
.panel-screen .dsp-choice:focus-visible { outline: none; }


/* ---------- wordmark ---------- */

/* Engraved into the fascia rather than printed on it: a dark cut with a
   hairline of light along its lower edge, which is how a stamped legend
   catches an overhead lamp. */
/* Brushed metal, lit from above, with the same face on the mark and the word.
   The mark is a mask rather than an <img> so it takes the gradient too — an
   image can only be tinted, and a tint cannot carry a highlight. */
.wordmark {
  --wordmark-metal: linear-gradient(180deg, #c2b298 0%, #8a7d6a 38%, #5a5145 68%, #3f382f 100%);
  display: flex;
  align-items: center;
  gap: 13px;
}
/* The mark is the logo inlined, with the same gradient painted inside the SVG
   rather than a CSS mask over a gradient box. The mask version computed
   perfectly — right url, right size, right background — and painted nothing;
   an inline fill has no such ambiguity, and a paint-server fill is the only
   way to get one gradient across both the mark and the word anyway. */
.wordmark-mark {
  width: 26px;
  height: 26px;
  flex: none;
  line-height: 0;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .8));
}
.wordmark-mark svg { display: block; width: 100%; height: 100%; }
/* Off the typescale on purpose: 15px with 5.7px of tracking is a logotype
   drawn once, not a step anyone else should reach for. The scale has no
   15px and should not grow one to hold this. */
.wordmark span {
  font-size: 15px;
  font-weight: 500;
  letter-spacing: .38em;
  text-transform: uppercase;
  background: var(--wordmark-metal);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .8));
}


/* ==========================================================================
   Amp face — the assembly
   --------------------------------------------------------------------------
   The whole instrument on one fascia. Stems and the play key on the left, the
   display across the middle with its own control row beneath, the mixing
   knobs on the right, and the badge in the centre where a real amp puts it.
   ========================================================================== */

.amp {
  /* The width of the two outer columns, and of the play key that sets it. The
     transport and the master are the fascia's two big controls, one at each
     end, and they have to occupy the same amount of panel or the face leans.
     One number, read by both: as an inline style on the key and a hard figure
     on the knob's slot they were free to drift, and had — 128 against 216. */
  --amp-key: 128px;
  /* Where both of those controls put their centre, measured down from the top
     of the row. Not the glass's own centreline, which is 86px: the volume is a
     cap with its legend under it and the transport has the stem keys under it,
     so both modules carry weight below their caps and a pair hung on the
     window's centre read as sitting low in the fascia. */
  --amp-line: 75px;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: var(--space-4xl);
  row-gap: var(--space-xs);
  /* 28px at the ends, not 26. The volume module's box is --dial wide — 183.87px
     in a 128px slot — because the min/mid/max legends hang at the corners of
     that square, outside the ring. So it overhangs its slot by 27.94px a side
     and the fascia has to hold that, not the ring's 10px. At 26px the box
     crossed the window's right edge by 1.9px at every width measured. Nothing
     was clipped, because the art stops 16px short of the box; it is the box
     that has to fit, so that a wider --amp-key cannot turn this into clipping. */
  padding: 20px 28px 18px;
  /* No top border, and the gradient opens on exactly the value .stage-body
     closes on. The line was left over from when this was a separate transport
     bar; with the roll's rim 14px above it, it made three horizontal edges
     inside seventeen pixels and the whole join read as a seam rather than as
     panel. There is one piece of fascia from the header down to the foot of
     the window, with two holes cut in it. */
  background-image:
    var(--grain-tex),
    var(--amp-face);
  background-size: 150px 150px, auto;
  background-blend-mode: multiply, normal;
}

/* Transport and stems share the left column: the key you press most on top,
   the three that qualify it underneath. They are placed as separate grid
   items rather than nested in a wrapper, so the stems land in the same row as
   the select-song key and the grid aligns the two — no gap to keep in step by
   hand. */
/* The right column mirrors the left: the big control on top, the small ones
   on the row beneath. Placed as separate grid items so the boost row lands in
   the same row as the stems and the under-bar, and the grid aligns all three. */
/* Fixed both ways. The boost panel and the volume knob take turns in this
   slot, and a slot that sizes to whichever is showing would resize the display
   beside it every time the panel opened. Whatever goes in here has to fit. */
.amp-volume {
  grid-column: 3;
  grid-row: 1;
  align-self: start;
  /* The play key's width exactly. It was 216px — sized to contain the scale's
     outermost figures — which left dead panel on each side of the knob against
     a transport with none. The knob came down to fit rather than the slot
     going out to hold it. */
  width: var(--amp-key);
  /* What you see the knob as being: the dot ring — cap, dish and marks. It is
     the one figure worth setting by eye, so it is the one that is set, and
     --dial is worked back from it rather than picked and checked.
     20px wider than the slot, which means it overhangs 10px each side into the
     26px column gap. That is panel either way: the two ends of the fascia
     still hold the same slot, which is what keeps the face from leaning. */
  --amp-ring: calc(var(--amp-key) + 20px);
  /* The ring's radius is cap/2 + dish(11) + 5 + the dot's own 1px, and on this
     fascia the cap is .62 of --dial:
         --amp-ring / 2  =  .31 * --dial  +  17px
     rearranged. Change --amp-ring and the cap, the dish, the marks and the
     scale all follow it. */
  --dial: calc((var(--amp-ring) / 2 - 17px) / 0.31);
  /* Onto the shared centreline. The wrap is --dial tall and its ring is centred
     in it, so half of --dial is what has to sit on the line — not half of
     --amp-key. The two were 23px apart before this, which is what a pair of
     hand-tuned margins drifts to the moment either size changes. */
  margin-top: calc(var(--amp-line) - var(--dial) / 2);
  /* Fixed, and the display's height. The settings bank takes this same slot,
     and a slot that sized to whichever of the two was showing would resize the
     glass beside it every time the panel opened. The legend hangs past the
     foot of it into the band the under-bar keys live in; nothing is clipped,
     because this height is what the grid measures rather than the content. */
  height: 172px;
  display: flex;
  align-items: flex-start;
  justify-content: center;
}
/* Measured from the top of the row rather than centred in it: the row is as
   tall as the display, and a key centred in it sagged toward the stem marks
   that were lifted into the space underneath. */
.amp-play {
  grid-column: 1;
  grid-row: 1;
  align-self: start;
  /* The same line, from the same variable — see --amp-line. */
  margin-top: calc(var(--amp-line) - var(--amp-key) / 2);
  display: flex;
  justify-content: center;
}
/* Three keys in a row under the play key, each with its mark engraved over it.
   Radio behaviour is the group's job, not the key's — a key only knows whether
   it is down. */
.stem-select { display: flex; justify-content: center; gap: var(--key-gap);
 grid-column: 1; grid-row: 2;
}

/* ---------- the master meter ---------- */

/* Under the master, in the band the stem keys occupy at the other end of the
   fascia — so the two ends of the panel each have a big control with a row of
   small things beneath it, and the meter sits on the line the stem keys and
   the under-bar are already on.
   A slot machined into the panel, not a bar drawn on it: dark at the bottom of
   a shallow recess, with the segments lit inside it. */
.amp-meter {
  grid-column: 3;
  grid-row: 2;
  justify-self: center;
  align-self: center;
  position: relative;
  /* The slot the master owns, exactly — the knob overhangs this width, the
     housing does not. */
  width: var(--amp-key);
  height: 15px;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, .38);
  box-shadow:
    inset 0 1px 2px rgba(0, 0, 0, .62),
    inset 0 -1px 0 rgba(255, 255, 255, .04),
    0 1px 0 rgba(255, 255, 255, .035);
  /* How far the segments run. Named, because the bar's width, the peak mark's
     position and the colour ramp all have to agree on it. */
  --meter-track: calc(var(--amp-key) - 6px);
}

/* The cells. One ruling, applied to the unlit bed and the lit bar alike, so
   the two are the same row of segments and cannot drift out of register — a
   bar drawn as a solid fill over a segmented bed lights half-cells at its
   leading edge, which reads as a smear rather than as a meter. */
.meter-bed,
.meter-fill {
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  -webkit-mask-image: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 5px);
  mask-image: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 5px);
}
/* Unlit segments, always there. The same ghost the seven-segment readouts
   carry: a display with nothing driven still shows you where its cells are. */
.meter-bed {
  right: 3px;
  background: rgba(var(--lit-rgb), .085);
}
.meter-fill {
  width: calc(var(--meter-track) * var(--level, 0));
  /* Amber for most of the scale, and the last few cells in the one colour on
     this fascia that has never meant "on". Sized to the whole track rather
     than to the bar, so a cell's colour depends on where it is on the scale
     and not on how far the bar happens to have reached. */
  background: linear-gradient(90deg,
    rgba(var(--lit-rgb), .85) 0%,
    rgba(var(--lit-rgb), .95) 64%,
    var(--warn) 86%,
    var(--hot) 100%);
  background-size: var(--meter-track) 100%;
  background-repeat: no-repeat;
}
/* Where it last got to. Not one of the cells and not masked into them: a peak
   mark is a needle laid across the scale, and snapped to the segment ruling it
   would be indistinguishable from the bar's own leading edge.
   Pulled back by its own width, so at full scale it sits flush inside the slot
   rather than over its edge. */
.meter-peak {
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: 2px;
  margin-left: -2px;
  transform: translateX(calc(var(--meter-track) * var(--peak, 0)));
  background: var(--peak);
  opacity: 0;
  transition: opacity var(--motion-slow) var(--ease);
}
.amp-meter[data-live] .meter-peak { opacity: var(--dim-near); }
/* The under-bar stands off the glass. Its keys belong to the display — they
   are what drives it — but sitting hard against the bottom rim they read as
   part of the frame rather than as controls beneath it.
   Put here rather than on the grid's row-gap on purpose: the gap would open
   under the play key and the volume knob too, and neither of those needs it.
   The three row-two items still land together, because they are one grid row
   and this only moves where that row starts. */
.amp-display {
  grid-column: 2;
  grid-row: 1;
  padding-bottom: var(--space-md);
}


/* Under the glass: transport-adjacent controls left, badge centred, monitoring
   right. Three tracks rather than space-between, so the badge stays centred on
   the display however wide the two sides get. */
.amp-under { grid-column: 2; grid-row: 2;

  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-2xl);
}
.amp-under-left { display: flex; align-items: center; gap: var(--key-gap); }
.amp-under-right { display: flex; align-items: center; justify-content: flex-end; gap: var(--key-gap); }
/* Mark then lamp key — exactly the stem keys' arrangement, so the mic reads as
   one more switch on the fascia rather than a labelled exception.
   Its lamp is red, not amber: on a machine that records you, the light that
   means "this is live" has meant red since the tally lamp. */
/* Tighter than --key-gap on purpose: this is a mark bound to its key, not two
   keys side by side. At the group gap the mic mark would sit as far from its
   own key as the settings key does, and stop reading as its legend. */
.amp-mic { display: flex; align-items: center; gap: 5px; }
.amp-mic .btn--radio { --lamp: var(--hot); --lamp-rgb: 207, 47, 24; }

/* Select song is a latch — it holds the display in list mode — so it sits on
   the same key as everything else rather than being a special case. */
.amp-select { width: auto; }
.amp-select .btn-face { padding: var(--space-md) var(--space-2xl); }


/* The settings knobs put their dial to the left with a sentence beside it;
   on the amp face there is no room for a sentence and the legend belongs
   under the cap, so the dial comes back into the flow and the column
   stacks. */
.knob--stack {
  min-height: 0;
  padding-left: 0;
  align-items: center;
  /* The wrap runs a good way below the cap to hold the scale, so a legend
     spaced off the wrap reads as spaced off nothing. It is pulled back up
     across that margin instead — centred, so it passes between the two end
     values rather than through them. */
  gap: 0;
}
.knob--stack > .knob-head { margin-top: calc(var(--dial, 116px) * -.05); }
/* relative, not static. The cap inside is absolutely positioned at top:50%,
   which resolves against the nearest positioned ancestor — with the wrap
   static that was .knob, whose box includes the legend, so the cap sat low in
   its own module and the readout beside it looked misaligned. */
.knob--stack .dial-wrap {
  position: relative;
  top: auto;
  left: auto;
  transform: none;
}
/* Legend over readout, both centred under the cap. */
.knob--stack .knob-head {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
}
.knob--stack .knob-label { text-align: center; }

/* --bare: the ring without its engraved values. On a small secondary knob the
   three numbers are more ink than the scale is worth, and the lit readout
   beside it already says where the knob is. The ticks stay: they are the
   scale, the numbers were only its labels. */
.knob--bare .dial-min,
.knob--bare .dial-mid,
.knob--bare .dial-max { display: none; }


/* Set against the end ticks rather than the corners of the box — the ticks sit
   at 74.7% down — but standing off them. Flush against the dots they read as
   part of the ring rather than as its terminals. */
.knob--tight .dial-min,
.knob--tight .dial-max {
  bottom: auto;
  top: calc(var(--dial, 116px) * .86);
}
.knob--tight .dial-min { left: calc(var(--dial, 116px) * .095); }
.knob--tight .dial-max { right: calc(var(--dial, 116px) * .095); }


/* No width cap. The module is flex: 1 1 0 with a shrinkable meta column, so a
   legend takes one line wherever the row can afford it and breaks only where
   it cannot — which is what makes this section responsive rather than one
   fixed layout with a smaller copy. Capping the width wrapped every legend at
   every size, including the wide one with room to spare. */

/* --tight: cap and readout on one line, legend under the pair, and the scale
   pulled in against the cap. Kept as a modifier rather than folded into
   .knob--stack, because the amp's volume and boost want the plain form —
   changing the base changed every knob in the app, popover included. */
/* Cap, then legend over readout. Both are left-aligned against the same edge,
   so the four knobs line up down a common left margin rather than each
   centring on its own text. */
.knob--tight { gap: 5px;
 flex-direction: row; align-items: center; gap: 11px;
}
/* The column has to be allowed to shrink. Without this it takes its widest
   child as a floor and pushes the module past its share instead.
   Fixed height with the readout at the foot: a legend that wraps to two lines
   was pushing its own readout down, so the four readouts sat at three
   different heights across the bank. Anchored to the bottom of a constant box
   they all line up, and a wrapped legend grows upward into the space a
   one-line legend leaves empty. */
.knob-meta {
  min-width: 0;
  height: 62px;
  justify-content: flex-end;

  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 9px;
  /* The cap fills only 62% of its wrap, so the rest is scale margin. The
     column is pulled back across part of it — as a whole, so the legend and
     the readout stay flush with each other — but not all of it: at -.15 the
     column closed to about 16px of the cap, which is nearer than the ring's
     own numbers sit. */
  margin-left: calc(var(--dial, 116px) * -.06);
}
/* Wrapping, not nowrap: at 1264px the row has to give somewhere, and a legend
   breaking to a second line is cheaper than the caps shrinking or the bank
   overflowing its container. */
.knob--tight .knob-label {
  text-align: left;
  line-height: var(--leading-normal);
}

/* The voice-boost pair reads as one module: its switch over its level. */


/* The dial itself is ui/components/dial. This is only where it hangs: inside a
   knob module, centred on the module's own box. */
.dial-wrap {
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
}
