/* ============================================================================
   app.css — application styling for the EUCLID single app.
   Migrated from the inline <style> in index.html. euclid.css holds the
   brief's design-system foundations; this file holds the app's own token
   set, reset, layout chrome, panels, and segment/track renderers.
   Loaded AFTER euclid.css to preserve the original cascade order.
   ============================================================================ */

/* ============================================================
   Tokens
   ============================================================ */

/* Trim @font-face is declared once in euclid.css (loaded first), wiring
   fonts/Trim-Regular.woff2 (400) + fonts/Trim-Medium.woff2 (500) — the only
   two weights breif-v2 §2 permits. @font-face is document-global, so no
   separate declaration is needed here. */

:root {
  /* Three type roles:
       brand — Trim, the EUCLID wordmark;
       ui    — Trim, the workhorse (body, labels, controls);
       num   — Roboto Mono, numeric readouts (locks the column so layout
               doesn't jitter as digits change).
     Trim is the intended chrome look. Per breif-v2 §2 only two weights are
     used: Regular (400) for body/labels, Medium (500) for emphasis (section
     titles, selected state, the EUCLID wordmark). The legacy --font-chrome /
     --font-data names are kept as aliases of the UI role so existing
     references resolve; --font-mono points at the numeric stack. */
  /* breif-v2 §2: Trim ONLY — two weights, no third typeface. Numerals are
     Trim with tabular-nums always on, so the numeric role points at Trim too
     (the prior Roboto Mono numeral face was a §2/§9 violation). The legacy
     --font-num / --font-mono / --font-chrome / --font-data names are kept as
     aliases of the Trim stack so the ~2.6k call-sites need no churn. */
  --font-brand: 'Trim', 'Helvetica Neue', Arial, sans-serif;
  --font-ui: 'Trim', 'Helvetica Neue', Arial, sans-serif;
  --font-num: 'Trim', 'Helvetica Neue', Arial, sans-serif;
  --font-mono: var(--font-num);
  --font-chrome: var(--font-ui);
  --font-data: var(--font-ui);

  /* breif-v2 reconciliation (DF-320): the app's legacy token set is now a
     thin alias layer over euclid.css's --eu-* tokens, which are the single
     source of truth. Values resolve to the brief's exact palette; the local
     names are kept so the ~2.6k-line call-sites need no churn. Visual
     distinction stays by line and ink, never by fill (one void surface). */
  --bg-void: var(--eu-bg);
  --bg-panel: var(--eu-bg);
  --bg-deep: var(--eu-bg);
  --bg-input: var(--eu-bg);

  --line-dim: var(--eu-rule);
  --line-mid: var(--eu-rule-strong);
  --line-bright: var(--eu-rule-strong);

  /* OVERVIEW remainder-bar fill — the faint ink end of the ramp, so the
     empty portion of a bar reads as inert backdrop yet stays visible. */
  --bar-empty: var(--eu-ink-4);

  /* Monochrome ink ramp → euclid.css --eu-ink-* (text + linework). */
  --ink-bright: var(--eu-ink-0);
  --ink-mid: var(--eu-ink-1);
  --ink-dim: var(--eu-ink-2);
  --ink-mute: var(--eu-ink-3);

  /* Monochrome pass: the interactive accent is now WHITE, not cyan. Remapping
     the --eu-cyan token here (the app layer, loaded after euclid.css) flips
     every cyan touchpoint — selection tab + number, transport, focus, knob
     arcs, bound meters, M/S solo, level filters — to white in one place,
     without editing the design-system file. White is the ink-ramp's bright end
     (--eu-ink-0), so it stays inside the system's scale.
     --turquoise-dim drops to a mid-grey so secondary states read monochrome too. */
  --eu-cyan: var(--eu-ink-0);
  --turquoise: var(--eu-cyan);
  --turquoise-dim: var(--eu-ink-2);

  --gap: 24px;
  /* Channel-row tokens — shared between the cypher tracks (left) and the
     OVERVIEW voice bars (right) so the two 16-row groups align visually
     and read as the same instrument viewed two ways. All sizes powers of
     2 (or sums thereof): the cypher data itself runs on 2^n (16 rings,
     8 channels, rotation_duration ∈ {8,16,32,64,128}), and the layout
     honours that. --row-h is the content height of one row; --row-gap is
     the space between rows; --prerow-h is the fixed-height "strip"
     above each 16-row group (timeline strip on the left, voice-head
     label on the right). */
  /* Track rows sit at their natural 32px — the height at which the M/S
     toggles read the same size as the segments beside them. The tracks no
     longer stretch to fill the viewport; the Binder now docks in the region
     below them (see .binder-overlay) and takes whatever height is left.
     --eu-row is the component token the track rows AND the OVERVIEW bars
     both consume (euclid.css), so the two 16-row groups stay aligned by
     construction; --row-h (the app-side row token) follows it. */
  --eu-row: 32px;
  --row-h: var(--eu-row);
  --row-gap: 4px;
  --prerow-h: 48px;

  /* v0.7 layout tokens — brainstem-derived columnar grid.
     Section-label gutter is narrow (10pt label sits at left edge);
     right rail hosts OVERVIEW (cypher row) and BIND (synth row). */
  --col-label: 64px;
  --col-rail: 384px;   /* 3 × 128 base unit — OVERVIEW / BINDINGS rail */
  --gap-col: 32px;
  /* between left-label, main, right rail */
  --gap-row: 32px;
  /* between section rows (CYPHER / SYNTH) */
  --gap-block: 24px;
  /* between sub-blocks within a section */
  --page-pad: 32px;
  /* page-edge padding left/right */

  /* Type scale — derived from the brainstem PDF measurements.
     Labels read at small caps with wide tracking; numbers grow to
     dominate as primary graphic. */
  /* Type scale — breif-v2 §2 (11 / 13 / 16 / 22 / 32 / 48). Nothing below
     11px: prior passes had shrunk labels to 7-10px, which is the single
     biggest reason the live app read smaller and cruder than the standard. */
  --fs-section: 11px;
  /* gutter section labels: CYPHER / SYNTH (caption role, wide tracking) */
  --fs-subhead: 11px;
  /* sub-block titles: CHANNELS / SYNTH / BINDINGS (label role) */
  --fs-tertiary: 11px;
  /* sub-sub labels: per-track captions */
  --fs-caption: 11px;
  /* small captions / axis / meta */
  --fs-body: 11px;
  /* in-line UI body text */
  --fs-num: 11px;
  /* knob values, bar readouts (inline-value role) */
  --fs-num-lg: 11px;
  /* clock (readout role) */

  /* Tracking scale (UI consistency pass) — a small named scale replacing
     the ad-hoc per-element letter-spacing values. Values chosen to match
     the dominant existing ones so the consolidation barely shifts the
     look. tight: body/data; ui: controls and mid labels; caps: small-caps
     labels and buttons; wide: section labels. The legacy tracking tokens
     alias onto the scale so their many call sites funnel through it too. */
  /* breif-v2 §2 tracking: labels 0.085em, captions 0.12em, display numerals
     0.005em. Prior values (0.1-0.2em) were too loose and read as a different
     system. tight=numerals, ui=data, caps=labels, wide=captions. */
  --track-tight: 0.005em;
  --track-ui: 0.06em;
  --track-caps: 0.085em;
  --track-wide: 0.12em;
  --tracking-label: var(--track-caps);
  --tracking-caption: var(--track-wide);
}

/* ============================================================
   Reset, base
   ============================================================ */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  background: var(--bg-void);
  color: var(--ink-bright);
  font-family: var(--font-ui);
  /* Trim Regular (400) is the body/label workhorse (breif-v2 §2). Everything
     UI inherits this weight; Medium (500) is reserved for emphasis (section
     titles, the wordmark, selected state), and the numeric role
     (var(--font-num)) is the other exception. */
  font-weight: 400;
  font-size: 11px;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
  text-rendering: geometricPrecision;
  font-variant-numeric: tabular-nums;   /* breif-v2 §2: numerals align in columns everywhere */
  letter-spacing: var(--track-tight);
  /* The page fills the viewport and never scrolls itself: the app (header +
     tracks) sits at its natural height up top, and the Binder dock fills the
     region below it. Both are flex children in this column. */
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* ---- Button system: one base + three ranks ----
   Base is the secondary look (outline → cyan on hover, no fill) — the
   default for navigation/toggle controls. Ranks layer on top:
     .btn-primary  commit actions (SAVE / LOAD / ADD) — accent fill on hover
     .btn-quiet    low rank (close, secondary list actions) — grey
   One transition and one hover rule each; a shared :focus-visible ring
   makes the editor keyboard-navigable (was missing entirely). The single
   interactive accent is cyan; magenta is the alert/over-threshold signal
   (warnings, destructive hovers) per breif-v2 §3. No orange. */
button {
  font-family: var(--font-ui);
  font-size: inherit;
  font-weight: 400;   /* Trim Regular — the body/control weight */
  letter-spacing: var(--track-caps);
  background: transparent;
  border: 1px solid var(--line-mid);
  color: var(--ink-bright);
  padding: 6px 14px;
  cursor: pointer;
  transition: border-color 80ms linear, color 80ms linear, background 80ms linear;
}

button:hover:not(:disabled) {
  border-color: var(--turquoise);
  color: var(--turquoise);
}

button:disabled {
  color: var(--ink-mute);
  border-color: var(--line-dim);
  cursor: not-allowed;
}

button:focus-visible {
  outline: 2px solid var(--turquoise);
  outline-offset: 1px;
}

.btn-primary {
  color: var(--turquoise);
  border-color: var(--turquoise-dim);
}

.btn-primary:hover:not(:disabled) {
  background: var(--turquoise);
  color: var(--bg-void);
  border-color: var(--turquoise);
}

.btn-primary:disabled {
  background: transparent;
  color: var(--ink-mute);
  border-color: var(--line-dim);
}

.btn-quiet {
  color: var(--ink-dim);
  border-color: var(--line-mid);
}

.btn-quiet:hover:not(:disabled) {
  color: var(--ink-bright);
  border-color: var(--ink-mid);
  background: transparent;
}

input,
select,
textarea {
  font-family: inherit;
  font-size: inherit;
  background: var(--bg-input);
  border: 1px solid var(--line-mid);
  color: var(--ink-bright);
  padding: 6px 10px;
  outline: none;
}

/* Full-width is opt-in by container, not global — a global
   `input, select { width: 100% }` forces every future field full-width
   (and stretched checkboxes). The modal form fields want it; flex-sized
   and fixed-width controls (bound rows, the cypher-id box) size
   themselves. */
.field input,
.field select {
  width: 100%;
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--turquoise);
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--turquoise);
  outline-offset: 1px;
}

select option:disabled {
  color: var(--ink-mute);
}

/* ============================================================
   Layout chrome
   ============================================================ */
header.app {
  display: flex;
  align-items: center;
  gap: 16px;
  /* top padding matches the page inset so the logo sits the same distance from
     the top of the page as from the left */
  padding: var(--page-pad) var(--page-pad) 24px;
}
/* Only the EUCLID identity shows in the header for now — everything to the
   right of it is hidden. (Kept in the DOM; the app logic still drives them.) */
header.app .brand ~ * { display: none; }

/* EUCLID identity — Medium (500) wordmark. Sits with the brainstem-style
   "system monitor" caption block at the right end of the header. */
header.app .brand {
  font-family: var(--font-brand);   /* the one element kept in Trim — identity */
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 500;
  letter-spacing: var(--track-wide);
  font-size: 11px;
  color: var(--ink-bright);
}

header.app .brand-mark {
  display: block;
  flex-shrink: 0;
}

header.app .spacer {
  flex: 1;
}

header.app .clock {
  color: var(--ink-bright);
  /* Roboto Mono via --font-num — monospace locks the column so
     the surrounding button row doesn't jitter as digits tick. Sized down to
     sit in the single-size control band rather than tower over it. */
  font-family: var(--font-num);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-tight);
  font-size: 11px;
}

/* Brainstem-style system monitor block — three lines of tiny right-
   aligned caption text at the page-edge. Identity / version / handle. */
header.app .system-monitor {
  text-align: right;
  font-size: var(--fs-caption);
  letter-spacing: var(--tracking-caption);
  color: var(--ink-dim);
  line-height: 1.5;
  text-transform: uppercase;
  font-weight: 400;
  margin-left: 12px;
}

/* Audio / MIDI state indicators (DF-404) — sit just left of the overhead. Dim
   ink = disabled; bright ink = enabled. Pure status; the connect logic is auto. */
header.app .system-monitor .io-state {
  display: inline-flex;
  gap: 9px;
  align-items: center;
  margin-right: 10px;
}
header.app .io-ind { color: var(--ink-dim); transition: color .2s linear; }   /* grey = disabled */
header.app .io-ind.on { color: #eceef0; }                                     /* white = enabled */
.midi-zone[hidden] { display: none !important; }

/* Overhead indicator — a corner triangle reading frame load. Its colour ramps
   from the dim ink at idle toward the alert red as the loop eats more of the
   frame budget (--overhead, 0..1, set by ui-overhead). When the page is actually
   dropping frames the wrapper gains .overhead--stress and the triangle flashes. */
header.app .system-monitor .overhead {
  --overhead: 0;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 5px;
  cursor: default;
}
header.app .overhead-tri {
  display: block;
  width: 11px;
  height: 11px;
  fill: color-mix(in srgb, #eb5757 calc(var(--overhead) * 100%), var(--ink-dim));
  transition: fill .25s linear;
}
header.app .overhead-pct {
  font-variant-numeric: tabular-nums;
}
header.app .overhead--stress .overhead-tri {
  animation: overhead-flash .7s steps(1, end) infinite;
}
@keyframes overhead-flash {
  0%, 50%        { fill: #eb5757; }
  50.01%, 100%   { fill: color-mix(in srgb, #eb5757 25%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  header.app .overhead--stress .overhead-tri { animation: none; fill: #eb5757; }
}

/* ============================================================
   Status area — fixed, bottom-left, always in view. Messages are
   graded by severity: CRITICAL (red), WARNING (amber), INFO (white).
   It is a ROTATING QUEUE: one message shows at a time for ~10s, then
   the surface advances to the next (each line poured in with the
   oracle-style eased reveal + glitch). The queue is plumbing — it is
   never shown in full; rollovers (hover-hints) pre-empt it on the spot.
   See src/ui/ui-status.js. Kept deliberately frame-free — just a dot,
   the line, and a faint backing — so it sits quietly over the page.
   Level colours are scoped here rather than the global palette so the
   rest of the UI keeps its monochrome standard.
   ============================================================ */
.status-area {
  position: fixed;
  left: 12px;
  bottom: 12px;
  z-index: 9000;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  max-width: min(420px, calc(100vw - 24px));
  /* Clicks pass through everywhere except the live row itself. */
  pointer-events: none;
}

.status-msg {
  --status-colour: var(--eu-ink-0, #f4f4f4);   /* INFO / white default */
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 100%;
  /* No box, no border, no bullet — just the line of text. A soft shadow
     (rather than a backing panel) lifts it off busy backgrounds. */
  color: var(--status-colour);
  font-size: var(--fs-caption, 11px);
  letter-spacing: var(--tracking-caption, 0.04em);
  text-transform: uppercase;
  font-weight: 500;
  line-height: 1.4;
  text-shadow: 0 1px 2px var(--eu-bg, #070808), 0 0 6px var(--eu-bg, #070808);
}

.status-msg[data-level="critical"] { --status-colour: #eb5757; }   /* red */
.status-msg[data-level="warning"]  { --status-colour: #e0a23b; }   /* amber */
.status-msg[data-level="info"]     { --status-colour: var(--eu-ink-0, #f4f4f4); }   /* white */

/* One line, never two: the text stays on a single row and ellipsises if it
   would overflow. (Messages are also capped to 20 words in ui-status.js.) */
.status-msg__text {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.status-msg--clickable { cursor: pointer; }

.status-msg__close {
  flex: 0 0 auto;
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 0 0 0 4px;
  opacity: 0.6;
}
.status-msg__close:hover { opacity: 1; }

/* Chromatic glitch burst — JS adds .status-msg--glitch for ~0.5s when a new
   line takes the screen, echoing the oracle panel's "speak" tell. The text
   itself is poured in letter-by-letter by the eased reveal in ui-status.js. */
.status-msg--glitch { animation: status-glitch 0.4s linear 1; }
@keyframes status-glitch {
  0%   { transform: translateX(0);    text-shadow: none; }
  15%  { transform: translateX(-3px); text-shadow: 3px 0 #fff, -3px 0 var(--status-colour); }
  35%  { transform: translateX(3px);  text-shadow: -2px 0 #fff, 2px 0 var(--status-colour); }
  55%  { transform: translateX(-2px); text-shadow: 2px 0 #fff, -2px 0 var(--status-colour); }
  75%  { transform: translateX(1px);  text-shadow: 1px 0 #fff, -1px 0 var(--status-colour); }
  100% { transform: translateX(0);    text-shadow: none; }
}
@media (prefers-reduced-motion: reduce) {
  .status-msg--glitch { animation: none; }
}

.midi-zone {
  display: flex;
  align-items: center;
  gap: 14px;
}

/* ENABLE AUDIO / MIDI are secondary (base look); SAVE / LOAD PATCH carry
   .btn-primary in the markup. Sized to the band's single control size (the
   load-cypher cyc-* size) — no control in the band is larger than another. */
.midi-btn {
  padding: 3px 10px;
  font-size: 10px;
}

.midi-info {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 10px;
  letter-spacing: var(--track-ui);
  color: var(--ink-mid);
}

.midi-dot {
  width: 6px;
  height: 6px;
  background: var(--turquoise);
}

#midi-in-port {
  color: var(--ink-bright);
  background: transparent;
  border: 1px solid var(--ink-faint, currentColor);
  font: inherit;
  font-size: 10px;
  letter-spacing: var(--track-ui);
  padding: 1px 4px;
  max-width: 160px;
}

#midi-in-port:disabled {
  color: var(--ink-mid);
}

/* The transport button is cyan throughout (breif-v2 §8: transport uses
   cyan). Running/live = FILLED cyan (the lit "playing" signal); stopped
   (.paused) = ghost cyan that fills on hover. State reads by fill, not hue.
   Sizing only differs from the button system. */
.play-btn {
  padding: 3px 10px;
  font-size: 10px;
  background: var(--eu-cyan);
  color: var(--eu-bg);
  border-color: var(--eu-cyan);
  min-width: 64px;
}

.play-btn:hover:not(:disabled) {
  background: var(--eu-cyan);
  color: var(--eu-bg);
  border-color: var(--eu-cyan);
}

.play-btn.paused {
  color: var(--turquoise);
  border-color: var(--turquoise-dim);
}

.play-btn.paused:hover:not(:disabled) {
  background: var(--turquoise);
  color: var(--bg-void);
  border-color: var(--turquoise);
}

/* Transport cluster — play/pause + stop. STOP is secondary (base look). The
   loop buttons live in their own .loop-zone; their engaged state is the shared
   white-outline treatment (see the .active rule in the button system below). */
.transport {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Loop cluster — SEGMENT / ARC. Each button toggles its own loop mode and
   lights via the shared .active treatment. Mirrors the PATCH zone layout. */
.loop-zone,
.help-zone {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* ============================================================
   v0.8 main grid — brainstem-derived columnar layout.

   Three columns: a narrow left gutter for section labels, the main
   content area, and a fixed-width right rail. Two explicit row areas
   run across all three columns: CYPHER (top) and SYNTH (bottom). The
   rail hosts two row-aligned blocks — OVERVIEW alongside CYPHER (the
   per-ring readout sharing row sizes with the 16 tracks) and BIND
   alongside SYNTH (the DRIVE/ENVELOPE lists for the currently-
   selected target, paired with the knob that authors them).

   Spacing is token-driven so any pass on density flows through one
   block of variables, not 40 selectors.
   ============================================================ */
main {
  display: grid;
  grid-template-columns:
    [labels] var(--col-label) [content] 1fr [rail] var(--col-rail);
  grid-template-rows:
    [cypher] auto [synth] 1fr;
  column-gap: var(--gap-col);
  /* row-gap 0: the cypher block already carries 32px of internal bottom padding
     (stripe 22 + sub-block 10), which equals the 32px top band between the
     header and the TRACKS title — so the space above and below the tracks reads
     the same size. */
  row-gap: 0;
  /* Top band reduced again (24 → 12px); bottom keeps --gap-row (32px). */
  padding: 12px var(--page-pad) var(--gap-row);
  /* Natural height — the tracks size themselves; the Binder dock below takes
     the rest of the viewport. flex:0 0 auto keeps main from stretching. */
  flex: 0 0 auto;
  min-height: 0;
}

/* Section labels — STEM/FLOW/SEGMENT analog. Small caps, wide tracking,
   dim ink, top-aligned with the first sub-header of their content row.
   They live in their own grid column so they line up across the whole
   page; they never sit inside the content blocks. */
.section-label {
  grid-column: labels;
  font-size: var(--fs-section);
  letter-spacing: var(--tracking-label);
  color: var(--ink-dim);
  font-weight: 500;
  text-transform: uppercase;
  align-self: start;
  padding-top: 1px;
  /* visual nudge to baseline-align with sub-header */
  white-space: nowrap;
}

.section-label[data-section="cypher"] {
  grid-row: cypher;
}

.section-label[data-section="synth"] {
  grid-row: synth;
}

/* The whole SYNTH band (thickness / modules / params) and the INSPECTOR are
   HIDDEN for now — the cypher TRACKS + OVERVIEW take the full page height,
   and the Binder is the authoring surface. Everything stays in the page
   (scripts still render into the hidden blocks harmlessly); delete this
   block to bring the lower band back. */
.section-label[data-section="synth"],
.section-content[data-section="synth"],
.rail-block[data-rail="inspector"] { display: none; }
main { grid-template-rows: [cypher] 1fr; }

/* Section content lives in the centre column. Each section may host
   multiple sub-blocks stacked vertically. */
.section-content {
  grid-column: content;
  display: flex;
  flex-direction: column;
  gap: var(--gap-block);
  min-width: 0;
  /* allow children to shrink below their intrinsic width */
}

.section-content[data-section="cypher"] {
  grid-row: cypher;
}

.section-content[data-section="synth"] {
  grid-row: synth;
}

/* Right rail — two row-aligned blocks. OVERVIEW sits in the cypher row
   so its 16 voice bars share row sizes with the cypher tracks; the
   INSPECTOR sits in the synth row below it, showing the clicked segment.
   Each block flex-stacks its own sub-blocks. */
.rail-block {
  grid-column: rail;
  display: flex;
  flex-direction: column;
  gap: var(--gap-block);
  min-width: 0;
}

.rail-block[data-rail="overview"] {
  grid-row: cypher;
}

.rail-block[data-rail="inspector"] {
  grid-row: synth;
}

/* Sub-block — one labelled unit of content inside a section.
   Brainstem's "BANDWIDTH PER STEM" / "LOCAL INTERSECTIONS" pattern.
   Header text sits above a horizontal rule; another rule closes the
   module at the bottom. Each rule is a 1px dim middle with brighter,
   50% thicker short end-caps (~1ch). Same family as the cypher segments'
   CAP idiom, traced back to a layout that predates Cypher. */
.sub-block {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-bottom: 10px;
}

.sub-header {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  row-gap: 4px;
  gap: 14px;
  min-width: 0;
  overflow: hidden;
  font-size: var(--fs-subhead);
  letter-spacing: var(--tracking-label);
  color: var(--eu-ink-1);
  font-weight: 500;
  text-transform: uppercase;
  white-space: nowrap;
  position: relative;
  padding-bottom: 8px;
}

/* The section rule under each sub-header — the breif-v2 §5 signature.
   A 1px element: a dim 1px middle line (--eu-rule) spanning the full
   width, capped at BOTH ends by a short bright-white segment
   (--eu-cap), 10px wide × 1px tall. Background stacking puts the caps
   on top of the middle so the bright ends overprint. The title sits
   ABOVE this rule; content hangs below it. No closing rule — the top
   rule alone marks the section without the panel reading as boxed-in. */
.sub-header::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background:
    linear-gradient(var(--eu-cap), var(--eu-cap)) left center / 10px 100% no-repeat,
    linear-gradient(var(--eu-cap), var(--eu-cap)) right center / 10px 100% no-repeat,
    linear-gradient(var(--eu-rule), var(--eu-rule)) center center / 100% 1px no-repeat;
  pointer-events: none;
}

.sub-header .sub-context {
  color: var(--eu-ink-3);
  font-weight: 400;
}

/* OVERVIEW's cypher label override — same shape as .sub-context but
   turquoise, so the cypher number reads as the "blue at the top" of
   the OVERVIEW column (parallel position to CHANNELS · CYPHER N on
   the left, but accented to mark which rail it's calling out). */
.sub-header .sub-cypher {
  color: var(--turquoise);
  font-weight: 400;
  letter-spacing: var(--track-wide);
}

.sub-header .sub-meta {
  font-family: var(--font-num);
  color: var(--ink-dim);
  font-weight: 400;
  letter-spacing: var(--tracking-caption);
  margin-left: auto;
}

/* ============================================================
   Bindings list
   ============================================================ */
/* Source-SVG link in the cypher panel head. Subtle text link — the
   heading itself reads "CYPHER 1019", so this is just a side affordance
   to inspect the original artwork. */
.svg-link {
  font-size: 10px;
  letter-spacing: var(--track-ui);
  color: var(--ink-dim);
  text-decoration: none;
  transition: color 80ms linear;
}

.svg-link:hover {
  color: var(--turquoise);
}

/* ============================================================
   Voice panel — header carries the parameter name, and below it
   a stack of binding-block elements. Each binding-block represents
   one binding on this parameter: its own "bound to ATTRIBUTE [×]"
   line plus, for BIND/EVENT bindings, a pair of MIN/MAX editors.
   Blocks are separated by a thin rule so the eye can see which
   MIN/MAX belongs to which source.
   ============================================================ */

/* One binding's worth of header + editors. Separator above every block
   after the first, so the parameter header keeps clean alignment to
   its first binding while subsequent bindings read as a stack. */
.binding-block {
  padding-top: 8px;
}

.binding-block+.binding-block {
  border-top: 1px solid var(--line-dim);
  margin-top: 8px;
}

/* Collapsed binding row — always present at the top of each block.
   Numbered tile (positional index) + source name + chevron pointing
   right. Hovering reveals the delete × at the far right so the
   resting state stays uncluttered. Click the row to toggle
   expansion; the chevron rotates 90° clockwise to ▼ when expanded. */
.binding-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 0;
  cursor: pointer;
  font-size: 11px;
  letter-spacing: var(--track-caps);
}

.binding-row-index {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 22px;
  border: 1px solid var(--line-mid);
  color: var(--ink-dim);
  font-size: 9px;
  font-family: var(--font-mono, monospace);
  letter-spacing: var(--track-ui);
  flex-shrink: 0;
}

.binding-row-name {
  color: var(--turquoise);
  font-weight: 400;
  letter-spacing: var(--track-caps);
  flex-shrink: 0;
}

.binding-row-chevron {
  color: var(--ink-mid);
  font-size: 9px;
  line-height: 1;
  transition: transform 120ms ease-out;
  pointer-events: none;
  flex-shrink: 0;
}

.binding-block.selected .binding-row-chevron {
  transform: rotate(90deg);
}

.binding-row-spacer {
  flex: 1;
}

.binding-row-delete {
  background: transparent;
  border: none;
  color: var(--ink-dim);
  font-size: 11px;
  line-height: 1;
  padding: 0 4px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 120ms linear, color 80ms linear;
}

.binding-row:hover .binding-row-delete {
  opacity: 1;
}

.binding-row-delete:hover {
  color: var(--eu-magenta);
}

/* Per-binding header row — "bound to ATTRIBUTE" on the left, kept tight
   with the standard gap; the × button gets pushed to the far right via
   margin-left: auto on the button itself (defined below). */
.voice-head-binding {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 10px;
  letter-spacing: var(--track-ui);
}

.voice-head-binding .bound-to {
  color: var(--ink-dim);
}

.voice-head-binding .bound-attr {
  color: var(--turquoise);
  font-weight: 400;
  letter-spacing: var(--track-caps);
}

/* Spacer is a no-op now that each binding lives in its own block, but
   the CSS rule is kept harmless in case any old code path still emits it. */
.voice-head-spacer {
  flex: 1;
}

.voice-head-remove {
  background: transparent;
  border: none;
  color: var(--ink-dim);
  font-size: 11px;
  line-height: 1;
  padding: 0 4px;
  cursor: pointer;
  transition: color 80ms linear;
  margin-left: auto;
}

.voice-head-remove:hover {
  color: var(--eu-magenta);
}

.voice-binding-ctrls {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px 0 10px;
  margin-bottom: 6px;
}

/* ============================================================
   BINDINGS panel — DRIVE / ENVELOPE sub-blocks. Each sub-block has
   its own header and stack; the binding-block composition (.binding
   block / .voice-head-binding / .voice-binding-ctrls) is unchanged.
   ============================================================ */

.bindings-stack {
  display: flex;
  flex-direction: column;
  /* Inset to the title-bar caps' inner edges (10px each side). */
  padding: 0 10px;
}

/* Empty state — distinct visual weight from a populated stack. Reads
   as guidance, not as data. */
.bindings-empty {
  font-size: 11px;
  letter-spacing: var(--track-ui);
  color: var(--ink-dim);
  padding: 18px 4px;
  line-height: 1.5;
}

.bindings-empty .empty-param {
  color: var(--ink-mid);
  letter-spacing: var(--track-caps);
}

/* Per-sub-block quiet empty state — sits under the BINDINGS sub-header
   in italic-dim ink when the selected target has no bindings. */
.bindings-empty-quiet {
  font-size: 9px;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-mute);
  padding: 4px 0;
}

/* The binding-block as a whole is the click-target for selection. When
   a binding is selected, its background deepens so the user can see the
   selection at a glance even with multiple bindings stacked. */
.binding-block {
  cursor: pointer;
  position: relative;
}

.binding-block.selected {
  background: var(--bg-deep);
}

/* Shape indicator — small suffix on the binding header showing the
   attached shape when it's not LINEAR (the default). Reads as an
   at-a-glance affordance without needing to click each binding to
   see its shape. */
.voice-head-binding .bound-shape {
  color: var(--ink-mid);
  font-style: italic;
  letter-spacing: var(--track-ui);
  margin-left: 6px;
}

/* Shape row — always rendered inside a .binding-block.selected. Holds
   the inline shape picker (a grid of square shape cells). Sits below
   the MIN/MAX editors so the primary binding controls keep their
   place; LINEAR is the implicit default cell and is active when no
   shape is explicitly set on the binding. */
.binding-shape-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 6px 0 2px;
  font-size: 9px;
  letter-spacing: var(--track-caps);
  color: var(--ink-dim);
}

/* Shape picker — uniform square cells laid out in a grid that wraps
   to multiple rows. Each cell contains a curve glyph (top) and a
   short 2/3-letter abbreviation (bottom), both centered. Cell size is
   on the power-of-2 grid (48x48). Styled after the brainstem control-
   group reference — square tiles, monospaced caps, restrained colour. */
.binding-shape-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  /* SHAPE label is aligned to top of the picker (via parent row's
     align-items: flex-start); pad-top a hair so the row sits visually
     centered against the first cell row. */
  padding-top: 1px;
}

.binding-shape-btn {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 48px;
  height: 48px;
  box-sizing: border-box;
  background: transparent;
  border: 1px solid var(--line-mid);
  color: var(--ink-mid);
  padding: 4px;
  font: inherit;
  font-size: 9px;
  letter-spacing: var(--track-ui);
  cursor: pointer;
  transition: color 80ms linear, border-color 80ms linear, background 80ms linear;
}

.binding-shape-btn:hover {
  color: var(--turquoise);
  border-color: var(--turquoise);
}

.binding-shape-btn.active {
  background: var(--turquoise);
  color: var(--bg-void);
  border-color: var(--turquoise);
}

/* Glyph SVG — rendered at 24x24 inside the 48x48 cell so the curve
   reads at a glance. Stroke inherits the button's text colour via
   currentColor, so hover / active states cascade for free. */
.binding-shape-btn-glyph {
  width: 24px;
  height: 24px;
  display: block;
  pointer-events: none;
}

.binding-shape-btn-glyph path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.binding-shape-btn-label {
  display: block;
  font-family: var(--font-mono, monospace);
  letter-spacing: var(--track-tight);
}

/* Parameterised-shape sub-editor. Appears below the shape picker row
   when the selected shape declares one or more params. One control
   group per param: a NAME label, a range slider, and a numeric
   readout. The slider's input event live-updates binding.shapeParams
   AND redraws the currently-active picker cell's glyph in place, so
   the curve morphs visually as the slider drags — no separate preview
   widget needed, the cell the user is already looking at IS the
   feedback. The bindings panel is NOT re-rendered during the drag
   (would lose mouse capture); only the active glyph path's `d`
   attribute and the readout's text change in-place. */
.binding-shape-params {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 2px 0 4px;
}

.binding-shape-param {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 9px;
  letter-spacing: var(--track-caps);
  color: var(--ink-dim);
}

.binding-shape-param-name {
  min-width: 16px;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.binding-shape-param-slider {
  flex: 1;
  -webkit-appearance: none;
  appearance: none;
  height: 2px;
  background: var(--line-mid);
  outline: none;
  cursor: pointer;
}

.binding-shape-param-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 8px;
  height: 8px;
  background: var(--turquoise);
  border: none;
  cursor: pointer;
}

.binding-shape-param-slider::-moz-range-thumb {
  width: 8px;
  height: 8px;
  background: var(--turquoise);
  border: none;
  cursor: pointer;
  border-radius: 0;
}

.binding-shape-param-readout {
  font-family: var(--font-mono, monospace);
  font-size: 9px;
  color: var(--ink-mid);
  min-width: 32px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}



/* Verb-prefix variants on the binding-row header — `driven by` for
   DRIVE bindings, `envelope on` for legacy EVENT bindings. The
   prefix uses .bound-to styling (dim) and the value uses .bound-attr
   (accent), regardless of which verb produced the binding, so the
   row vocabulary reads as a sentence with the noun emphasised. */
.voice-head-binding .bound-verb {
  color: var(--ink-dim);
}

/* For envelope bindings: a small filter readout below the header,
   so two envelope bindings on the same rate are distinguishable
   (`envelope on SEGMENT` / `envelope on SEGMENT` reads identical
   without the filter tail). */
.binding-filter-tail {
  font-size: 9px;
  letter-spacing: var(--track-ui);
  color: var(--ink-dim);
  padding: 4px 0 6px;
}

.binding-filter-tail .filter-label {
  color: var(--ink-dim);
}

.binding-filter-tail .filter-value {
  color: var(--ink-mid);
  letter-spacing: var(--track-caps);
}

/* MIN / MAX row: label · mode select · slider+readout (or attr select) */
.bound-row {
  display: grid;
  grid-template-columns: 36px 64px 1fr auto;
  align-items: center;
  gap: 10px;
}

/* CMIN / CMAX quick-fill — small secondary button at the row's right edge.
   Base button gives the outline + turquoise hover; this only sizes it. */
.bound-cfill {
  font-size: 9px;
  letter-spacing: var(--track-caps);
  padding: 3px 7px;
  color: var(--ink-dim);
}

.bound-label {
  color: var(--ink-dim);
  font-size: 10px;
  letter-spacing: var(--track-wide);
}

.bound-mode {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: none;
  color: var(--ink-mid);
  font: inherit;
  font-size: 10px;
  letter-spacing: var(--track-caps);
  padding: 4px 6px;
  cursor: pointer;
  outline: none;
}

.bound-mode:focus {
  outline: none;
}

.bound-mode option,
.bound-attr-select option {
  background: var(--bg-deep);
  color: var(--ink-bright);
  font-family: var(--font-data);
  letter-spacing: var(--track-tight);
}

.bound-control {
  display: flex;
  align-items: center;
  gap: 10px;
}

.bound-slider {
  flex: 1;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  height: 20px;
  cursor: pointer;
  outline: none;
}

.bound-slider:focus {
  outline: none;
}

.bound-slider::-webkit-slider-runnable-track {
  height: 2px;
  background: var(--line-mid);
  border: none;
}

.bound-slider::-moz-range-track {
  height: 2px;
  background: var(--line-mid);
  border: none;
}

.bound-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 12px;
  background: var(--turquoise);
  margin-top: -5px;
  border: none;
  border-radius: 50%;
  cursor: grab;
}

.bound-slider::-moz-range-thumb {
  width: 12px;
  height: 12px;
  background: var(--turquoise);
  border: none;
  border-radius: 50%;
  cursor: grab;
}

.bound-slider:active::-webkit-slider-thumb {
  cursor: grabbing;
}

.bound-readout {
  color: var(--ink-bright);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  min-width: 36px;
  text-align: right;
}

.bound-attr-select {
  flex: 1;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: none;
  color: var(--ink-bright);
  font: inherit;
  font-size: 11px;
  padding: 4px 6px;
  outline: none;
}

.bound-attr-select:focus {
  outline: none;
}

/* ============================================================
   Synth params knob panel — direct knobs for synth registers
   ============================================================ */
/* marker-turquoise modifier — all markers use --turquoise by default
   under the muted Light Table palette, so this rule is a no-op. Kept
   so existing markup with the modifier doesn't need rewiring. */
.marker-turquoise {
  /* intentionally empty */
}

/* Bottom band: INSPECTOR | PARAMS side by side (BINDINGS is the rail).
   Inspector fixed-width on the left; Params take the rest and shift right
   against the rail. Reposition only — the sub-blocks keep their own styles. */
/* Power-of-two band: knobs are the 128px base unit; PARAMS packs 4 columns
   (square knobs) and the Inspector (3 units = 384px) anchors to its left.
   The whole group is right-aligned against the rail; dead space falls to the
   left (a future home for more controls). */
.synth-band {
  display: flex;
  justify-content: flex-end;
  gap: var(--gap-col);
  align-items: flex-start;
  min-width: 0;
}

/* MINIMAP — sits at the left of the synth band, in the band's dead space, so it
   reads as the lower-left module directly below the tracks. A square stage holds
   the concentric ring stack, centred on the left edge. */
.synth-band [data-block="minimap"] {
  /* As narrow as comfortably possible (DF-372): the ring stack is anchored to
     the left edge and only reaches one radius rightward, so the column needs
     little more than half the old width. */
  flex: 0 0 104px;
  margin-right: auto;   /* hug the left; the rest of the band stays right-aligned */
}

.minimap-body {
  margin-top: 8px;
  padding: 0 8px;
}

/* THICKNESS readout next to the indicator title. The column is narrow, so the
   header is pinned to a single line (no wrap) with the value hugging the right
   edge — dim with an em-dash until a band is selected, then brought up to ink. */
.synth-band [data-block="minimap"] .sub-header {
  flex-wrap: nowrap;
  gap: 8px;
}
.minimap-thickness {
  margin-left: auto;
  font-family: var(--font-num);
  color: var(--ink-mute);
}
.minimap-thickness[data-active="true"] {
  color: var(--eu-ink-1);
}

.eu-minimap {
  /* Half-width stage — the SVG viewBox spans one radius (the rings are centred
     on the left edge), so the stage is half as wide as it is tall. */
  aspect-ratio: 1 / 2;
  width: 100%;
  max-width: 90px;
}

.eu-minimap__svg { display: block; width: 100%; height: 100%; }

/* Ring circles — stroked bands coloured exactly like the OVERVIEW bars (each
   carries its hue as --ring-colour). No fill; the stroke fills the ring's real
   band, its width set inline per-circle to the band thickness. Unselected rings
   take the bar BODY weight (colour at low opacity); the selected ring lifts to
   the bar HEAD weight (full colour). */
.eu-minimap__band {
  fill: none;
  stroke: color-mix(in srgb, var(--ring-colour, var(--eu-ink-3)) 30%, transparent);
  cursor: pointer;
  transition: stroke 120ms ease;
}
.eu-minimap__band:hover {
  stroke: color-mix(in srgb, var(--ring-colour, var(--eu-ink-3)) 55%, transparent);
}
.eu-minimap__band.is-sel {
  stroke: var(--ring-colour, var(--eu-cyan));
}

.synth-band [data-block="params"] {
  flex: 0 0 auto;
}

/* The live SEGMENT ID next to the INSPECTOR title — dim until a segment is
   selected, then turquoise. */
.inspector-segid {
  color: var(--ink-mute);
}
.inspector-segid[data-active="true"] {
  color: var(--turquoise);
}

.inspector-body {
  margin-top: 8px;
  /* Inset to the title-bar caps' inner edges (10px each side). */
  padding: 0 10px;
}

.inspector-empty {
  font-size: 11px;
  letter-spacing: var(--track-ui);
  color: var(--ink-mute);
  font-style: italic;
  padding: 24px 4px;
}

.inspector-diagram { width: 100%; height: auto; display: block; }

/* Callout leader lines + anchor dots + label/value text, drawn inline in the
   diagram SVG (document CSS reaches them — not a <use> shadow tree). */
.insp-leader { stroke: var(--line-mid); stroke-width: 1; }
.insp-anchor { fill: var(--ink-bright); }
.insp-label {
  fill: var(--ink-dim);
  font-size: 8px;
  letter-spacing: var(--track-caps);
  font-family: var(--font-ui);
}
.insp-value {
  fill: var(--turquoise);
  font-size: 9px;
  letter-spacing: var(--track-ui);
  font-family: var(--font-ui);
}

/* Info list beneath the diagram — label · value, quiet caption row. */
.inspector-info {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
  margin-top: 6px;
  font-size: 10px;
  letter-spacing: var(--track-ui);
  color: var(--ink-dim);
}
.inspector-info .k { color: var(--ink-mute); }
.inspector-info .v { color: var(--ink-bright); margin-left: 5px; }

.knobs {
  display: grid;
  /* Two rows of six, sized to fit within the title-bar caps (10px inset each
     side): 88px cells + 6px column gap = 558px, with 10px side padding. Tight
     row-gap pulls the bottom row up into the space freed by removing the
     per-knob binding-name line. */
  grid-template-columns: repeat(6, 88px);
  column-gap: 6px;
  row-gap: 0px;
  justify-content: end;
  margin-top: 8px;
  padding: 0 10px;
}

.knob {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 88px;
  height: auto;
  user-select: none;
  padding: 8px 8px 4px;
  cursor: pointer;
  background: transparent;
  transition: background 80ms linear;
  position: relative;
  box-sizing: border-box;
  border: 1px solid transparent;
}

/* Selected param: four L-corner brackets at the tile corners (crop-mark style)
   — lighter than a full frame, same white "selected" ink. */
.knob.selected::before {
  content: "";
  position: absolute;
  inset: 2px;
  pointer-events: none;
  background:
    linear-gradient(var(--eu-ink-0), var(--eu-ink-0)) 0 0 / 5px 1px no-repeat,
    linear-gradient(var(--eu-ink-0), var(--eu-ink-0)) 0 0 / 1px 5px no-repeat,
    linear-gradient(var(--eu-ink-0), var(--eu-ink-0)) 100% 0 / 5px 1px no-repeat,
    linear-gradient(var(--eu-ink-0), var(--eu-ink-0)) 100% 0 / 1px 5px no-repeat,
    linear-gradient(var(--eu-ink-0), var(--eu-ink-0)) 0 100% / 5px 1px no-repeat,
    linear-gradient(var(--eu-ink-0), var(--eu-ink-0)) 0 100% / 1px 5px no-repeat,
    linear-gradient(var(--eu-ink-0), var(--eu-ink-0)) 100% 100% / 5px 1px no-repeat,
    linear-gradient(var(--eu-ink-0), var(--eu-ink-0)) 100% 100% / 1px 5px no-repeat;
}

/* NOISE placeholder — greyed out and non-interactive. */
.knob--disabled {
  opacity: 0.35;
  pointer-events: none;
  cursor: default;
}

.knob:hover {
  background: rgba(255, 255, 255, 0.025);
}

/* Selection indicator — v0.7 drops the L-corner brackets (Light Table
   vocabulary, retired). The selected knob reads through colour and the
   focused-state borders applied at .knob.selected on the dial and value. */

/* MODULES panel — the swappable audio blocks, listed in full to the left of
   the PARAMS knobs. One button per available module; the active one is ringed
   in turquoise. Replaces the old per-knob ⇄ icon + floating picker (DF-370). */
.synth-band [data-block="modules"] {
  flex: 0 0 200px;
}

.module-list {
  margin-top: 8px;
  padding: 0 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.module-list__head {
  font-size: 11px;
  letter-spacing: var(--track-caps);
  color: var(--ink-mid);
  text-transform: uppercase;
  padding: 0 0 4px;
}

.module-list__opt {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 100%;
  text-align: left;
  background: transparent;
  border: 1px solid var(--eu-rule-strong);
  color: var(--ink-bright);
  padding: 6px;
  margin-bottom: 4px;
  cursor: pointer;
  transition: background 80ms linear, border-color 80ms linear;
}

.module-list__opt:hover {
  background: rgba(255, 255, 255, 0.04);
}

.module-list__opt.is-active {
  border-color: var(--turquoise);
}

.module-list__name {
  font-size: 12px;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

.module-list__opt.is-active .module-list__name {
  color: var(--turquoise);
}

.module-list__note {
  font-size: 11px;
  color: var(--ink-mid);
  text-transform: none;
  line-height: 1.3;
}

.module-list__hint {
  font-size: 11px;
  color: var(--ink-dim);
  line-height: 1.3;
  padding: 2px 0;
}

.knob-dial {
  width: 64px;
  height: 64px;
  cursor: ns-resize;
}

.knob-dial svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.knob-track {
  fill: none;
  stroke: var(--line-dim);
  stroke-width: 2;
  stroke-linecap: round;
}

.knob-arc {
  fill: none;
  stroke: var(--turquoise);
  stroke-width: 2;
  stroke-linecap: round;
  transition: stroke 80ms linear;
}

.knob-pointer {
  stroke: var(--ink-bright);
  stroke-width: 1.5;
  stroke-linecap: round;
  transition: stroke 80ms linear;
}

.knob-label {
  font-size: 11px;
  letter-spacing: var(--track-caps);
  color: var(--ink-bright);
  text-align: center;
  line-height: 1.1;
  margin-top: 10px;
}

/* Value readout beneath the knob, horizontally centred under the dial.
   Literal 11px (and higher specificity) so the .eu-value 32px never wins. */
.knob .knob-value {
  position: absolute;
  top: 40px;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  pointer-events: none;
  font-family: var(--font-num);
  font-size: 11px;
  color: var(--ink-bright);
  font-variant-numeric: tabular-nums;
  text-align: center;
  letter-spacing: var(--track-tight);
}

/* DRIVE WITH attribute picker — a grid of clickable buttons grouped
   by SourceCategory (PHASE / CONTINUOUS / DISCRETE). Category headers
   span the full picker width above their button group. The hidden
   #f-attribute select shadows the click choice so existing form code
   (buildBindingFromForm, populateBoundAttributeOptions, etc.) still
   reads it normally. */
.attribute-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
}

.attribute-list .attribute-group-header {
  grid-column: 1 / -1;
  margin-top: 8px;
  padding-bottom: 4px;
  font-size: 8px;
  letter-spacing: var(--track-wide);
  color: var(--ink-dim);
  text-transform: uppercase;
}

/* No top margin on the first header so the picker doesn't open with
   awkward dead space above the first category. */
.attribute-list .attribute-group-header:first-child {
  margin-top: 0;
}

.attribute-list button {
  padding: 6px 8px;
  background: var(--bg-deep);
  color: var(--ink-mid);
  border: 1px solid transparent;
  font: inherit;
  font-size: 9px;
  letter-spacing: var(--track-ui);
  cursor: pointer;
  text-align: left;
  transition: background 80ms linear, color 80ms linear;
}

.attribute-list button:hover {
  background: var(--bg-deep);
  border-color: var(--eu-ink-3);
  color: var(--eu-ink-2);
}

.attribute-list button.selected {
  background: var(--bg-deep);
  border-color: var(--eu-ink-0);
  color: var(--eu-ink-0);
}

/* Inline description for the hovered or selected attribute in DRIVE WITH.
   Reserves a fixed slot so the modal layout doesn't jump as descriptions
   (or their absence) change. When nothing is shown, an italic hint
   prompts the user to hover. */
.attribute-description {
  margin-top: 8px;
  padding: 6px 8px;
  font-size: 10px;
  line-height: 1.4;
  letter-spacing: var(--track-tight);
  color: var(--ink-mid);
  min-height: 2.8em;
}

.attribute-description:empty::before {
  content: 'Hover an attribute to see what it provides.';
  font-style: italic;
  opacity: 0.5;
}

/* CONDITION field: select + NOT toggle on a single row */
.condition-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}

.condition-row select {
  flex: 1;
}

.condition-not {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  font-size: 10px;
  letter-spacing: var(--track-caps);
  color: var(--ink-dim);
  border: 1px solid var(--line-mid);
  cursor: pointer;
  user-select: none;
  transition: color 80ms linear, border-color 80ms linear, background 80ms linear;
}

.condition-not:hover {
  color: var(--turquoise);
  border-color: var(--turquoise);
}

.condition-not input {
  display: none;
}

.condition-not:has(input:checked) {
  background: var(--turquoise);
  color: var(--bg-void);
  border-color: var(--turquoise);
}

/* Knob label becomes the affordance for creating a BIND binding on this
   target. Cursor + colour shift on hover. Locked knobs (.modulated) have
   pointer-events:none so the label isn't clickable when a binding already
   owns the target. */
.knob-label {
  cursor: pointer;
  transition: color 80ms linear;
}

.knob-label:hover {
  color: var(--turquoise);
}

/* When a binding modulates this target the knob can't drive it (no drag,
   no rebind via label). Still clickable so it can focus the live view —
   inspecting a bound parameter shouldn't require unbinding it first. */
.knob.modulated .knob-dial {
  cursor: default;
}

.knob.modulated .knob-label {
  cursor: default;
}

.knob.modulated .knob-label:hover {
  color: var(--ink-bright);
}

.knob.modulated .knob-arc,
.knob.modulated .knob-pointer {
  opacity: 0.45;
}

/* Hide the knob's stored value when modulated — it's stale (the binding,
   not the knob, is what's actually driving the voice). */
.knob.modulated .knob-value {
  visibility: visible;
  color: #34383b;
}

/* When bound, hide the value arc so only the dotted track shows (the centre
   reads "BOUND"). */
.knob.modulated .eu-knob__arc {
  display: none;
}

/* ============================================================
   Live output
   ============================================================ */
.voices {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* OVERVIEW wrapper — mirrors .cypher-stripe on the left exactly so
   ring N here and ring N on the left share the same baseline. Same
   background, same padding, same nesting depth inside the sub-block.
   (Background hex matches the page bg, so the only visual effect is
   the breathing room around the data block — symmetric to cypher.) */
.voice {
  background: var(--bg-deep);
  padding: 4px 10px 22px 10px;
}

/* OVERVIEW timeline-strip — mirror of .timeline-row + .timeline-strip
   on the cypher side. Fixed 40px spacer on the left matches the
   .reg-value column width below so the strip's 0% sits at the same X
   as each bar's 0%, and 100% at each bar's right edge. Inside the
   strip, 5 ticks at 0 / 25 / 50 / 75 / 100 percent of bar width, each
   with its value label at the top of the 32px band — vertically
   identical to the cypher's time-label / tick positioning so ring N
   on the left sits at the same Y as ring N on the right. */
.voice-head {
  display: flex;
  align-items: center;
  gap: 0;
  height: var(--prerow-h);
  margin-bottom: 0;
}

.voice-head-spacer {
  flex: 0 0 40px;
}

.overview-strip {
  flex: 1;
  height: 32px;
  position: relative;
  overflow: hidden;
}

.overview-strip .tick-label {
  position: absolute;
  top: 2px;
  font-size: 9px;
  color: var(--ink-dim);
  letter-spacing: var(--track-tight);
  font-variant-numeric: tabular-nums;
  transform: translateX(-50%);
  pointer-events: none;
  white-space: nowrap;
}

/* Edge labels (0.00 at 0%, 1.00 at 100%) shift inward so they don't
   hang past the strip boundaries. */
.overview-strip .tick-label.start {
  transform: none;
  left: 0;
}

.overview-strip .tick-label.end {
  transform: none;
  right: 0;
  left: auto;
}

/* Muted-row treatment on OVERVIEW — mirror of .track.muted .track-segs
   on the cypher side. M/S buttons live on the cypher row; toggling
   them updates both columns. */
.track.muted .reg-bar .segment,
.track.muted .reg-value {
  opacity: 0.25;
}

.voices-empty {
  padding: 32px 16px;
  color: var(--ink-dim);
  font-size: 11px;
  letter-spacing: var(--track-caps);
  text-align: center;
}

/* Value readout — sits where the ring number used to be. The cypher tracks
   on the left side already number the rings; repurposing this slot for
   the value reads tighter and drops the redundant per-row 0-15 column.
   Typography matches .track-num on the cypher side (default font,
   10px, 0.08em letter-spacing) so the two columns read as one family.
   Colour stays --ink-bright to mark the active data readout vs the
   identifier in track-num. Fixed 40px column inside the .track flex
   row; matches its counterpart on the cypher side (the M/S buttons +
   ring-number group) in spirit, though not in width. */
.reg-value {
  flex: 0 0 40px;
  font-size: 10px;
  color: var(--ink-bright);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-ui);
  text-align: right;
}

.reg-value.inactive {
  color: var(--line-dim);
}

.reg-value.inactive+.reg-bar .segment {
  opacity: 0.3;
}

/* Voice bar — container for two segment-style children using the same
   visual language as cypher tracks: a BLOCK segment for the value
   (coloured), and a HOLLOW segment for the remainder (grey frame).
   Both get CAP top + CAP bottom and PADDING for the inset clip. Sits
   inside a .track row, taking the remaining width after the .reg-value
   readout. Height is the same 14px as cypher's track-segs so any
   segment styling that fits one fits the other. */
.reg-bar {
  position: relative;
  flex: 1;
  min-width: 0;
  height: 14px;
  background: var(--bg-void);
}

/* Voice bars don't want the cypher-tracks click-to-select interaction
   — they're observation-only. Override the .segment defaults here. */
.reg-bar .segment {
  cursor: default;
  pointer-events: none;
}

/* Overview-bar colour token — every segment gets the bright accent
   regardless of fill type or binding state. The .bound class is still
   toggled by the renderer (for any future use) but no longer carries
   a visual distinction here: dim variants were hard to read at this
   size, so the panel commits to bright across the board. Inline
   --seg-colour (set per-tick when a cypher segment is crossing 0°
   on the ring) overrides this with the per-segment artwork hue —
   which is itself brightened in computeActiveSegmentColours so that
   low-luminance artwork colours stay readable in the overview. */
.reg-bar .segment {
  --seg-colour: var(--turquoise);
}

/* ============================================================
   Modal
   ============================================================ */
.modal-veil {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  display: none;
  align-items: flex-start;
  justify-content: center;
  padding: 80px 20px;
  z-index: 100;
}

.modal-veil.open {
  display: flex;
}

.modal {
  background: var(--bg-panel);
  width: 460px;
  max-width: 100%;
}

.modal-head {
  display: flex;
  align-items: center;
  padding: 14px 20px;
  background: var(--bg-deep);
}

.modal-head h3 {
  margin: 0;
  flex: 1;
  font-weight: 500;
  letter-spacing: var(--track-wide);
  font-size: 11px;
  color: var(--ink-bright);
}

.modal-head .close {
  color: var(--ink-dim);
  border: none;
  background: transparent;
  padding: 4px 8px;
  font-size: 11px;
  cursor: pointer;
}

.modal-body {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* The picker body — always-visible since the modal has a single purpose
   now (source-attribute selection). */
.picker-body {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.field[data-hidden="true"] {
  display: none;
}

.field-label {
  font-size: 11px;
  color: var(--ink-mid);
  letter-spacing: var(--track-wide);
}

.field-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

.bound-field .bound-row {
  display: flex;
  gap: 6px;
}

.bound-field .bound-mode {
  flex: 0 0 78px;
  font-size: 10px;
  padding: 6px 6px;
  letter-spacing: var(--track-ui);
  color: var(--turquoise);
}

.bound-field .bound-value {
  flex: 1;
  min-width: 0;
}

.bound-field .bound-value[data-hidden="true"] {
  display: none;
}

.valuemap-row {
  display: grid;
  grid-template-columns: 110px 1fr;
  gap: 10px;
  align-items: center;
}

.valuemap-row .label {
  color: var(--ink-mid);
  font-size: 11px;
  letter-spacing: var(--track-tight);
}

.modal-foot {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding: 14px 20px;
  background: var(--bg-deep);
}

/* ADD — primary commit (carries .btn-primary in markup). Only padding
   differs from the rank here. */
.modal-foot .submit {
  padding: 8px 18px;
}

/* Enabled ADD reads clearly clickable (white outline + label); disabled stays
   dim grey. Without this the unified .btn-primary grey made "valid" look the
   same as "disabled". */
.modal-foot .submit:not(:disabled) {
  border-color: var(--eu-ink-1);
  color: var(--eu-ink-1);
}

.modal-foot .submit:not(:disabled):hover {
  border-color: var(--eu-ink-0);
  color: var(--eu-ink-0);
}

/* The binding-rail action row — holds the ADD A BINDING button. */
.bindings-actions {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}

.bindings-actions .bindings-add {
  flex: 1;
  margin-top: 0;
}

/* ============================================================
   Patch save/load
   ============================================================ */
.patch-zone {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* SAVE AS row — name field flush against its save button. */
.patch-save-row {
  display: flex;
  gap: 8px;
}

.patch-save-row input {
  flex: 1;
  min-width: 0;
}

/* SAVE — primary commit (carries .btn-primary). border lives at rest now
   (via .btn-primary), so the hover no longer adds a 1px border → the old
   1px layout shift on hover is gone. */
.patch-save-row .submit {
  flex: 0 0 auto;
  padding: 6px 16px;
}

/* Patch notes — free-text reminder saved alongside the patch. Sits between
   the save row and the inline feedback line; vertical resize only so it
   can't break the modal's column width. */
.patch-notes {
  width: 100%;
  margin-top: 8px;
  resize: vertical;
  line-height: 1.4;
}

/* Inline feedback under the save row ("Saved", "Name a patch first"). */
.patch-note {
  font-size: 11px;
  letter-spacing: var(--track-ui);
  color: var(--ink-dim);
  min-height: 14px;
}

.patch-note[data-tone="ok"] { color: var(--turquoise); }
.patch-note[data-tone="warn"] { color: var(--eu-magenta); }

/* Saved-patch list — one row per patch, name + LOAD. */
.patch-list {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line-dim);
  max-height: 220px;
  overflow-y: auto;
}

.patch-list-empty {
  padding: 12px 12px;
  font-size: 11px;
  letter-spacing: var(--track-ui);
  color: var(--ink-mute);
  font-style: italic;
}

.patch-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--line-dim);
}

.patch-row:last-child { border-bottom: none; }

.patch-row .patch-row-name {
  flex: 1;
  min-width: 0;
  font-size: 11px;
  letter-spacing: var(--track-tight);
  color: var(--ink-bright);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.patch-row button {
  flex: 0 0 auto;
  padding: 4px 12px;
  font-size: 10px;
  letter-spacing: var(--track-caps);
  color: var(--turquoise);
  border-color: var(--turquoise-dim);
}

.patch-row button:hover {
  background: var(--turquoise);
  color: var(--bg-void);
  border-color: var(--turquoise);
}

/* DOWNLOAD is secondary to LOAD — muted until hovered, so the row's
   primary action (LOAD) stays the one that reads as turquoise. */
.patch-row button.patch-download {
  color: var(--ink-mute);
  border-color: var(--line-dim);
  letter-spacing: var(--track-ui);
}

.patch-row button.patch-download:hover {
  background: transparent;
  color: var(--ink-bright);
  border-color: var(--ink-mute);
}

/* Skip-warning surface — appears after a load that dropped references.
   Orange reads as "attention" within the existing palette without a
   new colour. Each line is one dropped binding or param + its reason. */
.patch-warnings {
  border-top: 1px solid var(--line-dim);
  padding-top: 14px;
}

.patch-warning-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 160px;
  overflow-y: auto;
}

.patch-warning {
  font-size: 11px;
  line-height: 1.4;
  letter-spacing: var(--track-tight);
  color: var(--eu-magenta);
  padding-left: 14px;
  position: relative;
}

.patch-warning::before {
  content: "!";
  position: absolute;
  left: 0;
  color: var(--eu-magenta);
}

/* Hidden by default until shown via JS */
[data-hidden="true"] {
  display: none !important;
}

/* ============================================================
   Track row scaffolding — used by both the cypher tracks (left side)
   and the OVERVIEW voice rows (right side). Sharing this base class is
   the alignment contract: same --row-h, same --row-gap, same
   .channel-boundary treatment, so ring N on the left always sits at
   exactly the Y of ring N on the right. Cypher-specific behaviour
   (clickable to focus, mute/solo, selection border) layers on through
   .cypher-tracks .track selectors below. The voice rows use the bare
   class with no interactivity.
   ============================================================ */
.cypher-tracks,
.voice-tracks {
  display: flex;
  flex-direction: column;
  gap: var(--row-gap);
}

.track {
  display: flex;
  align-items: center;
  gap: 10px;
  height: var(--row-h);
  padding: 0 6px 0 0;
}

/* Cypher-side interactive surface — selectable rows with a left-edge
   focus border and a hover lift. Voice rows skip all of this. */
.cypher-tracks .track {
  background: var(--bg-deep);
  cursor: pointer;
  border-left: 2px solid transparent;
  transition: border-color 80ms linear, background 80ms linear;
}

.cypher-tracks .track:hover {
  background: var(--bg-panel);
}

.cypher-tracks .track.selected {
  border-left-color: var(--turquoise);
  background: var(--bg-panel);
}

/* MUTE / SOLO — small outline buttons matching the ENABLE AUDIO /
   ENABLE MIDI style, but in greys: less important visual rank than
   primary actions, equal weight to each other (M vs S distinguishes
   function, not colour). */
.track-solo,
.track-mute {
  width: 22px;
  height: 14px;
  padding: 0;
  font-size: 8px;
  letter-spacing: var(--track-ui);
  color: var(--ink-dim);
  background: transparent;
  border: 1px solid var(--line-mid);
  cursor: pointer;
  flex-shrink: 0;
  transition: color 80ms linear, background 80ms linear, border-color 80ms linear;
}

.track-solo:hover,
.track-mute:hover {
  color: var(--ink-bright);
  border-color: var(--ink-mid);
}

.track-solo.active,
.track-mute.active {
  background: var(--ink-mid);
  color: var(--bg-void);
  border-color: var(--ink-mid);
}

.track-num {
  width: 22px;
  font-size: 10px;
  color: var(--ink-mid);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-ui);
  flex-shrink: 0;
  text-align: right;
}

.track-segs {
  flex: 1;
  height: 14px;
  display: block;
  position: relative;
  background: var(--bg-void);
}

/* SVG track substrate — one <svg> per active row fills .track-segs. Each
   segment's geometry is defined once in <defs>; the timeline tiling is
   <use> references (one per repeat), built once per load in track-svg.js.
   preserveAspectRatio="none" stretches the 0..100 x-axis to the strip
   width, so strokes whose width runs along x — vertical end caps/edges, the
   HOLLOW frame — set vector-effect="non-scaling-stroke" inline (a
   presentation attribute, so it survives <use>; a CSS class would not reach
   into the <use> shadow tree and the strokes would scale up fat). Horizontal
   span lines keep 1px for free (their width is the unscaled y-axis). This
   replaces the per-segment div + 2-pseudo tiling that physically duplicated
   geometry (the .segment rules below now serve only the OVERVIEW bars). */
.track-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  overflow: hidden;   /* clip a tile straddling the right edge — as the old min(segW,100-left) clamp did */
}

.track-svg use {
  pointer-events: all;   /* whole segment box hit-testable, even hollow / strokes-only */
  cursor: pointer;
}

/* Selection — bright outline over every repeat of the selected segment,
   drawn into the per-row .seg-sel layer. Mirrors the old
   `.segment.selected { outline }`. */
.track-svg .seg-sel .sel {
  fill: none;
  stroke: var(--ink-bright);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* A steady white square over the selected segment so it's easy to pick out.
   plus-lighter is an additive blend, so the fill only lifts the segment's own
   pixels — the lane behind it never glows. (Static, not breathing.) */
.track-svg .seg-sel .sel-pulse {
  fill: #fff;
  stroke: none;
  mix-blend-mode: plus-lighter;
  pointer-events: none;
  opacity: 0.2;
}

/* ============================================================
   Segment rendering — a visual mini-language. Originated in the cypher
   tracks (translating the Cypher generator's SpanType/FillType/EdgeType/
   PadType vocabulary into CSS). As of the SVG-substrate work the cypher
   tracks render via SVG (see .track-svg above / track-svg.js), so these
   .segment rules now serve only the OVERVIEW voice bars — which reuse the
   same vocabulary (a single span-inner stroke) so the two readouts share a
   typographic family. Each .segment is one absolutely-positioned div, with
   visuals driven from data-attributes and a per-segment --seg-colour
   custom property. All strokes are 1px — character expresses through
   pattern, not weight.

   Anatomy:
     data-span-outer  (0-5)  → top stroke (::before)
     data-span-inner  (0-5)  → bottom stroke (::after)
     data-edge        (0-1)  → start/end vertical strokes (borders)
     data-fill        (0-5)  → interior treatment (stackable for COMP)
     data-padding     (0-1)  → inset clip on the segment
     --seg-colour            → per-segment hue (from cypher data, or
                                set explicitly by other consumers)

   SpanType enum:  0 NONE | 1 ARC | 2 CAP | 3 ANG | 4 BRK | 5 DOTTED
   FillType enum:  0 NONE | 1 BLOCK | 2 HOLLOW | 3 TEXT | 4 INCREMENT | 5 COMP
   Edge enum:      0 NONE | 1 SIMPLE
   Pad enum:       0 NONE | 1 SINGLE

   Default .segment is `cursor: pointer; pointer-events: auto;` (set
   up for the cypher-track click-to-select flow). Voice bars don't
   want either, and override both via .reg-bar .segment.
   ============================================================ */

.segment {
  position: absolute;
  top: 0;
  bottom: 0;
  cursor: pointer;
  pointer-events: auto;
}

/* Outer (top) + inner (bottom) strokes — drawn as 1px tall pseudo
   elements that span the segment width. Each variant overrides the
   background of these elements to express its pattern. */
.segment::before,
.segment::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--seg-colour);
  pointer-events: none;
}

.segment::before {
  top: 0;
}

.segment::after {
  bottom: 0;
}

/* SpanType — outer (top stroke).
   0 NONE   — hide
   1 ARC    — solid (default)
   2 CAP    — solid + 2px end pips via box-shadow
   3 ANG    — corner ticks (solid at ends, dim middle)
   4 BRK    — dashed bookends (solid ends, gap middle)
   5 DOTTED — 1×2px repeating dots                                 */
.segment[data-span-outer="0"]::before {
  display: none;
}

.segment[data-span-outer="2"]::before {
  /* end ticks: short horizontal extensions at each end, same 1px height
     as the base stroke — uniform weight across all SpanType variants. */
  box-shadow:
    2px 0 0 0 var(--seg-colour),
    -2px 0 0 0 var(--seg-colour);
}

.segment[data-span-outer="3"]::before {
  background: linear-gradient(to right,
      var(--seg-colour) 0,
      var(--seg-colour) min(4px, 40%),
      color-mix(in srgb, var(--seg-colour) 35%, transparent) min(4px, 40%),
      color-mix(in srgb, var(--seg-colour) 35%, transparent) calc(100% - min(4px, 40%)),
      var(--seg-colour) calc(100% - min(4px, 40%)),
      var(--seg-colour) 100%);
}

.segment[data-span-outer="4"]::before {
  background: linear-gradient(to right,
      var(--seg-colour) 0, var(--seg-colour) min(3px, 40%),
      transparent min(3px, 40%), transparent calc(100% - min(3px, 40%)),
      var(--seg-colour) calc(100% - min(3px, 40%)), var(--seg-colour) 100%);
}

.segment[data-span-outer="5"]::before {
  background: repeating-linear-gradient(to right,
      var(--seg-colour) 0, var(--seg-colour) 1px,
      transparent 1px, transparent 3px);
}

/* SpanType — inner (bottom stroke). Mirror of outer; box-shadows
   drop down rather than up. */
.segment[data-span-inner="0"]::after {
  display: none;
}

.segment[data-span-inner="2"]::after {
  box-shadow:
    2px 0 0 0 var(--seg-colour),
    -2px 0 0 0 var(--seg-colour);
}

.segment[data-span-inner="3"]::after {
  background: linear-gradient(to right,
      var(--seg-colour) 0,
      var(--seg-colour) min(4px, 40%),
      color-mix(in srgb, var(--seg-colour) 35%, transparent) min(4px, 40%),
      color-mix(in srgb, var(--seg-colour) 35%, transparent) calc(100% - min(4px, 40%)),
      var(--seg-colour) calc(100% - min(4px, 40%)),
      var(--seg-colour) 100%);
}

.segment[data-span-inner="4"]::after {
  background: linear-gradient(to right,
      var(--seg-colour) 0, var(--seg-colour) min(3px, 40%),
      transparent min(3px, 40%), transparent calc(100% - min(3px, 40%)),
      var(--seg-colour) calc(100% - min(3px, 40%)), var(--seg-colour) 100%);
}

.segment[data-span-inner="5"]::after {
  background: repeating-linear-gradient(to right,
      var(--seg-colour) 0, var(--seg-colour) 1px,
      transparent 1px, transparent 3px);
}

/* EdgeType.SIMPLE — radial start/end strokes as 1px borders. Colour
   reduced to 50% intensity so the rectangle reads as a hairline frame
   at any density without sub-pixel border-width fragility. */
.segment[data-edge="1"] {
  border-left: 1px solid color-mix(in srgb, var(--seg-colour) 50%, transparent);
  border-right: 1px solid color-mix(in srgb, var(--seg-colour) 50%, transparent);
}

/* FillType — interior treatment. Intensities are kept low so COMP
   (which stacks BLOCK + INCREMENT + HOLLOW frame) reads as composition,
   not a solid colour block. */
.segment[data-fill="1"],
/* BLOCK */
.segment[data-fill="5"] {
  /* COMP includes BLOCK */
  background-color: color-mix(in srgb, var(--seg-colour) 15%, transparent);
}

.segment[data-fill="4"],
/* INCREMENT */
.segment[data-fill="5"] {
  /* COMP includes INCREMENT */
  background-image: repeating-linear-gradient(to right,
      color-mix(in srgb, var(--seg-colour) 35%, transparent) 0,
      color-mix(in srgb, var(--seg-colour) 35%, transparent) 1px,
      transparent 1px, transparent 10px);
}

.segment[data-fill="2"],
/* HOLLOW */
.segment[data-fill="5"] {
  /* COMP adds HOLLOW frame back on top */
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--seg-colour) 30%, transparent);
}

/* Padding — visual inset suggesting the SVG's pad behaviour. The inset
   is clamped to 20% of segment width so that narrow segments (e.g.
   sub-rotation rings like cypher_1019 ring 0 at ~5px wide) don't get
   erased by a fixed 2px+2px clip exceeding their box width. At
   typical (>10px) widths the cap is moot; below that the pad signal
   weakens proportionally rather than annihilating the segment. */
.segment[data-padding="1"] {
  clip-path: inset(0 min(2px, 20%) 0 min(2px, 20%));
}

/* Selection — bright outline overrides the dim edge treatment. */
.segment.selected {
  outline: 1px solid var(--ink-bright);
  outline-offset: 0;
  z-index: 1;
}

/* Tracks audibly muted (M active, or non-solo when any solo active) */
.track.muted .track-segs {
  opacity: 0.25;
}

.track.channel-boundary {
  margin-top: 2px;
  border-top: 1px solid var(--line-dim);
}

/* Dead-space rows — inactive-channel slots in the fixed 16-row grid. Inert:
   no pointer, no hover lift, not selectable. A faint diagonal hatch marks
   the frequency dead space so it reads as "nothing here", distinct from an
   empty-but-live track. The spacer fills the [M][S] gutter so the hatch
   band lines up with the active rows' segment column. */
.cypher-tracks .track.dead,
.cypher-tracks .track.dead:hover {
  background: var(--bg-deep);
  cursor: default;
  border-left-color: transparent;
}

.track-deadgap {
  width: 54px;   /* M (22) + gap (10) + S (22) — keeps track-segs aligned */
  flex-shrink: 0;
}

.track.dead .track-segs {
  background-color: var(--bg-void);
  background-image: repeating-linear-gradient(
    45deg, rgba(80, 80, 90, 0.18) 0 1px, transparent 1px 7px);
}

/* The cypher stripe — single unified background covering the timeline,
   the 16 ring tracks, and a matching band of padding below the last
   ring (same height as the timeline). This is the only filled region
   once module backgrounds are stripped; it reads as a clean horizontal
   band against the page. Padding is generous so the data has room to
   breathe; the stripe spans the viewport edge-to-edge. */
.cypher-stripe {
  background: var(--bg-deep);
  padding: 4px 10px 22px 0;
  position: relative;   /* containing block for the play-head overlay */
  /* Extend the stripe leftward across the gutter (without moving the section
     header, which stays in the content column) so each track row's controls —
     index + M + S — sit in the left gutter and the lane begins at the content
     edge, aligned with the OVERVIEW row to its right: one row across the page. */
  width: calc(100% + var(--col-label) + var(--gap-col) + var(--page-pad) - 8px);
  margin-left: calc(-1 * (var(--col-label) + var(--gap-col) + var(--page-pad) - 8px));
}

/* Play head — a thin vertical cursor over the timeline + tracks, riding
   elapsed time. It's the transport-live timing cursor, so it's orange (the
   sanctioned alarm/transport-live colour). Position is a single per-frame
   transform (no track re-emit); horizontal offset/width come from the
   cached timeline-strip geometry so it aligns with the ticks and the
   segment columns. Hidden at rest (stopped at position 0). */
.play-head {
  position: absolute;
  top: 68px;
  bottom: 22px;
  left: 0;
  width: 1px;
  background: var(--eu-cyan);
  pointer-events: none;
  opacity: 0;
  transform: translateX(-10px);
  will-change: transform;
  z-index: 3;
}

/* TIMELINE STRIP — second ticks + segment marks, aligned over the tracks.
   Pure HTML positioned absolutely (no SVG) so labels never get distorted
   by the non-uniform horizontal scaling. */
.timeline-row {
  display: flex;
  align-items: center;
  gap: 0;
  height: var(--prerow-h);
  margin: 0;
  /* Start the ruler strip at the left title-bar cap's inner edge (content
     column left + 10px cap), so the timeline + lanes sit inside the caps; the
     stripe's 10px right padding brings the right end inside the right cap. */
  padding-left: calc(var(--page-pad) + var(--col-label) + var(--gap-col) - 8px + 10px);
  padding-right: 0;
}

.timeline-spacer {
  display: none;
}

.timeline-strip {
  flex: 1;
  height: 32px;
  position: relative;
  overflow: hidden;
}

/* Shared tick rendering for both .timeline-strip and .overview-strip.
   One <svg> per strip with two <path> elements (minor + major); paths
   are bottom-anchored at y=32 and rise to y=28 (minor) or y=24 (major).
   vector-effect keeps strokes 1px while preserveAspectRatio="none"
   stretches the viewBox horizontally to fill the strip. Replaces the
   per-tick <div> divs used in v0.7 — ~130 divs collapse to one SVG. */
.tick-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.tick-svg path {
  fill: none;
  stroke: var(--line-mid);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.tick-svg path.minor {
  opacity: 0.6;
}

.timeline-strip .time-label {
  position: absolute;
  top: 2px;
  font-size: 9px;
  color: var(--ink-dim);
  letter-spacing: var(--track-tight);
  font-variant-numeric: tabular-nums;
  transform: translateX(-50%);
  pointer-events: none;
  white-space: nowrap;
}

/* Cypher attribute stats (LEVEL / INTRICACY / DECAY / CHAOS / CHANNELS)
   — rendered into #cypher-stats inside the CHANNELS sub-header. Reads
   as quiet caption metadata, not as a primary readout. */
#cypher-stats {
  display: inline-flex;
  flex-wrap: wrap;
  min-width: 0;
  gap: 4px 14px;
  color: var(--ink-dim);
  font-size: var(--fs-caption);
  letter-spacing: var(--tracking-caption);
}

/* Label-value pairs (LEVEL 1, INTRICACY 5, …): WHITE label, then the value
   close beside it in a darker grey. */
#cypher-stats .attr {
  color: var(--eu-ink-1);
}

#cypher-stats .attr .val {
  color: var(--eu-ink-3);
  margin-left: 4px;
}

/* Cypher loading controls — load-by-id, random-by-level (sticky filter),
   prev/next steppers. Live in the top header band alongside the transport
   cluster (one band, one size); the cyc-* sizes below are the band's single
   control size, which the transport buttons match. */
.cypher-controls {
  display: flex;
  align-items: center;
  gap: 14px;
}

.cyc-group {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.cyc-label {
  font-size: 10px;
  letter-spacing: var(--track-caps);
  color: var(--ink-mid);
}

.cyc-input {
  background: var(--bg-void);
  color: var(--ink-bright);
  border: 1px solid var(--line-dim);
  font: inherit;
  font-size: 11px;
  letter-spacing: var(--track-tight);
  width: 64px;
  padding: 3px 6px;
}

.cyc-input:focus {
  outline: none;
  border-color: var(--turquoise-dim);
}

/* Shared button look for LOAD / LOAD RANDOM / steppers / level cells.
   Rest reads at ink-mid on a line-mid border so the controls are legibly
   "live" before hover (the old ink-dim/line-dim rest was near-invisible to
   a first-time user); hover lifts to turquoise, the lit level fills. */
.cyc-btn,
.cyc-step-btn,
.cyc-level {
  background: transparent;
  color: var(--ink-mid);
  border: 1px solid var(--line-mid);
  font: inherit;
  font-size: 10px;
  letter-spacing: var(--track-caps);
  padding: 3px 10px;
  cursor: pointer;
  transition: color 80ms linear, border-color 80ms linear, background 80ms linear;
}

.cyc-step-btn {
  font-size: 11px;
  line-height: 1;
  letter-spacing: 0;
  padding: 3px 8px;
}

.cyc-btn:hover,
.cyc-step-btn:hover:not(:disabled),
.cyc-level:hover {
  color: var(--turquoise);
  border-color: var(--turquoise-dim);
}

.cyc-step-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

/* Level filter cells spaced like the cypher-id input ↔ LOAD gap (6px), each
   with its own border. */
.cyc-levels {
  display: inline-flex;
  gap: 6px;
}

.cyc-levels .cyc-level {
  padding: 3px 9px;
}

.cyc-level[data-active="true"] {
  color: var(--eu-ink-0);
  background: transparent;
  border-color: var(--eu-ink-0);
}

/* Inline feedback (id not in collection, empty level pool). */
.cyc-note {
  font-size: 10px;
  letter-spacing: var(--track-tight);
  color: var(--eu-magenta);
  min-height: 12px;
}

.cyc-note[data-tone="ok"] { color: var(--turquoise); }

/* ============================================================
   Onboarding — first-run welcome + replayable coachmark tour.
   The welcome reuses .modal styling. The tour is a spotlight veil
   (one darkened overlay with a lit cut-out over the named region)
   plus a card. Both sit above the modals so the tour can point at
   live controls. Pure chrome — no engine or draw-path involvement.
   ============================================================ */
.welcome-copy {
  margin: 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-mid);
}

.tour-veil {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: none;
}

.tour-veil.open { display: block; }

/* The spotlight: a transparent box whose huge spread shadow darkens
   everything outside it, leaving the named region lit and ringed in
   cyan. Razor-sharp corners (no rounding, per breif-v2 §1). Position
   transitions so stepping glides between regions. pointer-events:none
   so the click lands on the veil behind (the tour owns interaction
   while it's up). */
.tour-spot {
  position: absolute;
  border-radius: 0;
  box-shadow: 0 0 0 9999px rgba(7, 8, 8, 0.82);
  outline: 1px solid var(--turquoise);
  outline-offset: 3px;
  pointer-events: none;
  transition: top 180ms ease, left 180ms ease, width 180ms ease, height 180ms ease;
}

/* No on-screen target → collapse the ring, keep the screen dimmed, and
   the card centres itself (fallback path in positionTour). */
.tour-spot[data-empty="true"] {
  outline: none;
  width: 0;
  height: 0;
}

.tour-card {
  position: absolute;
  width: 300px;
  max-width: calc(100vw - 32px);
  background: var(--bg-panel);
  border: 1px solid var(--line-bright);
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  transition: top 180ms ease, left 180ms ease;
}

.tour-step {
  font-family: var(--font-num);
  font-size: 10px;
  letter-spacing: var(--track-ui);
  color: var(--turquoise);
}

.tour-title {
  margin: 0;
  font-weight: 400;
  font-size: 11px;
  letter-spacing: var(--track-wide);
  color: var(--ink-bright);
}

.tour-text {
  margin: 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-mid);
}

.tour-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 4px;
}

.tour-nav { display: flex; gap: 6px; }

.tour-foot button {
  padding: 4px 12px;
  font-size: 10px;
}

/* breif-v2 visual-standard pass — re-served */

/* The pointer notch is removed — the dotted track + solid arc already show the
   value; the end-of-progress line is no longer needed. */
.eu-knob__pointer {
  display: none;
}

/* ============================================================
/* ============================================================
   §7 — CYPHER track selection signature
   Selection is shown by the track-number button (white border + white number
   when the row is selected); there is no separate selection tab.
   ============================================================ */
/* The track number is a third button in the M/S group — same hairline-square
   look, with the number as its label. It selects the row on click (handled by
   the row click handler; it deliberately has no .eu-ms class so it doesn't
   trigger mute/solo). Control width (M+S+#) is 88px, so the lane stays aligned
   to the content column. */
.eu-track__index {
  width: 28px;
  justify-content: center;
  margin: 4px 0 4px 4px;
  border: 1px solid transparent;
  color: var(--eu-ink-3);
  font-size: var(--eu-t-cap);
  letter-spacing: var(--track-ui);
  cursor: pointer;
  padding-left: 0;
}

.eu-track__index:hover {
  border-color: var(--eu-ink-3);
  color: var(--eu-ink-2);
}

.eu-track.is-sel .eu-track__index {
  border-color: var(--eu-cyan);
}

/* Direction indicator at the right end — same hairline-square look as M/S.
   ‹ = CCW, › = CW. Lives in the right gutter (the stripe is extended right to
   hold it) so the lane still ends at the panel edge. */
.eu-track__dir {
  flex: 0 0 24px;
  width: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 4px 0 4px 16px;
  border: 1px solid color-mix(in srgb, var(--eu-ink-3) 20%, transparent);
  color: var(--eu-ink-3);
  font-size: var(--eu-t-cap);
  letter-spacing: var(--track-ui);
  cursor: pointer;
  box-sizing: border-box;
}

.eu-track__dir:hover {
  border-color: var(--eu-ink-3);
  color: var(--eu-ink-2);
}

/* Clockwise rings: same colour as CCW (--eu-ink-3, the default) but at 5%
   opacity, so CW is all but invisible while CCW reads at full strength. */
.eu-track__dir[data-dir="cw"] {
  opacity: 0.05;
}

/* Counter-clockwise indicator — a small ‹ arrow that sits in the gutter between
   the track-number button and the lane's left edge. Only emitted for CCW rings,
   so CW (the default) shows nothing. Absolutely positioned so it never shifts
   the lane out of the content column. */
.eu-track__ccw {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--page-pad) + var(--col-label) + var(--gap-col) - 8px + 10px - 18px);
  width: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--eu-ink-2);
  font-size: var(--eu-t-cap);
  pointer-events: none;
}

/* Line the lane (and its segments) up with the CHANNELS panel's left edge
   (the content column), then inset by the 10px title-bar cap so nothing below
   the rule extends past the caps' inner edges. */
.eu-lane {
  margin-left: calc(var(--page-pad) + var(--col-label) + var(--gap-col) - 8px - 88px + 10px);
}

/* Selection / loop highlight on the linear track lanes (linear-tracks.js).
   A clicked segment's group gains `is-sel`; a looped segment's group gains
   `is-loop`; an ARC loop lights the whole lane via `.eu-lane__loop`. The
   highlight is a DUPLICATE of the segment's own art composited additively
   (plus-lighter), so the segment lifts in its own colour — no white wash, and
   the lane behind it never glows. A brightness lift sets the strength; the loop
   reads brighter (it's pinned, not just inspected). */
.eu-seg__dup {
  mix-blend-mode: plus-lighter;
  pointer-events: none;
}
.eu-seg.is-sel  .eu-seg__dup { filter: brightness(1.8); }
.eu-seg.is-loop .eu-seg__dup { filter: brightness(2.6); }

/* ARC loop: a faint full-lane wash behind the segments. */
.eu-lane__loop {
  fill: var(--ink-bright);
  opacity: 0.1;
}

/* ============================================================
   §6 / §8 — OVERVIEW as ring-node meters on the track grid
   The OVERVIEW shows the selected parameter's value across all 16 rings.
   Each row now shares the exact track metric — 32px row height + a single
   1px bottom rule, no inter-row gap — so the column reads as a literal
   continuation of the 16 tracks (§8), not a bolted-on widget. The bar is
   the §6 meter signature: a hollow ring node at the origin, a solid value
   fill drawn over a dotted connector (2px on / 4px off), and an end node
   that fills at full value. Colour is signal only: green for an unbound
   target, cyan when a binding owns it, overridden per-frame by the active
   segment's hue.
   ============================================================ */
.eu-overview-rows {
  display: block;
}

.eu-osum {
  display: flex;
  align-items: center;
  gap: 8px;
  height: var(--eu-row, 32px);
}

/* Two tracks per channel: a grey channel-separator line is drawn between each
   pair (above rows 2, 4, 6 … 14 — i.e. between tracks 1|2, 3|4 … 13|14), and
   nothing within a pair or at the top/bottom edges (none before 00, none after
   15). Pure positional rule (the grid is always 8 channels × 2 rings), applied
   identically to the cypher tracks (.eu-track) and the OVERVIEW (.eu-osum).
   The per-row bottom hairline is removed so only the channel lines remain. */
.eu-track,
.eu-osum {
  border-bottom: none;
  position: relative;
}

/* The channel separator is drawn only across the data region — beginning where
   the lane (cypher) / meter (overview) begins and ending at the panel's right
   edge — so it lines up with the timeline + lanes and never runs under the
   controls or the value label. */
.eu-tracks .eu-track:nth-child(odd):not(:first-child)::before,
.eu-overview-rows .eu-osum:nth-child(odd):not(:first-child)::before {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  height: 1px;
  background: color-mix(in srgb, var(--eu-rule-strong) 40%, var(--eu-bg));
  pointer-events: none;
}

/* Cypher: from the left cap's inner edge to the right cap's inner edge (the
   stripe's 10px right padding supplies the right inset). */
.eu-tracks .eu-track:nth-child(odd):not(:first-child)::before {
  left: calc(var(--page-pad) + var(--col-label) + var(--gap-col) - 8px + 10px);
  right: 0;
}

/* Overview: from the meter's left edge (past the value label) to the row's right. */
.eu-overview-rows .eu-osum:nth-child(odd):not(:first-child)::before {
  left: 48px;
}

.eu-osum.dead {
  /* No opacity dimming — the channel separator above a dead row must read
     identically to every other separator (see the channel-line rule). */
}

/* Empty/collapsed cypher tracks: no lane hatch — an inactive channel reads as
   simply empty. The signal that it's inactive is the track number, dimmed to
   the faint end of the ink ramp (vs --eu-ink-2 on active rows). */
.eu-track--empty .eu-lane {
  background: none;
}

.eu-track--empty .eu-track__index {
  color: var(--eu-ink-4);
}

.eu-osum__val {
  flex: 0 0 40px;
  text-align: right;
  font-size: 11px;
  color: var(--eu-ink-2);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-ui);
}

.eu-osum__val.inactive {
  color: var(--eu-ink-4);
}

/* The meter element carries the value colour as --seg-colour. Default is a
   neutral grey (no current segment) — never green; green bars didn't
   correspond to any green lane segment. A latched playing-segment hue
   overrides this per-frame. */
.eu-osum__meter {
  /* full available width (the row minus the 40px value readout + 8px gap). */
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0;
  --seg-colour: var(--eu-ink-3);
}

/* The OVERVIEW bar, in the tool look: a track the thickness of a Binder cord,
   coloured like the nodes, carrying the value as a clean line in the segment's
   own hue. Same colour as before — just simpler: no faint tail, no end-cap. */
.eu-osum__line {
  position: relative;
  flex: 1;
  min-width: 0;
  height: var(--cb-cord-w, 4.5px);
  border-radius: 999px;
  background: transparent;   /* no grey track — just the coloured value-line on the black */
}

.eu-osum__fill {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  border-radius: 999px;
  background: var(--seg-colour);
}

/* Align the OVERVIEW scale strip's 0% with the meter origin (val 40 + 8 gap). */
.voice-head-spacer {
  flex: 0 0 48px;
}

/* Align the two 16-row groups: the OVERVIEW header is sized in JS to match
   the CYPHER CHANNELS header's live height (syncOverviewHeader), so each
   title+rule sits at the same Y and the stripes — ring N on each side —
   start aligned at every width. Bottom-anchoring keeps the title hugging
   the rule when the OVERVIEW header is grown to match. */
.rail-block[data-rail="overview"] .sub-header {
  align-content: flex-end;
}


/* Consistent type: every size clamped to 11px (the EUCLID wordmark is the one
   exception, restored below). Includes overriding euclid.css's larger display
   tokens. */
:root {
  --eu-t-label: 11px;
  --eu-t-body: 11px;
  --eu-t-val-s: 11px;
  --eu-t-val: 11px;
  --eu-t-disp: 11px;
}
header.app .brand { font-size: 18px; }

/* ============================================================
   Unified control-button style — every header / filter control matches the
   track selector (.eu-ms / .eu-track__index): 1px rule-strong border, ink-3
   label, 11px small-caps, subtle hover, solid white fill when active/engaged.
   ============================================================ */
.cyc-btn,
.cyc-step-btn,
.cyc-level,
.midi-btn,
.play-btn,
.play-btn.paused,
#cancel-btn {
  background: transparent;
  border: 1px solid var(--eu-rule-strong);
  color: var(--eu-ink-3);
  font-size: var(--eu-t-cap);
  letter-spacing: var(--track-ui);
}

.cyc-btn:hover:not(:disabled),
.cyc-step-btn:hover:not(:disabled),
.cyc-level:hover,
.midi-btn:hover:not(:disabled),
.play-btn:hover:not(:disabled),
.play-btn.paused:hover:not(:disabled),
#cancel-btn:hover:not(:disabled),
.btn-primary:hover:not(:disabled) {
  background: transparent;
  border-color: var(--eu-ink-3);
  color: var(--eu-ink-2);
}

/* Patch SAVE / LOAD drop their accent treatment to match the rest. */
.btn-primary {
  background: transparent;
  border: 1px solid var(--eu-rule-strong);
  color: var(--eu-ink-3);
  font-size: 11px;
  letter-spacing: var(--track-caps);
}

/* Active / lit / engaged — NO fill, exactly like the mute button: just a
   brighter (white) outline + label. */
.cyc-level[data-active="true"],
.play-btn:not(.paused),
.midi-btn.active,
.btn-primary.active {
  border-color: var(--eu-ink-0);
  color: var(--eu-ink-0);
}

/* The unused part of the knob's radial track is dotted — round dots (not
   dashes) with wide spacing and a thicker stroke, drawn as an inline-SVG ring
   (stroke-dasharray "0 gap" + round caps = dots). The solid white arc overlays
   the filled portion on top. */
.eu-knob {
  border: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='31' fill='none' stroke='%2334383b' stroke-width='1.4' stroke-dasharray='0 3.4' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 100% 100%;
}

/* The lane segment labels (and track text) must not be selectable — selection
   interferes with click/drag interaction on the strip. */
.cypher-stripe {
  user-select: none;
  -webkit-user-select: none;
}

/* ADD A BINDING — the add affordance now lives in the BINDINGS panel (the
   knob + was removed). Hairline button matching the system; sits left-aligned
   below the bindings (or alone when there are none). */
.bindings-add {
  align-self: flex-start;
  margin-top: 8px;
  padding: 6px 10px;
  background: transparent;
  border: 1px solid var(--eu-rule-strong);
  color: var(--eu-ink-3);
  font-size: 11px;
  letter-spacing: var(--track-caps);
  cursor: pointer;
}

.bindings-add:hover {
  border-color: var(--eu-ink-3);
  color: var(--eu-ink-2);
}
