:root{
  /* FSCLN brand values already defined by the shared design system (/shared/_ds/v1/, linked in
     index.html before this file). --ink, --accent, --accent-dark and --divider are used
     straight from the DS, never re-declared here — see grattern's app.css for why re-declaring
     a var() self-reference doesn't work. The names below alias the DS tokens under this app's
     own, shorter names since the rest of this file reads more clearly with them. */
  --bg: var(--bg-dark);
  --beige: var(--text-primary);
  --sec: var(--text-secondary);
  --ter: var(--text-tertiary);
  --mono: var(--font-mono);
  --display: var(--font-display);

  /* Beige-rail borders. The design system is dark-first and has no light-surface border ramp,
     so these stay app-specific (contract point 4 — declared once, with a reason). */
  --line:#d6d4cd; --line2:#c2bfb6;
  --rail-label:#7a776f;
  --rail-faint:#9d9a92;
  --ink-press:#000;
  --err:#d1372f;
  --err-soft:#e0685c;         /* Go Pro modal validation text — a lighter shade of the same
                                  role as --err, kept distinct rather than unified (same
                                  reasoning grattern's app.css gives for its own copy) */
  --scrim:rgba(10,10,10,.7);   /* #pro/#blocked modal backdrop */

  /* Track/grid tones with no equivalent in the shared system — the sequencer canvas is a
     surface the design system doesn't otherwise describe. */
  --grid-line: rgba(239,238,233,.08);
  --grid-line-bar: rgba(239,238,233,.22);
  --track-bg: rgba(239,238,233,.03);
  --track-bg-assigned: rgba(163,224,82,.05);

  --rail:300px;
  /* Doubled (v4.0.0, was 52px) per explicit request. --label-w shrank in v4.1.0 (296 -> 220px) —
     the dial cluster moved from one long horizontal row into a compact 2-column x 3-row grid (see
     .dialGrid below), which needs far less width than six knobs in a line did; the track number,
     meanwhile, moved out of the label entirely into its own full-height column, sized by
     --numbtn-w. */
  --trackh:104px;
  --numbtn-w:40px;
  --label-w:220px;
  --ctrls-w:108px;
  --header-h:70px;
  /* v5.0.1 — shared by #ruler's own height and #timeline's sticky `top` offset, so the two stay
     in sync by construction instead of two separately-maintained 46px literals. */
  --ruler-h:46px;
}

/* Per-category tones, each a custom property pair rather than a color set inline from JS — the
   CSP here (default-src 'none') has no 'unsafe-inline' in style-src, so every color that varies
   per element has to be a class the stylesheet already knows about, never a `.style.*` write.
   One rule per pitch-register category (see the comment above SOUNDS in app.js), not per sound —
   all 49 sounds across both packs (29 Futuristic, 20 Drumkit) share these same 6 bands, each pack
   sorted low to high by its own spectral centroid: deep < low < mid < upper < high < bright. Add a
   `.tone-<category>` rule here only if a future sound needs a band none of these six cover (max 8
   total — see soundTone() in app.js). */
.tone-deep{--tone:#8b6bff;--tone-tail:rgba(139,107,255,.35)}
.tone-low{--tone:#5ea0ff;--tone-tail:rgba(94,160,255,.35)}
.tone-mid{--tone:#33e0c0;--tone-tail:rgba(51,224,192,.35)}
.tone-upper{--tone:var(--accent);--tone-tail:rgba(163,224,82,.35)}
.tone-high{--tone:#f5c451;--tone-tail:rgba(245,196,81,.35)}
.tone-bright{--tone:#ff5ec8;--tone-tail:rgba(255,94,200,.35)}
*{box-sizing:border-box}
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:var(--bg);color:var(--beige);font-family:var(--display)}
button{font-family:inherit}
#app{display:flex;height:100%;width:100%}

/* ---------- rail ---------- */
/* overflow-y:scroll, not auto (v4.0.0) — auto only reserves the scrollbar's track width once
   content actually overflows, so expanding a sound group that pushes #rail past the viewport
   height made the whole rail's content width visibly jump (everything shifting left by the
   scrollbar's width) right as the visitor was mid-interaction. `scroll` reserves that width
   unconditionally, before it's ever needed, so there's nothing to jump — the tradeoff is an
   always-visible scrollbar track even on a short sounds list, which reads as more consistent than
   a track that pops in and out. */
#rail{width:var(--rail);flex:0 0 var(--rail);height:100%;background:var(--beige);color:var(--ink);border-right:1px solid var(--line2);display:flex;flex-direction:column;overflow-y:scroll}
/* #rail is flex column only so #foot can pin itself to the bottom with margin-top:auto — every
   *other* direct child must stay flex-shrink:0, or once the sounds list (49 rows across two
   groups, and counting) makes #rail's total content taller than the viewport, the flex algorithm
   shrinks whichever child has the most slack between its own content height and its specified
   height to claw back space, rather than just letting #rail's own overflow-y:auto scroll. #head
   was exactly that child (a 70px header with ~30px of actual content) — it silently collapsed
   toward 30px instead of scrolling, pulling it out of alignment with #transportBar. */
#rail>*{flex-shrink:0}
/* height:var(--header-h), not padding-derived — #transportBar (in the stage column) matches
   this exactly so its border-bottom lands at the same Y as this rail's first section's
   border-top, reading as one continuous line across the whole app (requested explicitly). */
#head{height:var(--header-h);display:flex;align-items:center;gap:12px;padding:0 20px}
#head img{height:30px;width:auto;display:block}
/* 26px (v3.6.0, was 15px) to visually match the logo mark's own 30px height — #sub ("Soundboard
   Sequencer") was removed the same release rather than kept underneath at the bigger size, since
   there's no longer a second line for it to sit on without pushing #head past --header-h. */
#name{font-family:var(--display);font-weight:600;font-size:26px;letter-spacing:.01em;line-height:1.1}
/* v5.0.0 — shown while an entered unlock code (full or demo) still grants something; see
   updateProTag() in app.js. margin-left:auto pushes it to the opposite end of #head's own flex
   row from the logo/name, i.e. the header's top-right corner. */
#proTag{margin-left:auto;font-family:var(--mono);font-size:9px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--ink);background:var(--accent);padding:4px 9px;border-radius:999px}
#proTag[hidden]{display:none}
section{padding:16px 20px;border-top:1px solid var(--line)}
h2{margin:0 0 14px;font-family:var(--display);font-size:19px;font-weight:500;letter-spacing:-.01em;line-height:1.1;color:var(--ink)}
.row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.row+.row{margin-top:10px}
.spread{justify-content:space-between}
.lbl{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--rail-label)}
.val{font-family:var(--mono);font-size:12px;color:var(--ink);font-variant-numeric:tabular-nums}
/* Restored v3.7.0 — BPM went back to a slider (see #bpmRange in index.html), just indexing the
   fixed BPM_PRESETS array instead of a raw number, so this rule is unchanged from before v3.6.0
   dropped it. Nothing else in this app uses a range input. */
input[type=range]{flex:1 1 auto;min-width:0;accent-color:var(--ink);width:100%}
/* The Name field (v3.7.0) — plain text input styled to match the rail's other controls, sitting
   above BPM. maxlength="20" in the markup is the hard character cap; app.js's own input handler
   sanitizes on every keystroke (spaces to underscores, disallowed characters stripped) so this
   never needs to show a validation error. */
/* flex:1 1 auto (was width:100%, v5.1.0) — the field now shares its row with #resetAppBtn (the
   old standalone "Project" h2 was removed in the same change), so it needs to eat the remaining
   row width rather than claim the whole row and push the button onto its own line, the same
   flex:1 1 auto;min-width:0 trick .trackLabel .name already uses next to its own trailing
   controls. */
.projectNameInput{flex:1 1 auto;min-width:0;box-sizing:border-box;font-family:var(--mono);font-size:12px;letter-spacing:.02em;color:var(--ink);background:transparent;border:1px solid var(--line2);border-radius:4px;padding:7px 10px;outline:none;transition:border-color .16s}
.projectNameInput::placeholder{color:var(--rail-faint)}
.projectNameInput:focus{border-color:var(--ink)}
/* v5.0.0: the name is now required before any export — ensureProjectName() in app.js toggles this
   when an export is attempted with the field still empty, focusing it at the same time. */
.projectNameInput.invalid{border-color:var(--err)}
/* v5.1.0 — resetWholeApp()'s own button, sitting next to the Name field now that the "Project" h2
   is gone. Rail-toned like .soundPlay (transparent/var(--line2)/var(--rail-label)), not the
   stage's dark .rowToolBtn look, since this lives in the rail; turns --err on hover rather than
   --accent, the same "this one is destructive" treatment #clearCanvasBtn/#trackClearBtn already
   use, since resetWholeApp() clears every canvas and the Track at once, behind its own
   window.confirm(). */
.resetAppBtn{flex:0 0 auto;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border:1px solid var(--line2);border-radius:4px;background:transparent;color:var(--rail-label);cursor:pointer;transition:border-color .16s,color .16s}
.resetAppBtn:hover{border-color:var(--err);color:var(--err)}
.resetAppBtn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.hint{margin:10px 0 0;font-family:var(--mono);font-size:9px;letter-spacing:.06em;color:var(--rail-label)}
#foot{margin-top:auto;padding:14px 20px 18px;border-top:1px solid var(--line);font-family:var(--mono);font-size:10px;letter-spacing:.08em;color:var(--rail-label)}

/* ---------- sound list ---------- */
.soundList{display:flex;flex-direction:column;gap:6px}
.sound{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--line2);border-radius:4px;background:transparent;cursor:grab;user-select:none;transition:border-color .16s,background .16s}
.sound:hover{border-color:var(--ink);background:rgba(32,30,31,.04)}
.sound:active{cursor:grabbing}
.sound.dragging{opacity:.4}
.soundDot{width:10px;height:10px;border-radius:50%;flex:0 0 auto;background:var(--tone)}
.soundName{font-family:var(--mono);font-size:11px;letter-spacing:.04em;color:var(--ink);flex:1 1 auto}
.soundPlay{width:22px;height:22px;flex:0 0 auto;display:flex;align-items:center;justify-content:center;border:1px solid var(--line2);border-radius:50%;background:transparent;color:var(--ink);cursor:pointer}
.soundPlay:hover{border-color:var(--ink)}
.soundPlay svg{width:9px;height:9px;fill:currentColor}

/* One .soundGroup per sound pack (v3.6.0) — the whole header is a single clickable button
   (.soundGroupBtn), not a text label sitting next to a separate small circular toggle button, per
   the explicit "make the sounds collection a button" request. v3.7.0 added a border around the
   button itself (bordered pill, not the borderless text-row v3.6.0 shipped) plus a per-pack icon
   (.soundGroupIcon, one path set per pack inlined directly in index.html) ahead of the title. The
   +/- glyph (.groupToggleIcon) stays a bare stroked path with no circle/border of its own —
   "keep the plus and minus icon (without the outline)" was specifically about that glyph, not the
   button around it. Both packs start collapsed (index.html's `hidden`/`aria-expanded="false"`/
   plus-path default) — see the sound-group toggle binding in app.js. */
.soundGroup+.soundGroup{margin-top:10px}
.soundGroupBtn{display:flex;align-items:center;justify-content:space-between;width:100%;margin:0 0 10px;padding:8px 10px;border:1px solid var(--line2);border-radius:6px;background:transparent;cursor:pointer;transition:border-color .16s}
.soundGroupBtn:hover{border-color:var(--ink)}
.soundGroupLabel{display:flex;align-items:center;gap:8px;min-width:0}
.soundGroupIcon{width:16px;height:16px;flex:0 0 auto;stroke:var(--rail-label);fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
.soundGroupTitle{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--rail-label)}
.groupToggleIcon{width:14px;height:14px;flex:0 0 auto;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round}
.soundGroupBody[hidden]{display:none}

/* ---------- stage ---------- */
#stage{position:relative;flex:1 1 auto;height:100%;display:flex;flex-direction:column;background:var(--bg);overflow:hidden}
/* space-between (v4.1.0, was flex-end) now that #transportBar holds two clusters — #masterControls
   on the left, everything that used to be the whole bar wrapped into #transportRight on the
   right (its own gap:10px replaces what used to live on #transportBar itself, since space-between
   already spaces the two clusters apart from each other). */
#transportBar{height:var(--header-h);box-sizing:border-box;flex:0 0 auto;display:flex;justify-content:space-between;align-items:center;padding:0 24px;border-bottom:1px solid var(--divider)}
#transportRight{display:flex;align-items:center;gap:10px}
/* Master volume + LED meter (v4.1.0) — top-left of the stage, opposite the transport buttons.
   One .dialCell (the same knob+value wrapper every per-track dial uses, see .dialGrid below)
   plus a bank of LED segments — see #masterVolKnob/tickMeter() in app.js for what drives them. */
#masterControls{display:flex;align-items:center;gap:14px}
#masterVolKnob{width:26px;height:26px}
.ledMeter{display:flex;gap:3px;align-items:flex-end}
.led{width:5px;height:16px;border-radius:2px;background:rgba(239,238,233,.12);transition:background .06s}
/* 6 green / 3 yellow / 1 red, the standard VU-meter zone split — colors match the design system's
   own --accent/high-tone/err rather than inventing new ones. */
.led:nth-child(-n+6).lit{background:var(--accent)}
.led:nth-child(n+7):nth-child(-n+9).lit{background:#f5c451}
.led:nth-child(10).lit{background:var(--err)}
/* v5.0.0: #trackTransportBar (the Track section's own duplicated transport row) shares every one
   of these button looks with #transportBar — same component, same rules, just also selected by
   its id so the two rows can never visually drift apart. */
#transportBar button,#trackTransportBar button{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;border-radius:4px;border:1px solid var(--divider);background:transparent;color:var(--beige);height:32px;padding:0 14px;cursor:pointer;transition:border-color .16s,color .16s,background .16s,transform .16s}
#transportBar button:hover,#trackTransportBar button:hover{border-color:var(--accent);color:var(--accent)}
#transportBar button:active,#trackTransportBar button:active{transform:scale(.98)}
#transportBar button.on,#trackTransportBar button.on{background:var(--accent);border-color:var(--accent);color:var(--ink)}
#transportBar button.on:hover,#trackTransportBar button.on:hover{background:var(--accent-dark);border-color:var(--accent-dark);color:var(--ink)}
#transportBar button.pri,#trackTransportBar button.pri{background:var(--accent);border-color:var(--accent);color:var(--ink)}
#transportBar button.pri:hover,#trackTransportBar button.pri:hover{background:var(--accent-dark);border-color:var(--accent-dark)}
#transportBar svg,#trackTransportBar svg{width:12px;height:12px;flex:0 0 auto;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
#playBtn svg,#trackPlayBtn svg{fill:currentColor;stroke:none}
/* Play and Repeat get the same fixed width (sized to fit "Repeat", the longer of the two labels)
   instead of each sizing to its own text — without this, Play toggling between "Play" and
   "Pause" (different string lengths) visibly resized the button on every click. */
#playBtn,#repeatBtn,#trackPlayBtn,#trackRepeatBtn{width:96px;justify-content:center}
/* Repeat is a persistent toggle, not a momentary action like Play/Export, so it keeps a green
   outline even in its "off" state instead of falling back to the same neutral grey border every
   other transport button uses — the two states read as "on, filled" vs "off, outlined", never
   "off, looks disabled". */
#repeatBtn,#trackRepeatBtn{border-color:var(--accent);color:var(--accent)}
#repeatBtn:hover,#trackRepeatBtn:hover{border-color:var(--accent-dark);color:var(--accent-dark)}
#repeatBtn.on,#trackRepeatBtn.on{background:var(--accent);border-color:var(--accent);color:var(--ink)}
#repeatBtn.on:hover,#trackRepeatBtn.on:hover{background:var(--accent-dark);border-color:var(--accent-dark);color:var(--ink)}
.transportDivider{width:1px;height:20px;background:var(--divider);flex:0 0 auto}
/* Momentary and destructive, like the per-track Clear button — not a persistent toggle like
   Repeat, so it stays the plain transport button look and only turns --err on hover. Sits
   leftmost in the bar, ahead of the Clear/Play divider, per an explicit design call. */
#clearCanvasBtn:hover,#trackClearBtn:hover{border-color:var(--err);color:var(--err)}
/* "Soundboard:" labels the icon-only export/import pair that follows it (v3.6.0 — both buttons
   used to carry their own text; now the label sits once, in front, and the buttons carry only
   their icon plus a title tooltip for accessibility). */
.transportLabel{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--sec);flex:0 0 auto}
/* A square icon-only transport button — same 32px height as every other transport button, width
   matched to it instead of sized to text, and no gap since there's no label to space from the
   icon any more. v5.1.0 added #trackTransportBar to this selector (was #transportBar alone) —
   #trackImportProjectBtn/#trackRewindBtn are icon-only buttons in that row too and were missing
   this square treatment, falling back to the same text-sized padding as a labeled button. */
#transportBar button.iconOnly,#trackTransportBar button.iconOnly{width:32px;padding:0;justify-content:center;gap:0}

/* v5.0.1 — no top padding (was 18px): #rulerRow is sticky and opaque, but that top padding sat
   *above* it, outside its own box, so it was never actually covered by anything solid — as you
   scrolled, the track rows underneath visibly scrolled up through that gap before disappearing
   behind the sticky row, reading as a flickering transparent band right under #transportBar.
   Zeroing it removes the gap outright rather than just recoloring it. */
#gridScroll{flex:1 1 auto;overflow:auto;padding:0 24px 18px;background:var(--bg)}
#gridInner{position:relative;width:100%}
/* #rulerRow is the sticky header band itself. .rulerGutter is a plain spacer exactly as wide as
   a track row's trackNumBtn + trackLabel columns together, so #ruler's bars still line up under
   the grid below regardless of what does or doesn't occupy that space up here. #trackTools (the
   copy/paste pair for the *selected* track — see copySelectedTrack()/pasteSelectedTrack() in
   app.js) moved to the opposite end in v4.1.0 — the same width as .trackCtrls (--ctrls-w),
   sitting directly above the move/clear/delete stack every row already has there, so the two
   "acts on a track" button clusters read as one column instead of opposite ends of the app. */
#rulerRow{display:flex;position:sticky;top:0;z-index:3;background:var(--bg)}
.rulerGutter{flex:0 0 calc(var(--numbtn-w) + var(--label-w));display:flex;align-items:center;padding:3px 4px 3px 0}
/* v5.0.0 — the Q/W/E/R/T/Y canvas-switch buttons, moved here from a footer row below the tracks
   into the actual top-left corner of the grid (the first row/column of "the table"). 6 buttons at
   the old 44x34px gridFoot size don't fit this gutter's width, so they're a 3x2 grid instead,
   stretched to fill the gutter in both dimensions — #ruler grew from 38 to 46px to give two rows
   of button room without feeling cramped. */
.canvasGrid{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,1fr);gap:3px;width:100%;height:100%}
#ruler{height:var(--ruler-h);display:flex;flex:1 1 auto}
#trackTools{flex:0 0 var(--ctrls-w);display:flex;align-items:center;justify-content:center;gap:4px}
/* Bars are an equal flex share of the ruler's width, not a width computed from step count —
   BARS is fixed (app.js), so a bar is always exactly 1/BARS of the total regardless of the
   Steps resolution, and the grid below divides the same total width by its own (larger) step
   count via `1fr` columns. Both end up 100% of the available width without either needing a
   `.style.width` write (this app's CSP has no 'unsafe-inline' in style-src). */
.rulerBar{position:relative;flex:1 1 0;display:flex;align-items:flex-end;padding-bottom:4px;padding-left:4px;font-family:var(--mono);font-size:9px;letter-spacing:.08em;color:var(--ter);border-left:1px solid var(--grid-line-bar)}

/* Copy/paste a whole section (every track's hits in this one bar) — shown on hovering the bar's
   own ruler number, not a track row (see .rowTools above for the per-track version, which this
   sits alongside rather than replaces). Same hover-only, pointer-events-scoped-to-the-buttons
   pattern as .rowTools, just anchored top-right of a 38px-tall .rulerBar instead of vertically
   centered over a 52px track row — there's no room to also center these without covering the
   bar number at the bottom-left. */
.rulerTools{position:absolute;top:4px;right:4px;display:flex;gap:4px;opacity:0;pointer-events:none;transition:opacity .14s;z-index:4}
.rulerBar:hover .rulerTools{opacity:1}
/* v3.5.1 doubled these to 40px ("twice as big"); a later request asked for the opposite —
   matched to .rowToolBtn's own size, not bigger than it, since having two different copy/paste
   button sizes in the same app read as inconsistent rather than intentional. Same 30px size, same
   16px icon, same --bg-mid/--divider-strong/--beige contrast treatment as .rowToolBtn below;
   #ruler stepped down from 48 to 38px (30px button + 4px top/bottom) to match. */
.rulerToolBtn{pointer-events:auto;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border:1px solid var(--divider-strong);border-radius:4px;background:var(--bg-mid);color:var(--beige);cursor:pointer;transition:border-color .16s,color .16s}
.rulerToolBtn:hover{border-color:var(--accent);color:var(--accent)}
.rulerToolBtn:disabled{opacity:.4;cursor:default}
.rulerToolBtn:disabled:hover{border-color:var(--divider-strong);color:var(--beige)}
.rulerToolBtn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}

/* ---------- playhead dot (restored v3.7.0 — the vertical line stays removed) ---------- */
/* Sits between the ruler and the tracks, same inset as both (#ruler's own left/right edges —
   #timeline reuses that inset rather than needing its own variables; the left side now accounts
   for --numbtn-w too, v4.1.0, matching .rulerGutter's own width now that the track number moved
   into its own column ahead of the label). No #timelineLine/#timelineTracksWrap any more: v3.6.0
   removed the line along with the dot, and only the dot came back, so there's no vertical element
   requiring the tracks column to be its own positioning context this time.
   v5.0.1 — now sticky too (`top:var(--ruler-h)`, docking directly under #rulerRow's own sticky
   band), so the playhead stays visible while scrolling through 8 tracks instead of scrolling away
   with them. The inset moved from `margin` to `padding` specifically for this: margin sits
   *outside* the border box, so a sticky element's own background never covers it — the gutters on
   either side of the thin #timelineBar would stay unpainted and let scrolled track rows show
   through exactly like the #gridScroll gap this same release fixed above. `padding` keeps the
   border box (and so the background painted on it) full width, with the bar/dot still visually
   inset by the same amount via the padding instead. */
#timeline{position:sticky;top:var(--ruler-h);z-index:2;background:var(--bg);padding:12px var(--ctrls-w) 12px calc(var(--numbtn-w) + var(--label-w))}
#timelineBar{position:relative;height:4px;border:1px solid var(--accent);border-radius:999px}
/* Draggable (see bindTimelineDrag() in app.js) — cursor and a larger hit area invite it; the
   visible dot stays 10px, the invisible padding just widens what you can grab. */
#timelineDot{position:absolute;top:50%;left:0;width:10px;height:10px;border-radius:50%;background:var(--accent);transform:translate(-50%,-50%);box-shadow:0 0 6px rgba(163,224,82,.6);cursor:grab;touch-action:none;z-index:6}
#timelineDot::before{content:'';position:absolute;inset:-8px}
#timelineDot:active{cursor:grabbing}

#tracks{display:flex;flex-direction:column;gap:6px}
.track{position:relative;display:flex;align-items:stretch;height:var(--trackh)}
/* A solid, filled outline — deliberately not the dashed one .dragOver uses below, so "this row
   is selected" never reads as "drop here". Click the track's own name (or its number button —
   see .trackNumBtn) to toggle (bindRowSelect()/its own click handler in app.js); purely a visual
   marker, independent of the copy/paste clipboard. */
.track.selected{outline:3px solid var(--accent);outline-offset:-3px;border-radius:4px}
/* The track number (1-TRACK_COUNT) is its own full-height button now (v4.1.0), the leftmost
   column of every row — moved out of .trackLabel entirely after a report that the old small
   badge (.trackNum) was too easy to miss once a sound was assigned. Styled like .canvasBtn
   (Q/W/E/R, below) rather than invented fresh: same font treatment, same "filled solid when
   active/selected" pattern, just full row height instead of --header-h. Always shown and always
   clickable, assigned or not, since it's what the 1-TRACK_COUNT keyboard shortcuts refer to. */
.trackNumBtn{flex:0 0 var(--numbtn-w);height:100%;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:15px;font-weight:600;letter-spacing:.02em;border:1px solid var(--divider);border-radius:4px 0 0 4px;background:transparent;color:var(--sec);cursor:pointer;transition:border-color .16s,color .16s,background .16s}
.trackNumBtn:hover{border-color:var(--accent);color:var(--accent)}
.trackNumBtn.on{background:var(--accent);border-color:var(--accent);color:var(--ink)}
.trackNumBtn.on:hover{background:var(--accent-dark);border-color:var(--accent-dark);color:var(--ink)}
/* No justify-content:space-between — with name, the mute button, and a 2x3 dial grid as its
   three children, that would put equal space in every gap, so the dial grid's position would
   drift depending on how long the sound name happened to be. `.name` taking the flexible
   remaining space instead pins the mute button + dial grid to the right edge every time. */
/* No overflow:hidden here (v5.0.0, had one before) — .name below already clips/ellipsizes its own
   text, so trackLabel's own overflow was only ever needed as a belt-and-suspenders backstop. It
   turned out to be actively harmful: every knob's hover tooltip (`[data-label]::after`) pops
   *above* the knob, and the dial grid's top row (Volume/Pitch) sits close enough to trackLabel's
   own top edge that its tooltip rendered past that edge — right where this overflow clipped it,
   silently, with nothing in the console to explain why. Rows 2/3 had enough room below the top
   edge to not be affected, which is why only "the top 2 knobs" were ever reported as invisible. */
.trackLabel{flex:0 0 var(--label-w);display:flex;align-items:center;gap:8px;padding:0 10px;background:var(--track-bg);font-family:var(--mono)}
/* Clicking the label selects the track (bindRowSelect() in app.js) — v4.0.0 bound on every row
   now, assigned or not (the 1-TRACK_COUNT keyboard shortcuts select by position regardless of
   assignment, so a click needs to match), not just when a sound is present. cursor:pointer so it
   reads as clickable rather than as selectable text, and user-select:none so a fast click can't
   also highlight the name. */
.trackLabel{cursor:pointer;user-select:none}
.trackLabel .name{flex:1 1 auto;min-width:0;font-size:11px;letter-spacing:.04em;color:var(--beige);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.trackLabel .empty{font-size:9px;letter-spacing:.06em;color:var(--ter);text-transform:uppercase}
.track.assigned .trackLabel{background:var(--track-bg-assigned)}
/* Mute (v4.1.0) — a per-track speaker toggle (bindMuteBtn() in app.js), sitting between the name
   and the dial grid. .muteX (the diagonal slash) is always in the markup; hidden here unless the
   button carries .on, so muting is a plain class toggle, never an innerHTML rewrite. Filled --err
   (not --accent) when on, since "muted" is closer in meaning to the other --err-toned controls
   (Clear canvas on hover, the trash button) than to a persistent-and-neutral toggle like Repeat. */
/* v5.0.0 — a vertical per-track VU meter stacked above the mute button (tickTrackMeters() in
   app.js), same LED-bank treatment as the master meter (.ledMeter above) just oriented vertically
   and narrower (5 segments, not 10) to fit this column. column-reverse puts the first <span> at
   the bottom, so "more lit" reads as "fills up" the same way a vertical level bar should, while
   .lit is still toggled on segments 0..n in plain DOM order, same as .ledMeter. */
.trackMeterCol{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;gap:4px}
.trackLedMeter{display:flex;flex-direction:column-reverse;gap:2px}
.trackLedMeter .led{width:12px;height:5px;border-radius:2px;background:rgba(239,238,233,.12);transition:background .06s}
.trackLedMeter .led:nth-child(-n+3).lit{background:var(--accent)}
.trackLedMeter .led:nth-child(4).lit{background:#f5c451}
.trackLedMeter .led:nth-child(5).lit{background:var(--err)}
.muteBtn{flex:0 0 auto;width:22px;height:22px;display:flex;align-items:center;justify-content:center;border:1px solid var(--divider);border-radius:4px;background:transparent;color:var(--sec);cursor:pointer;transition:border-color .16s,color .16s,background .16s}
.muteBtn:hover{border-color:var(--err);color:var(--err)}
.muteBtn.on{background:var(--err);border-color:var(--err);color:var(--beige)}
.muteBtn svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.muteBtn:not(.on) .muteX{display:none}

/* Pitch knob — rotation is driven by a paused Web Animations API animation whose currentTime is
   scrubbed directly (see bindPitchKnob() in app.js), never `element.style.transform`. */
.pitchKnob{flex:0 0 auto;width:22px;height:22px;border-radius:50%;border:1px solid var(--line2);background:var(--beige);position:relative;cursor:ns-resize;touch-action:none}
.pitchKnob:hover{border-color:var(--ink)}
.knobDial{position:absolute;inset:0;border-radius:50%}
.knobDial::after{content:'';position:absolute;top:2px;left:50%;width:2px;height:7px;background:var(--ink);border-radius:1px;transform:translateX(-50%)}
/* Every knob's own value readout, wrapped together with it in one .dialCell (v4.1.0 — see
   dialCellHtml() in app.js). Below the knob rather than to its side now, which is what makes a
   compact 2-column dial grid possible at all — six knobs in a row each with a value to their
   right needed far more horizontal room than this app's track rows have to spare. */
.dialGrid{display:flex;flex-direction:column;gap:3px}
.dialRow{display:flex;gap:8px}
.dialCell{width:30px;display:flex;flex-direction:column;align-items:center;gap:1px}
.dialVal{font-size:8px;color:var(--rail-label);font-variant-numeric:tabular-nums;line-height:1}
/* A short floating tooltip naming the control — added v3.7.0, shared by every knob via the
   `data-label` attribute each one carries (set once in renderTracks()'s template, in app.js).
   Names only — the live numeric reading is .dialVal above, not this tooltip — a pure-CSS
   `content: attr()` tooltip needs no JS positioning logic and nothing to keep in sync as a value
   changes. */
[data-label]{position:relative}
[data-label]::after{content:attr(data-label);position:absolute;bottom:100%;left:50%;transform:translateX(-50%);margin-bottom:6px;background:var(--bg-mid);color:var(--beige);font-family:var(--mono);font-size:9px;letter-spacing:.04em;text-transform:uppercase;padding:3px 7px;border-radius:4px;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .12s;z-index:10}
[data-label]:hover::after{opacity:1}
/* Per-track effect knobs — same WAAPI-driven rotation mechanism as .pitchKnob (bindEffectKnobs()
   in app.js), just smaller and each carrying its own border color so the four are distinguishable
   at a glance. Colors are deliberately distinct from both the six .tone-* sound-category colors
   and the volume knob's red, so nothing here can be confused with a sound's own color or the
   volume control. */
.fxKnob{flex:0 0 auto;width:16px;height:16px;border-radius:50%;border:2px solid var(--line2);background:var(--beige);position:relative;cursor:ns-resize;touch-action:none}
.fxKnob:hover{border-color:var(--ink)}
.fxKnob-echo{border-color:#5ea0ff}
.fxKnob-reverb{border-color:#8b6bff}
.fxKnob-bitcrush{border-color:#33e0c0}
.fxKnob-distortion{border-color:#ffd23f}
/* background:transparent (v4.1.0, was a faint track-wide tint) — cells now carry their own
   individual background (see .cell below) and the grid itself has a `gap`, so the stage's own
   black (--bg) shows through between them, reading as a row of discrete, pressable buttons
   rather than one continuous lane. */
.trackGrid{flex:1 1 auto;display:grid;height:100%;background:transparent;gap:3px}
.track.rejecting .trackLabel{background:rgba(209,55,47,.15)}
.track.dragOver .trackLabel,.track.dragOver .trackGrid{outline:2px dashed var(--accent);outline-offset:-2px}
/* `1fr` columns, not a fixed pixel width — the grid always fills 100% of the row's remaining
   width (after the label and controls columns), matching the ruler's own flex-share bars above.
   Only one possible value since v3.7.0 removed the Steps selector (BARS=6, BEATS_PER_BAR=4,
   GRID_SPB=1 — see app.js) — still a static class rather than the bare `.trackGrid` selector, so
   the pattern stays "add a `.trackGrid.steps-N` rule" if GRID_SPB/BARS ever change, the same as
   when this app did have more than one possible value. A static class beats a
   `.style.gridTemplateColumns` write under this app's CSP either way. */
.trackGrid.steps-24{grid-template-columns:repeat(24,1fr)}

/* Every cell is its own individually-rounded, individually-backgrounded button now (v4.1.0) —
   previously only a *hit* cell had rounded corners, and only on its left side (border-radius:3px
   0 0 3px), with the border-left grid lines doing the job of separating cells visually. Now that
   .trackGrid itself has a `gap` (above), the gap is what separates cells, so the border-left grid
   lines are gone too — .cell.barStart marks a bar boundary with an inset box-shadow instead,
   which (unlike a border) doesn't fight with the gap for space. */
.cell{position:relative;background:var(--track-bg);border-radius:4px;cursor:default}
.cell.barStart{box-shadow:inset 2px 0 0 var(--grid-line-bar)}
.track.assigned .cell{cursor:pointer;background:var(--track-bg-assigned)}
.cell:hover{filter:brightness(1.3)}
/* A hit is the cell itself, not an overlay positioned with `.style.left` — no ::after "decay
   tail" bleeding into the next cell any more (removed v4.1.0, on request); every cell keeps the
   exact same uniform rounding whether it's hit or not, so painting a hit never changes a cell's
   own shape, only its color. Qualified as `.trackGrid .cell.hit`, not the bare `.cell.hit` —
   `.track.assigned .cell` above is also three selectors deep (.track + .assigned + .cell), so a
   bare two-selector `.cell.hit` loses the specificity tie and a hit cell on an assigned track
   would silently render as the plain assigned-track background instead of its own tone color. */
.trackGrid .cell.hit{background:var(--tone);cursor:pointer}
.cell.hit:hover{filter:brightness(1.15)}
/* v3.6.0 removed the moving playhead dot/line entirely in favor of this — igniteHits() in app.js
   adds .playing to a hit cell the instant its step's audio actually starts (against
   audioCtx.currentTime, same clock the scheduler itself uses) and removes it again when that
   note's own duration has elapsed, so the "flash" is exactly as long as the note. Only ever
   applied alongside .hit, so --tone/--tone-tail (set by that cell's own .tone-<category> class)
   are already in scope — no separate color to keep in sync. A plain box-shadow needs no
   animation/transition of its own for this. */
.cell.playing{box-shadow:0 0 0 2px var(--tone),0 0 16px 5px var(--tone-tail);z-index:2}

/* .trackCtrls holds only the vertical icon stack now (v4.0.0) — the volume knob moved out into
   .trackLabel's own dial grid, so this column no longer needs to split its width between a fader
   and a button grid. Always visible now, assigned or not — reordering (the drag handle) is
   meaningful on an empty track too, now that the row count is fixed at TRACK_COUNT and reordering
   is the only way to change which position an empty slot sits in. */
.trackCtrls{flex:0 0 var(--ctrls-w);display:flex;align-items:center;justify-content:center;padding-left:4px}
/* Vertical stack, top to bottom: move (reorder), clear (hits only), delete (resets the whole
   track back to unassigned — see resetTrack() in app.js, shared with the Backspace keyboard
   shortcut; it can no longer remove the row itself, since TRACK_COUNT is fixed). height:100% +
   space-between spreads the three buttons across the full (now doubled) track height rather than
   clustering them — "all together the same height as the track bar" was explicit. */
.trackCtrlButtons{display:flex;flex-direction:column;align-items:center;justify-content:space-between;height:100%;padding:6px 0}
/* Matched to .rowToolBtn's own dimensions/weight (v4.1.0, was 22px/1px/transparent) — #trackTools
   sits directly above this stack now (see #rulerRow in index.html), so the two "acts on a track"
   button clusters read as one column, one visual weight, not two different button styles
   stacked on top of each other. */
.ctrlBtn{width:30px;height:30px;display:flex;align-items:center;justify-content:center;border:1px solid var(--divider-strong);border-radius:4px;background:var(--bg-mid);color:var(--beige);box-shadow:var(--shadow-md);cursor:pointer;transition:border-color .16s,color .16s}
.ctrlBtn:hover{border-color:var(--accent);color:var(--accent)}
.ctrlBtn.remove:hover{border-color:var(--err);color:var(--err)}
.ctrlBtn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round}
.dragHandle{cursor:grab}
.dragHandle:active{cursor:grabbing}
.track.dragging{opacity:.5}

/* Volume knob — the rotated native <input type=range> it replaced had unreliable hit-testing
   (a rotated form control's pointer/click box doesn't rotate with it in every browser), which is
   what "not functioning" meant in practice. Same WAAPI-driven-rotation mechanism as .pitchKnob
   (bindVolumeKnob() in app.js), just red so it reads as a distinct control from pitch/effects.
   Sits in .trackLabel's own dial grid now (v4.1.0, first in reading order — swapped ahead of
   Pitch on request) — see .dialGrid above; also reused as-is for the master volume knob
   (#masterVolKnob in index.html), just sized up a little there. */
.volumeKnob{width:20px;height:20px;border-radius:50%;border:2px solid #ff5e5e;background:var(--beige);position:relative;cursor:ns-resize;touch-action:none}
.volumeKnob:hover{border-color:#ff3b3b}

/* #trackTools' own two buttons (v4.0.0) reuse .rowToolBtn's exact look — the higher-contrast
   --bg-mid/--divider-strong/--beige treatment a first pass found nearly invisible against the
   dark stage at --bg. Static, always-visible flex children of #trackTools now (see #rulerRow
   above), not absolutely positioned or hover-gated the way the old per-row floating buttons this
   replaced were — there's exactly one copy and one paste button in the whole app now, not one
   pair per row, so there's nothing to show/hide based on hover any more. */
.rowToolBtn{width:30px;height:30px;display:flex;align-items:center;justify-content:center;border:1px solid var(--divider-strong);border-radius:4px;background:var(--bg-mid);color:var(--beige);cursor:pointer;box-shadow:var(--shadow-md);transition:border-color .16s,color .16s}
.rowToolBtn:hover{border-color:var(--accent);color:var(--accent)}
.rowToolBtn:disabled{opacity:.4;cursor:default}
.rowToolBtn:disabled:hover{border-color:var(--divider-strong);color:var(--beige)}
.rowToolBtn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}

/* v5.0.0: lives in .canvasGrid (the ruler gutter's top-left corner) now, not a footer row below
   the tracks — sized to fill its own grid cell rather than a fixed 44x34px, since the gutter's
   available space is what's fixed here, not the button. */
.canvasBtn{width:100%;height:100%;font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:.06em;border:1px solid var(--divider);background:transparent;color:var(--sec);border-radius:4px;cursor:pointer;transition:border-color .16s,color .16s,background .16s}
.canvasBtn:hover{border-color:var(--accent);color:var(--accent)}
/* The active canvas is filled solid, exactly like every other persistent on/off toggle in this
   app (#repeatBtn, a .soundGroupBtn's own state) — always exactly one of the four is .on, never
   zero or more than one, kept in sync by updateCanvasUI() in app.js. */
.canvasBtn.on{background:var(--accent);border-color:var(--accent);color:var(--ink)}
.canvasBtn.on:hover{background:var(--accent-dark);border-color:var(--accent-dark);color:var(--ink)}

/* #exportBtn/#trackExportBtn are plain .pri transport buttons (styled with playBtn/repeatBtn
   above) — these rules add only what's specific to the recording state. */
#exportBtn:disabled,#trackExportBtn:disabled{opacity:.5;cursor:progress}
#exportBtn.recording,#trackExportBtn.recording{background:var(--err);border-color:var(--err);color:var(--beige);animation:exportPulse 1s ease-in-out infinite}
@keyframes exportPulse{0%,100%{opacity:1}50%{opacity:.6}}

/* ---------- Transfer-to-Track button + Track arranger (v5.0.0) ---------- */

/* A single wide bar between the board grid and the Track section — deliberately not styled like
   a plain transport button (full-width, taller, bigger label) since it's the one bridge between
   the two halves of the app, not a peer of Clear/Play/Export. transferToTrack() in app.js does the
   actual work; #transferCanvasLabel is kept in sync with the active canvas by updateCanvasUI(). */
/* v5.0.1 — now one of #trackTransportBar's own buttons (pinned to the row's left edge via
   DOM order, with #trackTransportRight's own margin-left:auto pushing everything else to the
   right — see #trackTransportBar below), not a standalone full-width bar between the board and
   the Track section any more. Kept its own accent-outlined look (distinct from the plain
   #trackTransportBar buttons around it) since it's still the one button that crosses between the
   two sections, not a peer action within either one. */
#transferToTrackBtn{flex:0 0 auto;height:32px;display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;padding:0 14px;border:1px solid var(--accent);border-radius:4px;background:transparent;color:var(--accent);cursor:pointer;transition:background .16s,color .16s}
/* v5.1.0 fix — #transportBar button:hover,#trackTransportBar button:hover (specificity 1 id + 1
   type + 1 pseudo-class) was silently beating this rule's own :hover color (1 id + 1
   pseudo-class, one selector part short of that), leaving the hover state green text on the same
   rule's own green background — unreadable. #trackTransportBar #transferToTrackBtn:hover (two
   ids + 1 pseudo-class) unambiguously outranks it without needing !important. */
#transferToTrackBtn:hover{background:var(--accent)}
#trackTransportBar #transferToTrackBtn:hover{color:var(--ink)}
#transferToTrackBtn svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
#transferToTrackBtn:active{transform:scale(.99)}

/* The Track section itself — fixed height (it's a secondary, scrollable-within-itself area, not
   something that should eat the whole stage the way the board's own #gridScroll does), its own
   transport row on top, a horizontally-scrolling timeline/lanes area below. */
#trackSection{flex:0 0 auto;display:flex;flex-direction:column;border-top:1px solid var(--divider);height:260px}
/* v5.0.1 — #transferToTrackBtn (this row's first/only direct button child) sits at the row's own
   left edge by plain DOM order; #trackTransportRight (below) is the one child with
   margin-left:auto, so it — and everything inside it — is what actually gets pushed to the right
   edge, leaving Transfer pinned left with the row's own flex gap no longer doing that job alone. */
#trackTransportBar{height:var(--header-h);box-sizing:border-box;flex:0 0 auto;display:flex;align-items:center;padding:0 24px;border-bottom:1px solid var(--divider)}
#trackTransportRight{display:flex;align-items:center;gap:10px;margin-left:auto}
#trackTimelineScroll{flex:1 1 auto;overflow-x:auto;overflow-y:hidden;padding:12px 24px}
/* A CSS grid spanning the ruler, the playhead bar, and every lane row — not three independently
   block-sized elements — specifically so all three stay pixel-aligned under horizontal scroll
   with zero JS width-syncing. The second column is a *fixed* width — `calc(8px * 105)`, i.e. 8px
   per second across the full TRACK_MAX_SECONDS (105s/1:45 — v5.0.1, was 240s/4min) — not
   `minmax(…,1fr)`: an elastic column would stretch to fill whatever the window happens to be,
   which on a wide monitor made the whole timeline fit with no scrolling at all and no consistent
   scale, and squeezed a typical (short, ~3s) transferred block down to a few unreadable,
   undraggable pixels wide. A fixed per-second scale keeps block width meaningful and
   `#trackTimelineScroll` (the scrolling ancestor) reliably scrollable regardless of viewport
   width. `#trackLanes`/`.trackLane` are `display:contents` — they exist only for app.js to
   address (one lane at a time) and group each lane's three cells together in markup; they don't
   themselves become grid boxes, so their children (`.laneNumBtn`, `.laneStrip`,
   `.laneCtrlButtons`) place directly into this same grid. v5.1.1 made it a *three*-column grid —
   `.laneCtrlButtons` (the
   reorder/clear/delete cluster) moved from beside `.laneNumBtn` on the left to its own column on
   the right, per explicit request, so each lane's three grid items are now plain siblings
   (`.laneNumBtn`, `.laneStrip`, `.laneCtrlButtons`) with no wrapper needed for either side any
   more — the v5.1.0 `.laneHead` wrapper that used to group the left two into one item is gone,
   since there's nothing left on the left to group. */
#trackTimelineGrid{display:grid;grid-template-columns:40px calc(8px * 105) 100px;row-gap:6px;align-items:center}
/* display:flex here is what makes .trackRulerSeg's own flex:1 1 0 actually spread the segments
   horizontally — same equal-share-of-a-fixed-total trick the board's own #ruler/.rulerBar use.
   v5.1.1 fix — grid-row:1/2 added here (was column-only, row left to auto-placement). With only
   a definite *column*, the grid auto-placement algorithm resolved these two items' rows first
   (as "column-locked" items), then ran the fully-automatic pass for every lane's three items
   (.laneNumBtn/.laneStrip/.laneCtrlButtons) in DOM order against whatever cells that first pass
   left free — which skipped straight past row 1/2's column 2 (taken by these two) and wrapped
   each lane's three items into the wrong columns entirely (observed: numBtn landing in column 3,
   strip in column 1, ctrl in column 2 — a silent one-column rotation, invisible until actually
   measured). Giving every item in this grid both axes explicitly (these two rows 1/2, each lane's
   three items `.laneRowN` below) removes the auto-placement pass — and the column skipping it was
   doing — entirely. */
#trackRuler{grid-column:2;grid-row:1;display:flex;height:18px;font-family:var(--mono);font-size:9px;color:var(--ter)}
.trackRulerSeg{flex:1 1 0;border-left:1px solid var(--grid-line-bar);padding-left:4px}
#trackTimelineBar{grid-column:2;grid-row:2;position:relative;height:4px;border:1px solid var(--accent);border-radius:999px}
#trackTimelineDot{position:absolute;top:50%;left:0;width:10px;height:10px;border-radius:50%;background:var(--accent);transform:translate(-50%,-50%);box-shadow:0 0 6px rgba(163,224,82,.6);cursor:grab;touch-action:none;z-index:6}
#trackTimelineDot::before{content:'';position:absolute;inset:-8px}
#trackTimelineDot:active{cursor:grabbing}
#trackLanes{display:contents}
.trackLane{display:contents}
/* v5.1.1 — every lane item now gets an explicit grid-column (was left to auto-placement — see
   the fix note on #trackRuler above for why that silently scrambled which column each item
   landed in) and an explicit grid-row via one of the three `.laneRowN` classes below, added by
   renderTrackLanes() in app.js alongside each lane's own three elements —
   TRACK_LANE_COUNT is a small, fixed constant (3), so three static row classes is the same
   "enumerate into classes" rule this app already applies to `.laneStrip.bpm0`..`bpm4` and
   everywhere else CLAUDE.md's CSP bullet describes, not something to compute via `.style`. Rows
   1/2 are #trackRuler/#trackTimelineBar's own explicit rows (see above), so lanes start at row 3. */
.laneRow0{grid-row:3}
.laneRow1{grid-row:4}
.laneRow2{grid-row:5}
/* v5.0.1 — halved from 54px to 27px, per explicit request. v5.1.1 removed the v5.1.0
   `flex:0 0 32px`/.laneHead wrapper this needed while it shared a grid cell with
   .laneCtrlButtons — back to a bare grid item (column 1, 40px — see #trackTimelineGrid above)
   that stretches to fill its own column by default, the original pre-v5.1.0 shape. */
.laneNumBtn{grid-column:1;display:flex;align-items:center;justify-content:center;height:27px;font-family:var(--mono);font-size:13px;font-weight:600;border:1px solid var(--divider);border-radius:4px;background:transparent;color:var(--sec);cursor:pointer;transition:border-color .16s,color .16s,background .16s}
.laneNumBtn:hover{border-color:var(--accent);color:var(--accent)}
.laneNumBtn.on{background:var(--accent);border-color:var(--accent);color:var(--ink)}
/* v5.1.0 — reorder/clear/delete for one lane, laid out horizontally (the explicit ask, contrasted
   with the board's own vertical .trackCtrlButtons stack) — same --bg-mid/--divider-strong/--beige
   contrast treatment as .ctrlBtn/.rowToolBtn. v5.1.1 moved this cluster from beside .laneNumBtn
   on the left to its own column on the right (see #trackTimelineGrid above) and sized it up 25%
   (18px -> 22.5px, 11px icon -> 13.75px) per explicit request — now its own grid item, centered
   in its 100px-wide column, rather than packed into a shared gutter with .laneNumBtn. .laneDelete
   turns --err on hover rather than --accent, matching .ctrlBtn.remove's own "this one is the
   stronger action" treatment. */
.laneCtrlButtons{grid-column:3;display:flex;align-items:center;justify-content:center;gap:4px}
.laneCtrlBtn{width:22.5px;height:22.5px;display:flex;align-items:center;justify-content:center;border:1px solid var(--divider-strong);border-radius:3px;background:var(--bg-mid);color:var(--beige);cursor:pointer;transition:border-color .16s,color .16s}
.laneCtrlBtn:hover{border-color:var(--accent);color:var(--accent)}
.laneCtrlBtn.laneDelete:hover{border-color:var(--err);color:var(--err)}
.laneCtrlBtn svg{width:13.75px;height:13.75px;stroke:currentColor;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
/* v5.0.1 — a repeating grid pattern showing where a block could land: one cell = one board loop
   at the *current* BPM (`totalDuration()` in app.js — 360/bpm seconds, the same number regardless
   of which board is active, since BPM is shared across all of them). Only BPM_PRESETS.length (5)
   possible cell widths exist, so — same CSP rule as everywhere else in this app — this is 5 static
   classes (`.bpm0`..`.bpm4`, toggled by renderTrackLanes() off state.bpmIndex) rather than a
   computed `background-size`/`.style` write. Widths below are `8px * (360 / bpm)` for each preset
   (100/120/130/140/170), matching #trackTimelineGrid's own 8px-per-second scale exactly, so a
   grid line always lands on a real second boundary. */
/* v5.1.0 — cursor:pointer added: clicking empty space on a strip now sets both the Transfer
   target lane and the Track's playhead (see the .laneStrip click listener in renderTrackLanes()
   in app.js) rather than doing nothing, so the strip needed a visual cue that it's clickable. */
.laneStrip{grid-column:2;position:relative;height:27px;background:var(--track-bg);border:1px solid var(--divider);border-radius:4px;cursor:pointer}
.laneStrip.bpm0{background-image:repeating-linear-gradient(to right,var(--grid-line-bar) 0 1px,transparent 1px 28.8px)}
.laneStrip.bpm1{background-image:repeating-linear-gradient(to right,var(--grid-line-bar) 0 1px,transparent 1px 24px)}
.laneStrip.bpm2{background-image:repeating-linear-gradient(to right,var(--grid-line-bar) 0 1px,transparent 1px 22.15px)}
.laneStrip.bpm3{background-image:repeating-linear-gradient(to right,var(--grid-line-bar) 0 1px,transparent 1px 20.57px)}
.laneStrip.bpm4{background-image:repeating-linear-gradient(to right,var(--grid-line-bar) 0 1px,transparent 1px 16.94px)}
/* A block is positioned/sized with two independent paused WAAPI Animations (left/width), scrubbed
   by hand via setBlockGeometry() in app.js — the exact same CSP-safe technique CLAUDE.md already
   uses for the knob rotation and the playhead dot, extended to a second, genuinely continuous,
   time-driven value (a block's start/duration can land anywhere across 4 minutes, which can't be
   enumerated into a finite set of static classes the way almost everything else in this app is). */
.trackBlock{position:absolute;top:3px;bottom:3px;border-radius:4px;background:var(--accent);color:var(--ink);display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:.06em;cursor:grab;overflow:hidden;box-shadow:var(--shadow-md)}
.trackBlock:active{cursor:grabbing}
/* v5.1.1 fix — this used to be outline-offset:-2px in var(--beige), sitting *inside* the block's
   own border edge, overlapping a fill that's already var(--accent) (green) — a beige ring laid
   over a green fill barely registered as "selected" at a glance, the same class of green-on-green
   visibility bug #transferToTrackBtn's own hover state had. Flipping the offset positive draws
   the ring in the gap between the block and its lane strip instead, where it reads clearly
   against the strip's own near-black `--track-bg`; switching the color to var(--accent) (actual
   green) is what makes it the "green outlined" box that was asked for, rather than leaning on a
   color that happened to read as a little lighter. 3px/positive-offset also matches the board's
   own `.track.selected` sizing/shape, just outside instead of inset (that rule is on a dark,
   non-accent row background, so an *inset* outline already reads fine there — this block's own
   fill is why it needs the opposite placement). */
.trackBlock.selected{outline:3px solid var(--accent);outline-offset:2px}
/* A lane being dragged over as a cross-lane move target (v5.0.1, bindTrackBlockDrag() in app.js)
   — the same dashed-outline "drop here" language `.track.dragOver` already uses for the board's
   own row-reorder drag, not the solid outline `.trackBlock.selected`/`.track.selected` use, so the
   two states can never be confused. */
.laneStrip.dragOver{outline:2px dashed var(--accent);outline-offset:-2px}
/* The right-edge grab handle that duplicates a block forward (v5.0.1, bindTrackBlockResize() in
   app.js) — a narrow strip just inside the block's own right edge rather than a separate sibling
   element, so it moves/resizes with the block for free. Deliberately always visible (not
   hover-gated like `.rulerTools`) — a block can be as little as ~17px wide at the fastest BPM
   preset, too narrow to reliably discover a hover-only affordance on. */
.trackBlockHandle{position:absolute;top:0;right:0;bottom:0;width:6px;cursor:ew-resize;background:rgba(0,0,0,.18)}
.trackBlockHandle:hover{background:rgba(0,0,0,.32)}
/* The live preview while dragging that handle — one dashed rectangle spanning every duplicate the
   drag currently covers at once (cheaper than animating N separate ghost elements, and reads
   identically), positioned/sized the same WAAPI-Animation way every other block is. Never actually
   interactive — pointer-events:none, since it's #trackTimelineBar's/the handle's own drag driving
   it, not anything on the ghost itself. */
.trackBlockGhost{position:absolute;top:3px;bottom:3px;border-radius:4px;border:2px dashed var(--accent);background:rgba(163,224,82,.18);pointer-events:none}

/* Stage-window footer (v3.6.0) — a plain static bar, not JS-driven; #gridScroll's own flex:1 1 auto
   already pushes it to the bottom of #stage without needing margin-top:auto here. FSCLN is called
   out in --accent to match how the rail's own wordmark reads, everything else at --ter like the
   gate footer's own copyright-style line. */
#stageFoot{flex:0 0 auto;text-align:right;padding:14px 24px;border-top:1px solid var(--divider);font-family:var(--mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--ter)}
#stageFoot .stageFootBrand{color:var(--accent);font-weight:600;text-decoration:none}
#stageFoot .stageFootBrand:hover{text-decoration:underline}

#toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);background:var(--bg-mid);border:1px solid var(--divider);color:var(--beige);font-family:var(--mono);font-size:11px;padding:10px 16px;border-radius:6px;z-index:99;box-shadow:var(--shadow-md)}
#toast[hidden]{display:none}

/* ---------- desktop-only gate, Go Pro modal, domain-block modal ---------- */
/* Ported from fscln/grattern's #gate/#pro/#blocked (PLATFORM.md D5) — same box treatment, same
   token names (this file already aliases --bg/--beige/--sec/--ter/--mono/--display the same way
   grattern's does, and --accent/--accent-dark/--divider/--divider-strong/--bg-mid come straight
   from the shared DS in both), just re-copied here since each app ships its own app.css. */
#gate{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;padding:32px;background:var(--bg)}
#gate[hidden]{display:none}
#gateBox{max-width:420px;text-align:left}
#gateMark img{height:34px;width:auto;display:block}
#gateEyebrow{margin-top:28px;font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent)}
#gateTitle{margin:14px 0 0;font-family:var(--display);font-weight:500;font-size:30px;line-height:1.15;letter-spacing:-.01em;color:var(--beige)}
#gateBody{margin:16px 0 0;font-family:var(--mono);font-size:12px;line-height:1.7;color:var(--sec);text-wrap:pretty}
#gateFoot{margin-top:32px;padding-top:16px;border-top:1px solid var(--divider);font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--ter)}

#pro{position:fixed;inset:0;z-index:9998;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--scrim);backdrop-filter:blur(6px)}
#pro[hidden]{display:none}
#proBox{width:100%;max-width:400px;box-sizing:border-box;background:var(--bg-mid);border:1px solid var(--divider);border-radius:8px;padding:28px;display:flex;flex-direction:column;gap:18px}
#proEyebrow{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent)}
#proTitle{margin:12px 0 0;font-family:var(--display);font-weight:500;font-size:26px;line-height:1.15;letter-spacing:-.01em;color:var(--beige)}
#proBody{margin:12px 0 0;font-family:var(--mono);font-size:12px;line-height:1.7;color:var(--sec);text-wrap:pretty}
#proField{display:flex;flex-direction:column;gap:6px}
#proField label{font-family:var(--mono);font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--ter)}
#proCode{width:100%;box-sizing:border-box;font-family:var(--mono);font-size:12px;letter-spacing:.06em;color:var(--beige);background:var(--bg);border:1px solid var(--divider);border-radius:4px;padding:10px 12px;outline:none}
#proCode:focus{border-color:var(--accent)}
#proErr{font-family:var(--mono);font-size:9px;letter-spacing:.08em;text-transform:uppercase;color:var(--err-soft)}
#proErr[hidden]{display:none}
#proActions{display:flex;gap:8px;justify-content:flex-end}
#proActions button{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;border-radius:4px;height:32px;cursor:pointer;transition:border-color .16s,color .16s,background .16s}
#proCancel{border:1px solid var(--divider-strong);background:transparent;color:var(--sec);padding:0 14px}
#proCancel:hover{border-color:var(--accent);color:var(--accent)}
#proUnlock{border:none;background:var(--accent);color:var(--ink);padding:0 16px}
#proUnlock:hover{background:var(--accent-dark)}

/* Domain-block modal — same box treatment as #pro, no eyebrow/field since there's nothing to
   submit, just information and a close button. */
#blocked{position:fixed;inset:0;z-index:9998;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--scrim);backdrop-filter:blur(6px)}
#blocked[hidden]{display:none}
#blockedBox{width:100%;max-width:400px;box-sizing:border-box;background:var(--bg-mid);border:1px solid var(--divider);border-radius:8px;padding:28px;display:flex;flex-direction:column;gap:18px}
#blockedTitle{font-family:var(--display);font-weight:500;font-size:26px;line-height:1.15;letter-spacing:-.01em;color:var(--beige)}
#blockedBody{margin:0;font-family:var(--mono);font-size:12px;line-height:1.7;color:var(--sec);text-wrap:pretty}
#blockedActions{display:flex;gap:8px;justify-content:flex-end}
#blockedClose{border:1px solid var(--divider-strong);background:transparent;color:var(--sec);font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;border-radius:4px;height:32px;padding:0 14px;cursor:pointer;transition:border-color .16s,color .16s}
#blockedClose:hover{border-color:var(--accent);color:var(--accent)}

@media (max-width:900px){
  /* --trackh/--label-w doubled and grew alongside the non-mobile values in v4.0.0, same reasons
     (see :root above) — this media query only ever applies to a narrowed desktop window anyway
     (a genuinely touch/narrow device hits #gate instead), so it stays proportional rather than
     picking its own independent sizing. --numbtn-w/--label-w scaled down again in v4.1.0 to match
     the non-mobile shrink (the dial grid takes far less width than the old single-row layout). */
  :root{--rail:250px;--trackh:88px;--numbtn-w:34px;--label-w:190px;--ctrls-w:92px}
  #head{padding:0 16px}
  section{padding:14px 16px}
  #gridScroll{padding:0 14px 14px}
  /* The bigger 30px ctrlBtn (see the non-mobile rule above, v4.1.0) no longer fits three of them
     stacked inside the shorter 88px mobile --trackh with room to breathe, so it steps back down
     here specifically — matched to .rowToolBtn's own mobile-equivalent size, same reasoning as
     the non-mobile rule. */
  .ctrlBtn{width:24px;height:24px}
  .ctrlBtn svg{width:13px;height:13px}
  .dialCell{width:26px}
  .volumeKnob{width:16px;height:16px}
}
