/* Self-hosted fonts (offline-safe — no Google Fonts dependency at the install).
   MONO-FORWARD: one monospace face drives both chrome and data (like the matte
   tool). Two candidates, each a variable file (weight axis 300-700) — swap the
   active one via --ui-font below. Paths are relative to this file (src/ui/). */
@font-face { font-family: "Spline Sans Mono"; font-style: normal; font-weight: 300 700; font-display: swap; src: url("../../fonts/SplineSansMono.woff2") format("woff2"); }
@font-face { font-family: "Martian Mono"; font-style: normal; font-weight: 300 700; font-display: swap; src: url("../../fonts/MartianMono.woff2") format("woff2"); }
@font-face { font-family: "Commit Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("../../fonts/CommitMono.woff2") format("woff2"); }
/* System85 Mono Pro — the active UI face (pixel/system mono, rendered smooth). */
@font-face { font-family: "System85"; font-style: normal; font-weight: 400; font-display: swap; src: url("../../fonts/System85MonoPro-Regular.woff2") format("woff2"); }
@font-face { font-family: "System85"; font-style: normal; font-weight: 500 700; font-display: swap; src: url("../../fonts/System85MonoPro-Medium.woff2") format("woff2"); }

/* ── Design tokens (matte-inspired: monospace, lifted panels, vibrant hues) ── */
:root{
  /* Active UI typeface — MONO-FORWARD: one family for chrome AND data.
     Swap the look by switching the active line: Spline ⇄ Martian. */
  /* Active UI face — Commit Mono (clean humanist mono; the app's original face).
     System85 / Spline / Martian remain self-hosted above as swaps. */
  --ui-font: "Commit Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --mono: var(--ui-font);
  /* SOLID SOFTWARE (Resolume-calibrated): panels are opaque dark-gray surfaces a
     step brighter than the old near-black, so chrome reads as solid tooling that
     the canvas sits INSIDE of, not a ghost floating over it. One gray ramp,
     hairlines one step stronger. */
  /* FALLBACK palette — regenerated from palette.js themeVars({accent:'#22bcd6'}) so a
     pre-JS / FOUC flash matches the runtime (palette.js writes these inline on <html>).
     Keep in sync: re-run themeVars if the accent default or anchors change.
     NEUTRAL gray base + ONE fixed cyan accent (MadMapper blue × Resolume teal) that pops. */
  --bg:#1c1c1c; --panel:#242424; --panel-2:#2b2b2b; --hover:#383838;
  --stage-bg:#101011;          /* display surfaces stay near-black in BOTH themes (LED content wants black) */
  --panel-solid:#242424;       /* menus/popovers share the panel surface */
  --line:#3a3a3a; --line-2:#4c4c4c;       /* hairline borders (neutral) */
  --text:#ffffff; --muted:#ccccd2; --faint:#959599;
  --accent:#22bcd6;            /* the one fixed cyan accent (MadMapper blue × Resolume teal) */
  --accent-soft:#10383f;       /* selected/active background tint */
  --accent-text:#76d5e6;       /* text on accent surfaces */
  --accent-line:#14515c;       /* active border */
  --accent-head:#295157;       /* muted cyan group-header bar */
  --accent-underline: color-mix(in srgb, var(--accent) 24%, transparent);   /* section-header underline (auto-tracks --accent) */
  --t-fast: 120ms;             /* the ONE hover/reveal transition duration */
  --shadow-pop: none;    /* NO drop shadows — floating chrome separates by its 1px hairline (flat/structural) */
  --shadow-modal: none;  /* (kept as tokens so a future elevation change is one line) */
  --readout:#ffffff;           /* numeric readouts */
  --danger:#ff6b6b;
  --warn:#cfa14f;              /* advisory (over an fps budget), not an error */
  --success:#57c98a;           /* online / live status (the one non-accent semantic colour) */
  --accent-dark:#0d1f22;       /* dark glyph ON a bright accent-filled surface */
  --field-bg:#141414;                 /* recessed input fill */
  --field-border:var(--line-2);
  --surface:var(--panel); --surface-2:var(--panel-2); --surface-hover:var(--hover);
  --border-2:var(--line);
  --glass:var(--panel);                   /* SOLID — panels are opaque, not floating glass */
  --badge-bg:rgba(0,0,0,.6);              /* dark chip background on thumbnails/clips (repeated) */
  /* Spacing scale — tightened (slim, Resolume-dense). */
  --s1:2px; --s2:4px; --s3:6px; --s4:10px; --s5:14px; --s6:8px;   /* --s6 fills the common 8px gap */
  /* Typography system — mono-forward: --sans aliases the one --ui-font face so
     the semantic chrome/data split survives if we ever re-introduce a true sans. */
  --sans: var(--ui-font);
  /* --ui-scale (set from Settings › Appearance) multiplies every text token. */
  --ui-scale: 1;
  /* Fill opacity of the floating panels (device editor + timeline). Settings › Appearance
     "Translucency" drives this; lower = more see-through. */
  --pop-opacity: 62%;
  --fs-micro:calc(11px * var(--ui-scale)); --fs-body:calc(12px * var(--ui-scale)); --fs-ctrl:calc(13px * var(--ui-scale)); --fs-title:calc(14px * var(--ui-scale)); --fs-glyph:calc(16px * var(--ui-scale));
  --fw-reg:400; --fw-med:600;   /* second weight ON — section headers/active/modified read heavier (hierarchy without color) */
  --lh-tight:1.2; --lh-ctrl:1.4; --lh-ui:1.45;
  --ls-caps:.06em; --ls-ui:.04em;   /* mono caps need less tracking than sans */
  /* Layout / control dimensions — shared tokens so components stay in sync
     (no stray hardcoded px). Change one place, every consumer follows. */
  --radius:0px; --radius-sm:0px;   /* square corners */
  --left-w:282px;    /* left inspector column width (was an undefined var + magic fallback) */
  --side-w:268px;    /* right panel (Devices/Inventory) width — single pane (editor is the left Device tab) */
  --side2-w:300px;   /* second sidebar (selected fixture/device editor) width */
  --layer-w:128px;   /* layer-head column AND the Composition tab width (must match) */
  --clip-w:96px;     /* clip-cell width */
  --clip-h:84px;     /* clip-cell + layer-head row height (the deck row module) — squareish */
  --row-h:26px;      /* ONE row height — every label·control row (input, select, slider, bool) */
  --ctrl-h:22px;     /* ONE control height — fields/selects/buttons inside rows (row-h minus breath) */
  --col-gutter:14px; /* inspector label/cog gutter (the section-triangle position) */
  --col-label:84px;  /* param label column (fixed-width fields) */
  --col-val:44px;    /* param value / readout column */
  --slider-w:120px;  /* fixed slider width — matte-style: label fills left, value far right */
}
html,body{ background:var(--bg); color:var(--text); font-family:var(--sans); line-height:var(--lh-ui);
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; }
/* ═══════════ SHARED APP-SURFACE BACKBONE (keep in sync with LEDger
   src/style.css) ═══════════ Rock-solid-app hardening both tools share: no OS
   highlights/tap flashes/ghost drags/overscroll; themed caret + selection. */
html, body {
  overscroll-behavior: none;
  cursor: default;                            /* arrow over chrome, not I-beam */
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
}
input, textarea, [contenteditable] { cursor: text; caret-color: var(--accent); }
::selection { background: color-mix(in srgb, var(--accent) 30%, transparent); color: var(--text); }
/* Advanced-mode gate: .adv-only elements (layout controls, gamma, technical settings)
   show ONLY when Settings › Advanced mode is on (body.advanced). Off = the simple set. */
body:not(.advanced) .adv-only { display: none !important; }
img, a, button, summary, label { -webkit-user-drag: none; user-drag: none; }
input:focus-visible, summary:focus-visible, a:focus-visible {
  outline: 1px solid var(--line-2); outline-offset: 1px;
}
/* ═══════════ end shared backbone ═══════════ */
/* Keyboard cheat-sheet overlay (near-black card, hairline, mono keys). */
#shortcuts-overlay { position: fixed; inset: 0; z-index: 30; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.55); }
#shortcuts-overlay[hidden] { display: none; }
.shortcuts-card { background: var(--panel); border: 1px solid var(--line-2); padding: 16px 18px; min-width: 320px; max-width: 90vw; }
.shortcuts-title { font: var(--fw-med) var(--fs-micro) var(--sans); letter-spacing: var(--ls-caps); text-transform: uppercase; color: var(--accent); margin-bottom: 12px; }
.shortcuts-grid { display: grid; grid-template-columns: auto 1fr; gap: 5px 14px; align-items: center; }
.shortcuts-key { font: var(--fs-body) var(--mono); color: var(--text); background: var(--field-bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 1px 6px; justify-self: start; white-space: nowrap; }
.shortcuts-act { font: var(--fs-body) var(--sans); color: var(--muted); }
/* No text selection anywhere — this is an app surface, not a document. Editable
   fields keep selection so you can still edit their contents. */
*{ user-select:none; -webkit-user-select:none; }
input,textarea,[contenteditable]{ user-select:text; -webkit-user-select:text; }
/* MONO only for data: numeric readouts, lowercase param keys, editable name/coord
   inputs. Numbers read as aligned data (tabular) — neutral colour, not a hue. */
.ly-readout, .g-val, .cmp-readout, .lh-op-val, .anim-num,
.ly-plabel, .ly-nameedit, .lh-name, .imp-base,
.output-grid input, .fx-field input { font-family: var(--mono); }
.ly-readout, .g-val, .cmp-readout, .lh-op-val { color: var(--readout); font-variant-numeric: tabular-nums; letter-spacing: 0; }

/* Hide scrollbars everywhere (still scrollable). */
* { scrollbar-width: none; -ms-overflow-style: none; }
*::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* --- Top-level workspace tabs: flat segments in the header bar --- */
.tabs { display: inline-flex; gap: 0; align-self: stretch; }
.tab {
  display: inline-flex; align-items: center;
  background: transparent; color: var(--muted); border: none; border-bottom: 2px solid transparent;
  padding: var(--s5) var(--s5); font: var(--fw-med) var(--fs-ctrl) var(--sans); letter-spacing: var(--ls-caps); text-transform: uppercase;
  cursor: pointer;
}
.tab:hover { color: var(--text); }
/* Active = ACCENT text (family rule shared with LEDger: the selected tab is
   warm, not just bright) — at every tier: workspace tab, sub-tab, section tab. */
.tab-active, .tab-active:hover { color: var(--accent); background: none; }   /* one tab look: accent text, no fill */

/* --- Inspector sub-tabs (Clip | Composition) --- */
/* CHILD row of the Design/Output switch: full width, equal thirds (big touch
   targets), but a step SMALLER than the parent and with no border of its own
   (the parent row above carries the divider) — scale + position say "these
   live inside the section", keeping the text-only OCR language. */
/* The sub-tab row continues the active master's DARK accent tint, so it reads as
   "these belong to the selected master" (no gap above — flush to the master row). */
.subtabs { display: flex; gap: 0; margin-bottom: 0; padding: 0; background: var(--accent-soft); }
.subtab {
  flex: 1 1 0; text-align: center;            /* Clip · Layer · Composition = equal thirds */
  background: transparent; color: var(--faint); border: none;          /* unselected = gray */
  border-radius: 0; padding: 6px 0; font: var(--fw-med) var(--fs-body) var(--sans); cursor: pointer;
  text-transform: uppercase; letter-spacing: var(--ls-caps);
}
.subtab:hover { color: var(--muted); }
/* The active SUBTAB pops in the BRIGHT accent with dark on-accent text. */
.subtab-active, .subtab-active:hover { color: var(--accent); background: none; }   /* same tab look at every tier */

/* Output mapping hint line (small, muted). */
.seg-hint { font-size: var(--fs-micro); color: var(--faint); line-height: var(--lh-ctrl); }

/* Live FFT spectrum in the clip Audio Trigger inspector. */
.clip-spectrum { display: block; margin-top: var(--s2); }

/* --- Output mapping: fixture list --- */
/* Add-fixture / add-device / inventory icons at the right of the "Devices" title. */
#grp-patch .island-head { display: flex; align-items: center; gap: var(--s2); }
.island-acts { margin-left: auto; display: flex; align-items: center; gap: 4px; }
.island-act { background: transparent; border: none; color: var(--muted); cursor: pointer; padding: 2px; display: inline-flex; }
.island-act .ic { width: 18px; height: 18px; fill: currentColor; display: block; }
.island-act:hover { color: var(--accent); }
/* + Controller / + Fixture / Scan — COMPACT buttons on the Output header (next to the
   title, hugging their text — no full-width whitespace). Only shown on the open section. */
.acc-head-acts { flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--s1); }
.acc-sec:not(.is-open) > .acc-head .acc-head-acts { display: none; }
.out-add { flex: 0 0 auto; height: var(--ctrl-h); padding: 0 var(--s3); background: var(--field-bg); border: 1px solid var(--line); color: var(--muted);
  font: var(--fw-med) var(--fs-micro) var(--sans); letter-spacing: var(--ls-ui); text-transform: uppercase; cursor: pointer; white-space: nowrap; }
.out-add:hover { color: var(--accent); border-color: var(--line-2); }
.out-add:disabled { opacity: .35; cursor: default; }
.out-add.is-scanning { color: var(--accent); border-color: var(--accent-line); }
.output-tools { display: flex; flex-direction: column; align-items: stretch; gap: 4px; margin: 0 0 8px; }
/* + fixture / + device side by side; the type picker (if any) sits full-width below. */
/* + Fixture / + Device side by side as square tiles. */
.output-addrow { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
.output-addrow .fx-add { width: auto; height: auto; aspect-ratio: 1; display: flex; align-items: center; justify-content: center; white-space: nowrap; }
.output-tools select {
  width: 100%; box-sizing: border-box; background: var(--field-bg); color: var(--text);
  border: 1px solid var(--field-border); border-radius: var(--radius);
  height: var(--ctrl-h); padding: 0 7px; font: var(--fs-body) var(--sans); cursor: pointer;
}
#add-fixture { color: var(--accent); border-color: var(--line); }
.snap-toggle { display: flex; align-items: center; gap: 4px; font-size: var(--fs-body); color: var(--muted); cursor: pointer; }
.snap-toggle input { width: auto; }
.output-eye { padding: 0 6px; line-height: var(--lh-ctrl); font-size: var(--fs-body); color: var(--accent); }
.output-eye.off { color: var(--faint); }
/* Full-bleed list rows: no outer border, no side margins, no rounded card — just a
   1px hairline SEPARATOR between rows. The first row sits flush under the section
   header (no doubled border between the group and its first item). */
.output-row {
  display: flex; align-items: center; gap: 8px; padding: 6px 8px; margin: 0;
  background: var(--panel-2); border: none; border-top: 1px solid var(--line);
  font-size: var(--fs-ctrl); cursor: pointer;
  /* No left identity bar — a fixture nests under its controller, so the tie is already
     clear from position (the controller keeps its colour swatch + on-canvas tint). */
}
/* Controller colour swatch before the header title (Output list). */
.dev-swatch { flex: 0 0 auto; width: 8px; height: 8px; border-radius: var(--radius); }
.output-row:first-child { border-top: none; }
.output-row span:first-of-type { flex: 1 1 auto; min-width: 0; color: var(--dev-color, var(--text)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }   /* name in its controller's identity colour; plain text when unassigned */
/* Liveness dot (Devices list): online = accent ("on"), offline = danger, checking
   = faint, no-ip = hollow. Stays in the warm-mono palette (no green/blue). */
.dev-dot { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; background: var(--faint); }
.dev-online { background: var(--accent); }
.dev-offline { background: var(--danger); }
.dev-check { background: var(--faint); opacity: .6; }
.dev-noip { background: transparent; box-shadow: inset 0 0 0 1px var(--faint); }
.dev-artnet { background: transparent; box-shadow: inset 0 0 0 1px var(--accent-line); }   /* Art-Net node — status not polled */
.output-dev { color: var(--accent); font-size: var(--fs-micro); }
.output-row .ly-rmfx { padding: 0 6px; line-height: var(--lh-ctrl); }
/* Declutter a long list: the per-fixture delete ✕ and the (lit/●) eye stay
   hidden until the row is hovered or selected — a HIDDEN fixture keeps its eye
   shown so you can still see/restore it. (Touch has no hover, but fixture
   editing is a desktop task.) */
.output-row .ly-rmfx, .output-row .output-eye { opacity: 0; transition: opacity var(--t-fast); }
/* Identify button — flashes this strip white on the wall. Hover-reveal like the others. */
.output-ident { flex: 0 0 auto; padding: 0 4px; background: none; border: none; color: var(--faint); cursor: pointer; line-height: var(--lh-ctrl); opacity: 0; transition: opacity var(--t-fast); }
.output-row:hover .output-ident, .output-row.selected .output-ident { opacity: 1; }
.output-ident:hover { color: var(--accent); }
.output-row:hover .ly-rmfx, .output-row:hover .output-eye,
.output-row.selected .ly-rmfx, .output-row.selected .output-eye,
.output-row .output-eye.off { opacity: 1; }
/* Inventory row duplicate (⧉): reveal on hover, like the delete/eye affordances. */
.lib-dup { opacity: 0; background: none; border: 0; color: var(--faint); cursor: pointer; padding: 0 4px; font-size: var(--fs-micro); line-height: var(--lh-ctrl); transition: opacity var(--t-fast), color var(--t-fast); }
.output-row:hover .lib-dup { opacity: 0.6; }
.lib-dup:hover { opacity: 1; color: var(--accent); }
/* Inventory row delete (✕): same hover-reveal affordance, danger on hover. */
.lib-del { opacity: 0; background: none; border: 0; color: var(--faint); cursor: pointer; padding: 0 var(--s2); font-size: var(--fs-micro); line-height: var(--lh-ctrl); transition: opacity var(--t-fast), color var(--t-fast); }
.output-row:hover .lib-del { opacity: 0.6; }
.lib-del:hover { opacity: 1; color: var(--danger); }
/* "+" = instantiate into the scene. Always visible (the primary Inventory action),
   accent on hover. */
.lib-add { background: none; border: 0; color: var(--accent); cursor: pointer; padding: 0 var(--s3); font-size: var(--fs-glyph); line-height: var(--lh-ctrl); opacity: .8; transition: opacity var(--t-fast); }
.lib-add:hover { opacity: 1; }
/* Selected = a FILLED accent tint (same as the Fixtures tree), so a picked
   controller / definition reads clearly as selected, with its name in the accent
   text colour. (The output tree's filled-selection pattern, applied here too.) */
.output-row.selected { background: color-mix(in srgb, var(--dev-color, var(--accent)) 30%, var(--panel-2)); border-color: color-mix(in srgb, var(--dev-color, var(--accent)) 45%, var(--line)); }
.output-row.selected > span:first-of-type { color: var(--text); }   /* white on the controller-colour highlight */
.output-row.selected .lr-suffix { color: color-mix(in srgb, var(--text) 62%, transparent); }   /* size suffix stays legible on the tint */
.output-edit { margin-top: var(--s3); border-top: 1px solid var(--line); padding-top: var(--s2); }
/* When the editor is the inspector's only content (a selected fixture), the
   separator margin/rule above it is dead space — drop it so GEOMETRY sits at
   the top. */
.output-edit:first-child { margin-top: 0; border-top: none; padding-top: 0; }
.output-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
/* Stage-corner chip row (3D mode): holds the FIELDS ghost toggle. Parked in
   the stage's top-left corner (the bottom is where the layer deck floats);
   hidden outside 3D mode. */
#proj-row { position: absolute; left: 10px; top: 10px; z-index: 5; display: none; align-items: center; gap: 2px; pointer-events: auto; }
body.mode-3d #proj-row { display: flex; }
/* FIELDS chip (volumetric field ghosts on/off). */
#proj-row .proj-fields { text-transform: uppercase; }

/* Fixture SHAPE row (Bar | Polyline | Bezier): equal-width segment buttons. */
.shape-row { display: flex; gap: 2px; margin-bottom: var(--s2); }
.shape-row .dir-btn { flex: 1 1 0; min-width: 0; justify-content: center; }
/* Shape toggle in the POSITION header: small icon buttons (Linear / Bezier),
   right-aligned like the group reset. Active = accent tint. */
.shape-toggle { margin-left: auto; flex: 0 0 auto; display: flex; gap: 2px; }
.shape-btn {
  display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 18px;
  padding: 0; background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--faint); cursor: pointer;
}
.shape-btn:hover { color: var(--accent); background: var(--accent-soft); }
.shape-btn.on { color: var(--accent-text); background: var(--accent-soft); border-color: var(--accent-line); }
.shape-btn .ic-sm { width: 13px; height: 13px; fill: currentColor; display: block; }
/* Per-vertex XYZ table (polyline/bezier fixture editor): # + X/Y/Z px columns.
   Inputs match the .fx-field chrome; Z is height off the canvas plane (3D). */
.vtx-grid { display: grid; grid-template-columns: auto 1fr 1fr 1fr; gap: 2px 4px; align-items: center; margin-bottom: var(--s2); }
/* A long imported arc can have dozens of vertices — cap the table (in its collapsed
   VERTICES group) so it scrolls instead of pushing the rest of the inspector away. */
.insp-sec[data-sec="vertices"] .vtx-grid { max-height: 240px; overflow-y: auto; }
.vtx-grid .vtx-h { font: var(--fw-med) var(--fs-micro) var(--sans); color: var(--faint); text-transform: uppercase; letter-spacing: var(--ls-caps); text-align: right; padding-right: 3px; }
.vtx-grid .vtx-h:first-child { text-align: left; }
.vtx-grid .vtx-idx { font: var(--fs-micro) var(--mono); color: var(--faint); padding-right: 3px; }
.vtx-grid input {
  min-width: 0; width: 100%; box-sizing: border-box; text-align: right;
  background: var(--field-bg); color: var(--text); border: 1px solid var(--field-border);
  border-radius: var(--radius); height: var(--ctrl-h); padding: 0 5px; font: var(--fs-ctrl) var(--mono);
}
/* Fixtures inputs reuse the shared .fx-field input chrome (same as Design) — no
   override here, so the two panels match. Hide number-input spinner arrows so a
   number field reads like the Design readouts (clean, no native chrome). */
input[type=number] { -moz-appearance: textfield; appearance: textfield; }
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; appearance: none; margin: 0; }

/* Fixture chains (stagger) — ordered groups in the Output view. */
.chains { margin-top: var(--s3); border-top: 1px solid var(--line); padding-top: var(--s2); }
/* Flattened (no floating rounded card): a top hairline separates each chain, no
   radius/outer-margin notches. */
.chain-card { border: none; border-top: 1px solid var(--line); border-radius: 0; padding: var(--s3) 0 0; margin: 0; background: transparent; }
.chain-card:first-child { border-top: none; padding-top: 0; }
.chain-head { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
.chain-name { color: var(--accent); font-size: var(--fs-ctrl); flex: 1 1 auto; }
.chain-fire { padding: 0 7px; line-height: var(--lh-ctrl); min-width: 0; color: var(--accent); border-color: var(--line); }
.chain-members { display: flex; flex-direction: column; gap: 0; margin-bottom: 6px; }
.chain-member { display: flex; align-items: center; gap: 5px; font-size: var(--fs-body); }
.chain-member .chain-mname { flex: 1 1 auto; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chain-member button { padding: 0 5px; line-height: var(--lh-ctrl); min-width: 0; font-size: var(--fs-micro); }
.chain-member button:disabled { opacity: .3; }
.chain-idx { flex: 0 0 auto; width: 16px; text-align: center; color: var(--bg); background: var(--accent); border-radius: var(--radius); font-size: var(--fs-micro); line-height: var(--lh-ctrl); }
.chain-axis { margin-left: auto; }

/* Full-bleed canvas background: fixed behind everything, centred + letterboxed
   to the composition aspect. The UI panels float over it (matte-style). */
/* Dropdown popover (File / Audio — opened from the bottom-left corner cluster).
   Solid (not translucent) so it reads over the canvas. Opens UPWARD (JS-placed). */
#menu-pop { position: fixed; z-index: 40; background: var(--panel-solid); border: 1px solid var(--line-2); min-width: 184px; padding: 5px 0; }
#menu-pop[hidden] { display: none; }
#menu-pop .menu-key { color: var(--faint); font: var(--fs-micro) var(--mono); }
#menu-pop .menu-item { display: flex; justify-content: space-between; gap: 18px; padding: 6px 14px; color: var(--text); font: var(--fs-body) var(--sans); cursor: pointer; white-space: nowrap; }
#menu-pop .menu-item:hover { background: var(--hover); color: var(--accent); }
#menu-pop .menu-sep { height: 1px; background: var(--line); margin: 4px 0; }
#menu-pop .menu-pad { padding: 8px 14px; }   /* control popover body (e.g. Audio gain) */
#menu-pop .menu-title { font: var(--fw-med) var(--fs-micro) var(--sans); text-transform: uppercase; letter-spacing: var(--ls-caps); color: var(--faint); padding: 4px 14px 6px; }

/* FLAT dark PASTEBOARD around the letterboxed composite (disguise/Notch-style desk —
   the transparency checker read as a web image editor). The canvas is distinguished
   from the desk by #stageinner's thin frame, not a pattern. */
#stagewrap {
  /* The canvas is the BODY of the Stage island — it fills the island below the header
     and clips its own zoom/pan, so it can't bleed over the neighbouring islands. A
     size container so the composite can fit-contain against BOTH dimensions (cqw/cqh). */
  flex: 1 1 auto; min-height: 0; position: relative; container-type: size;
  display: flex; align-items: center; justify-content: center;
  z-index: 0; pointer-events: none; overflow: hidden;
  background-color: var(--stage-bg);   /* display surface — stays dark in light mode */
}
/* In OUTPUT mode the desk is the fixture-editing surface and is inset BETWEEN the
   sidebars (so it never overlaps a panel) — make it catch pointer events so a
   fixture dragged onto the pasteboard MARGIN (outside the composition) is still
   hit-testable. Elsewhere it stays pass-through (empty clicks fall to body). */
body.output-mode #stagewrap { pointer-events: auto; }
/* The canvas region: solid (so empty output reads as "off"/black, distinct from
   the checker desk) with a thin frame marking the output bounds. */
#stageinner {
  position: relative; line-height: 0; pointer-events: auto;
  background: #000;
  box-shadow: 0 0 0 1px var(--line);
  /* Fit-contain the composite to its comp aspect against BOTH the cell width and
     height (never stretched): width = min(full width, height × aspect). */
  aspect-ratio: var(--comp-aspect, 1.778);
  width: min(100cqw, calc(100cqh * var(--comp-aspect, 1.778)));
  height: auto; max-width: 100%; max-height: 100%;
}
/* Canvas BASE size: the WHOLE composite always fits the window (letterboxed to its
   aspect, never cropped) — width = min(window width, the width that makes height ==
   the viewport), scaling up or down (--comp-aspect is published from the render loop
   since CSS can't read canvas attrs). Zoom/pan (a transform on #stageinner) roams
   freely on top; the ⤢ pill resets to this fitted view. */
#stageinner canvas#stage { display: block; width: 100%; height: 100%; }
/* 3D mode: the overlay renders the rig through the orbit camera; the flat
   composite would be spatially WRONG behind an angled scene, so it recedes to a
   faint ghost (kept, not hidden — continuity of what's playing, at zero cost). */
body.mode-3d #stageinner canvas#stage { opacity: .1; }
/* Live view (corner ▣ live): the composite is NOT dimmed (dimming made the lights
   hard to read) — instead every fixture's LED cells go full strength (see
   preview.js liveView) so the lights pop on top of the visuals. */
/* Preview overlay sits exactly over the stage (same box); it is the drag surface. */
#preview { position: absolute; inset: 0; width: 100%; height: 100%; }
/* Vector chrome layer over the lights canvas — stays sharp under the zoom/pan
   transform (SVG re-rasterizes, unlike the canvas bitmap). Non-interactive so the
   #preview canvas keeps the pointer hit-testing. */
#ovl { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }

/* ===================== DOCK WORKSPACE =====================
   A full-width top bar over three columns of stacked "island" panels (Photoshop /
   MadMapper style). Everything is visible at once — no section switching. Splitters
   between islands (and on the column edges) resize neighbours. */
:root { --dock-gap: 0px; --topbar-h: 42px; }   /* seamless dock — windows butt together, separated by hairlines */

/* TOP BAR: an island-styled control strip (window menus + view presets + canvas
   toggles + the runtime zoom pill) — framed + inset by a dock gutter like the other
   islands. Highest dock layer so its buttons are never clipped. */
#topbar {
  position: fixed; bottom: var(--dock-gap); left: var(--dock-gap); right: var(--dock-gap);   /* FOOTER */
  height: var(--topbar-h); z-index: 8;
  /* 3 columns: menus (left) · toggles (centred) · right cluster — the side columns are
     equal (1fr) so the toggle group is truly centred, and the columns RESERVE space so
     nothing overlaps; if the total exceeds the width the bar scrolls its own content. */
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 10px; padding: 0 8px;
  box-sizing: border-box; background: var(--panel-2); border: 1px solid var(--line);   /* footer bar */
  overflow-x: auto; overflow-y: hidden;
}
#topbar .corner-row { display: inline-flex; align-items: center; gap: 2px; }
#corner-menus { justify-self: start; }
#corner-toggles { justify-self: center; }   /* canvas view/toggles centred in the footer */
#corner-right { justify-self: end; }   /* bug + install at the far right */

/* INSTALLED PWA — window-controls-overlay: the OS folds its title bar into the window,
   so the top bar must live only inside the SAFE titlebar area (env(titlebar-area-*)),
   never under the traffic-lights (left) or the browser/window controls (right). The
   env() fallbacks resolve to a full-width bar, so NORMAL/browser mode is unchanged. */
@media (display-mode: window-controls-overlay) {
  #topbar {
    left: env(titlebar-area-x, var(--dock-gap));
    top: env(titlebar-area-y, var(--dock-gap));
    width: env(titlebar-area-width, auto);
    height: env(titlebar-area-height, var(--topbar-h));
    right: auto;
    /* Empty bar = draggable window-move region; interactive children opt out below. */
    -webkit-app-region: drag;
  }
  /* Keep every control clickable (and let users move the window from the gaps). */
  #topbar button,
  #topbar input,
  #topbar select,
  #topbar .corner-row { -webkit-app-region: no-drag; }
  /* Dock starts under the REAL top bar, which now sits at the OS titlebar height. */
  #dock {
    top: calc(env(titlebar-area-y, var(--dock-gap))
              + env(titlebar-area-height, var(--topbar-h))
              + var(--dock-gap));
  }
}

/* LOCK / performance mode — the show keeps running + outputting, but EVERYTHING is inert
   except the unlock button. The !important on descendants is required to beat the rules
   that set pointer-events:auto (`.island`, slider thumbs, #stagewrap). */
body.is-locked #dock, body.is-locked #dock *,
body.is-locked #device-pop, body.is-locked #device-pop *,
body.is-locked #topbar * { pointer-events: none !important; }
body.is-locked #menu-lock, body.is-locked #menu-lock * { pointer-events: auto !important; }
body.is-locked #dock-left, body.is-locked #dock-right, body.is-locked #timeline-island { opacity: .6; }
#menu-lock.is-on { color: var(--accent); }
/* Icon buttons (Save / Open / Import / Settings / Mapping / Control / Bug / Install):
   borderless, larger glyphs — the icon IS the affordance. */
.g-icon {
  background: transparent !important; border: none !important;
  min-width: 24px; padding: 2px 3px !important; position: relative; color: var(--muted);
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  height: auto !important;   /* don't inherit the 22px control height — size to the icon + caption */
}
/* Tiny uppercase caption under a top-bar icon — says what the button does at a glance;
   the title tooltip still carries the full description. */
.g-label { font-size: var(--fs-micro); line-height: 1; letter-spacing: .02em; text-transform: uppercase;
  color: var(--faint); white-space: nowrap; pointer-events: none; }
body.hide-toolbar-labels #topbar .g-label { display: none; }   /* Settings › Toolbar labels (off by default) */
.g-icon:hover .g-label { color: inherit; }
.g-icon.on .g-label, .g-icon.open .g-label { color: inherit; }
.g-icon:hover { color: var(--accent-text); }
.g-icon.open, .g-icon.on, .g-icon:active { color: var(--accent); }   /* active toggle = accent glyph (no box) */
.g-icon:disabled { opacity: .35; }
/* Pixelarticon SVGs render crispest at integer (16px) sizes; fill follows currentColor. */
.g-icon .ic { width: 22px; height: 22px; fill: currentColor; display: block; }
/* Inline icon used inside small buttons (rotation / reverse). */
.ic-sm { width: 14px; height: 14px; fill: currentColor; display: inline-block; vertical-align: middle; }
.ic-flip { transform: scaleX(-1); }   /* mirror for the opposite rotation direction */
/* Companion/daemon status dot tucked into the remote button's corner (no layout shift). */
#menu-remote .tab-dot { position: absolute; top: 4px; right: 2px; margin: 0; }
/* Daemon OFFLINE chip — only in the DOM flow while the bridge is down (danger-tinted
   warning). button# selectors outrank the generic `#topbar button` colour. */
#topbar button#daemon-chip, #topbar button#daemon-chip:hover { color: var(--danger); }
#daemon-chip .g-label { color: var(--danger); font-weight: var(--fw-med); }
#daemon-chip[hidden] { display: none; }

/* DOCK: three columns under the top bar, on the dark dock background. Gutters pass
   pointer events through; the islands re-enable them. */
#dock {
  /* The topbar sits at the BOTTOM now (cleared by the bottom offset). Top gutter is
     supplied by the dock's own padding alone, so the gap above the islands equals the
     side/inter-island gaps (--dock-gap) — top:0 here or it would double to 2×gap. */
  position: fixed; top: 0; left: 0; right: 0; bottom: calc(var(--topbar-h) + var(--dock-gap)); z-index: 6;
  display: flex; gap: var(--dock-gap); align-items: stretch;
  padding: var(--dock-gap); box-sizing: border-box; background: var(--bg); pointer-events: none;
}
.island { pointer-events: auto; }

/* COLUMNS: fixed-width side columns (sized for legible, non-wrapping fields) + a
   flexible centre. The view presets (Canvas / Split / Editor) adjust the balance. */
.dock-col { display: flex; flex-direction: column; gap: var(--dock-gap); min-height: 0; position: relative; }
#dock-left  { flex: 0 0 var(--left-w, 282px); min-width: 244px; }
#dock-center{ flex: 1 1 auto; min-width: 0; }

/* --- Two-screen shell: SCREEN 1 = the app (100vh); scroll down to SCREEN 2 = one panel
   with Settings / Library / Mapping as TABS (embedded iframes, replacing the popups).
   Scroll up or press Esc to return. --- */
html { scroll-behavior: smooth; }
#screen-app { position: relative; height: 100vh; }
#screen-app > #topbar { position: absolute; }   /* was fixed-to-viewport → now scrolls with screen 1 */
#screen-app > #dock { position: absolute; }
#screen-panels { position: relative; background: var(--bg); padding: 0 var(--dock-gap) var(--dock-gap); box-sizing: border-box; }
.scroll-panel { height: calc(100vh - var(--dock-gap)); display: flex; flex-direction: column; min-height: 0;
  background: var(--panel); border: 1px solid var(--line); box-sizing: border-box; overflow: hidden; }
/* SCREEN 2 is just the full-width Mapping panel (no tabs — Settings/Library moved to the
   sidebars). One iframe fills it. */
.panel-bodies { flex: 1 1 auto; position: relative; min-height: 0; }
.panel-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: var(--panel-2); display: none; }
.panel-frame.on { display: block; }
/* RIGHT sidebar is split in two equal side-by-side panes (Device editor | Devices/Inventory). */
#dock-right { flex: 0 0 var(--side-w, 268px); min-width: 220px; gap: var(--dock-gap); }
#grp-patch { flex: 1 1 0; }

/* ISLAND: a framed dock window — header bar + scrolling body. */
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 0; padding: var(--s3); box-sizing: border-box; }
.island {
  background: var(--panel); border: 1px solid var(--line); border-radius: 0;
  box-sizing: border-box; position: relative; min-width: 0; min-height: 0;
  display: flex; flex-direction: column; overflow: hidden;
}
/* Zero-gap dock: adjacent 1px borders would double into 2px lines — keep exactly ONE
   hairline at every seam. Side columns drop the edge facing the centre (the centre
   island's own border is the divider); stacked islands in a column share one line. */
#dock-left  > .island { border-right: none; }
#dock-right > .island { border-left: none; }
.dock-col > .island + .island { border-top: none; }
.island-head {
  flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
  padding: 4px 10px; background: var(--panel-2); border-bottom: 1px solid var(--line);
  user-select: none;
}
.island-title { font: var(--fw-med) var(--fs-body) var(--sans); text-transform: uppercase;
  letter-spacing: var(--ls-caps); color: var(--muted); }
.island-body { flex: 1 1 0; min-height: 0; overflow-y: auto; background: var(--panel-2); padding: var(--s3); }

/* Per column sizing: the flex-fill islands (left props · centre stage) absorb space;
   the centre timeline gets a fixed (curtain-resizable) height. */
.dock-col > .island { flex: 0 0 auto; }
#grp-props, #stage-island { flex: 1 1 0; min-height: 60px; }

/* ACCORDION dock windows (left inspector · right patch): stacked collapsible sections,
   exactly one open per host. The open section fills the column; closed = header only. */
.acc-host { display: flex; flex-direction: column; padding: 0; }
.acc-sec { display: flex; flex-direction: column; min-height: 0; border-bottom: 1px solid var(--line); }
.acc-sec:last-child { border-bottom: none; }
.acc-sec.is-open { flex: 1 1 auto; }
.acc-sec:not(.is-open) { flex: 0 0 auto; }
.acc-sec:not(.is-open) .acc-body { display: none; }
.acc-head { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none;
  padding: 6px 10px; background: var(--panel-2); }
.acc-head::before { content: ''; flex: 0 0 auto; width: 0; height: 0; transition: transform var(--t-fast);
  border-left: 4px solid var(--faint); border-top: 3px solid transparent; border-bottom: 3px solid transparent; }
.acc-sec.is-open > .acc-head::before { transform: rotate(90deg); border-left-color: var(--accent); }
.acc-title { flex: 1 1 auto; font: var(--fw-med) var(--fs-body) var(--sans);
  text-transform: uppercase; letter-spacing: var(--ls-caps); color: var(--muted); }
.acc-head:hover .acc-title { color: var(--text); }
.acc-sec.is-open > .acc-head .acc-title,
.acc-sec.is-open > .acc-head:hover .acc-title { color: var(--accent); }
/* Action icons on a section header (Output: add controller / fixture / scan) — only on
   the OPEN section; clicks are exempted from the accordion toggle in app.js. */
.acc-body { flex: 1 1 0; min-height: 0; overflow-y: auto; background: var(--bg); padding: var(--s3); }
/* An embedded app (Settings / Library iframe) fills the accordion body edge-to-edge. */
.acc-body.acc-embed { padding: 0; overflow: hidden; }
.acc-embed iframe { width: 100%; height: 100%; border: 0; display: block; background: var(--panel-2); }

/* Small icon-only tab (the Settings cog in the left column's tab row). */
.island-tab.island-tab-icon { flex: 0 0 auto; min-width: 0; padding: 0 9px; display: inline-flex; align-items: center; justify-content: center; }
.island-tab-icon .ic { width: 14px; height: 14px; fill: currentColor; display: block; }
/* Timeline floats BOTTOM-LEFT over the canvas, sized to its content (mirrors the device
   group top-right). Translucent so the output shows through. */
#timeline-island {
  position: absolute; left: var(--dock-gap); right: var(--dock-gap); bottom: var(--dock-gap); z-index: 50;
  max-height: 60%;   /* full width between the sidebars (was content-width, bottom-left) */
  background: color-mix(in srgb, var(--panel) var(--pop-opacity), transparent);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--line-2); border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
}
#timeline-island .island-body { flex: 0 0 auto; overflow: auto; background: color-mix(in srgb, var(--panel-2) calc(var(--pop-opacity) - 14%), transparent); }
#timeline-island #deckbar { flex: 1 1 auto; width: 100%; }
/* Canvas-island tab panes (Settings / Mapping / Control) fill below the tab row and
   scroll on their own; the Canvas tab body is #stagewrap. */
#mapping-pane, #control-pane { flex: 1 1 0; min-height: 0; }
#mapping-pane { display: flex; }
#mapping-frame { flex: 1 1 auto; width: 100%; height: 100%; border: 0; background: var(--panel-2); }
#control-pane { overflow-y: auto; background: var(--panel-2); }

/* Island header used as a TAB ROW (e.g. Fixtures | Inventory). Text-style tabs:
   active = bright accent, others muted — no boxes (OCR aesthetic). */
.island-tabs { gap: 0; padding: 0; }
.island-tab { flex: 1 1 0; background: transparent; border: none; border-radius: 0; cursor: pointer;
  padding: 5px 0; color: var(--faint); font: var(--fw-med) var(--fs-body) var(--sans);
  text-transform: uppercase; letter-spacing: var(--ls-caps); }
.island-tab:hover { color: var(--muted); }
/* Active tab = accent text/icon only (no background fill) — restrained, per the OCR look. */
.island-tab.is-on { color: var(--accent); }
/* The Stage + Timeline bodies hold the canvas / deck flush (no inset). */
#stage-island > #stagewrap, #timeline-island > .island-body { padding: 0; }

/* The deck fills the Timeline island body; its clip grid scrolls inside. */
#deckbar {
  flex: 1 1 auto; min-height: 0; padding: 0; background: transparent; border: none;
  display: flex; flex-direction: column; align-items: stretch; overflow: hidden;
}
/* When editing fixtures (fixtures overlay on) keep the deck's REAL controls live but
   let empty cells pass clicks to the canvas behind. */
body.output-mode #deckbar { pointer-events: none; }
body.output-mode #deckbar .lh-clickable,
body.output-mode #deckbar .clip-cell,
body.output-mode #deckbar .lh-op,
body.output-mode #deckbar .master-op,
body.output-mode #deckbar button { pointer-events: auto; }

/* Group flash — a brief accent ring when a selection surfaces a group. */
.island.grp-flash { box-shadow: inset 0 0 0 2px var(--accent-line); }

/* Panel visibility is the single source of truth (see setupLayout). The Canvas preset
   sets hide-left+hide-right; Editor widens; Split clears all; Overlay floats the panels.
   These hide-* rules are driven by BOTH the panel toggles and the presets. */
body.hide-left #dock-left { display: none; }
body.hide-right #dock-right { display: none; }
body.hide-timeline #timeline-island { display: none; }
body.view-edit #dock-left  { flex-basis: 360px; }
body.view-edit #dock-right { flex-basis: 360px; }

/* OVERLAY view: the canvas fills the whole dock as a full-bleed backdrop and the
   panels float TRANSLUCENT over it (frosted), so you see the output through the UI. */
body.view-overlay #stage-island { position: absolute; inset: var(--dock-gap); z-index: 0; border: none; }
body.view-overlay #dock-left, body.view-overlay #dock-right { position: relative; z-index: 1; }
/* (Timeline already floats bottom-left at all times — see #timeline-island.) */
body.view-overlay .island:not(#stage-island) { background: color-mix(in srgb, var(--panel) 60%, transparent); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
body.view-overlay .island:not(#stage-island) .island-head { background: color-mix(in srgb, var(--panel-2) 62%, transparent); }
body.view-overlay .island:not(#stage-island) .island-body { background: color-mix(in srgb, var(--panel-2) 42%, transparent); }

/* RESPONSIVE — rather than hiding panels on narrow screens, keep EVERYTHING visible and
   scroll the dock horizontally below a minimum width. The centre canvas has a min-width
   so it never collapses; once the columns + canvas exceed the viewport the dock scrolls.
   (The top bar likewise scrolls its own content — see #topbar overflow-x.) */
#dock { overflow-x: auto; overflow-y: hidden; }
#dock-center { min-width: 420px; }
/* Touch: bump tap targets on coarse pointers. */
@media (pointer: coarse) {
  .g-icon { min-width: 38px; min-height: 38px; }
  .tab-scan { padding: var(--s3) var(--s6); font-size: var(--fs-body); }
  .island-tab { padding: 9px 0; }
}

#fxinsp-body { display: contents; }

/* Selected-device editor as a floating GROUP — sits TOP-RIGHT over the canvas (placed
   in JS), height follows content (capped by max-height), slightly translucent. */
#device-pop {
  position: fixed; z-index: 60; width: min(290px, calc(100vw - 16px)); max-height: 88vh; overflow: auto;
  background: color-mix(in srgb, var(--panel) var(--pop-opacity), transparent);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--line-2); border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
}
#device-pop[hidden] { display: none; }
/* Override the docked-island body flex (1 1 0) so the group sizes to its CONTENT, and
   keep it translucent over the canvas. */
#device-pop .island-body { flex: 0 0 auto; background: color-mix(in srgb, var(--panel-2) calc(var(--pop-opacity) - 14%), transparent); }
#device-pop .island-head { background: color-mix(in srgb, var(--panel-2) calc(var(--pop-opacity) - 7%), transparent); }

/* (Settings opens as its own popup window — settings/ — from the top-left gear.) */

/* CURTAIN: a drag strip on the Timeline's top edge to resize it against the Canvas.
   No visible bar — the ns-resize cursor is the only affordance. */
/* The Canvas/Timeline curtain is obsolete now the timeline floats (no shared height). */
.curtain { display: none; }
body.resizing-row, body.resizing-row * { cursor: ns-resize !important; user-select: none; }

/* Settings preference rows: checkbox on the LEFT, then the full-width label. */
.set-toggle { justify-content: flex-start; gap: 9px; cursor: pointer; }
.set-toggle input[type=checkbox] { flex: 0 0 auto; width: 15px; height: 15px; cursor: pointer; }
.set-toggle span { flex: 1 1 auto; }

/* Dashboard knob disabled until something links to it. */
.dash-cell.is-unlinked { opacity: .35; pointer-events: none; }

/* (Settings lives in its own popup window — settings/;
   #system-control is a hidden manifest mount.) */

/* OVERLAY (the Control surface popup). */
.dock-overlay { position: fixed; inset: 0; z-index: 40; display: flex; align-items: flex-start; justify-content: center;
  background: rgba(0,0,0,.5); padding: 56px 16px 16px; }
.dock-overlay[hidden] { display: none; }
.dock-overlay-card { width: min(420px, 96vw); max-height: calc(100vh - 96px);
  display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--line-2); }
.dock-overlay-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
  padding: 8px 12px; background: var(--panel-2); border-bottom: 1px solid var(--line); }
.dock-overlay-close { background: transparent; border: none; color: var(--muted); font-size: 18px; cursor: pointer; line-height: 1; }
.dock-overlay-close:hover { color: var(--accent-text); }
/* Companion/daemon status dot on the CONTROL tab — red offline, accent live. */
/* Daemon/companion status dot — red offline, green when running (a fixed status
   green, independent of the theme accent). */
.tab-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--danger); margin-left: 7px; vertical-align: middle; }
.tab-dot.on { background: var(--success); }
.ctrlpane-head { display: flex; flex-direction: column; align-items: center; gap: var(--s3);
  padding: 0 0 var(--s4); margin-bottom: var(--s4); border-bottom: 1px solid var(--line); text-align: center; }
.ctrl-status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--danger); flex: 0 0 auto; }
.ctrl-status-dot.on { background: var(--accent); }
/* Inverted, theme-merged QR: accent modules on a dark inset (no white card). Fills
   the sidebar width (big = easy to scan) and is a link (click → open the surface). */
.ctrl-qr { display: block; line-height: 0; color: var(--accent); background: var(--field-bg);
  padding: 12px; border-radius: var(--radius); border: 1px solid var(--line); width: 100%; cursor: pointer; }
.ctrl-qr svg { display: block; width: 100%; height: auto; }
.ctrl-qr:hover { border-color: var(--accent-line); }
.ctrl-urlrow { display: flex; align-items: center; justify-content: center; gap: 7px; }
.ctrl-url { font: var(--fs-body) var(--mono); color: var(--accent); text-decoration: none; word-break: break-all; }
.ctrl-url:hover { color: var(--accent-text); }
/* Flattened: the exposed params sit directly in the pane (no floating card / gap). */
.ctrl-layer { background: transparent; border: none; border-radius: 0; padding: 0; margin: 0; }
.ctrl-layer-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.ctrl-layer-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); font: var(--fs-ctrl) var(--mono); }
.ctrl-blk { flex: 0 0 auto; width: 26px; height: var(--ctrl-h); border-radius: var(--radius); border: 1px solid var(--line-2);
  background: var(--field-bg); color: var(--muted); font: var(--fs-body) var(--sans); cursor: pointer; }
.ctrl-blk.on { background: var(--danger); border-color: var(--danger); color: var(--accent-dark); }
/* Clip triggers: compact, flat chips (one accent, hairline) — not chunky filled
   blocks. Active clip = accent text + hairline; press = brief accent fill. */
.ctrl-clips { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 7px; }
.ctrl-clip { height: var(--ctrl-h); border-radius: var(--radius); border: 1px solid var(--line); background: transparent;
  color: var(--muted); font: var(--fs-body) var(--sans); padding: 0 10px; cursor: pointer; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.ctrl-clip:hover { color: var(--accent-text); border-color: var(--line-2); }
.ctrl-clip:active { background: var(--accent); border-color: var(--accent); color: var(--accent-dark); }
.ctrl-clip.active { color: var(--accent); border-color: var(--accent-line); }
/* (Opacity + param sliders in Control reuse the editor's standard .ly-row Slider.) */
.ctrl-section { font-size: var(--fs-micro); letter-spacing: var(--ls-caps); text-transform: uppercase; color: var(--muted); margin: 14px 2px 8px; }
/* A clip whose video file was lost on reload. */
.clip-missing { position: absolute; left: 3px; top: 3px; font: var(--fs-micro) var(--mono); color: var(--danger);
  background: var(--badge-bg); padding: 1px 4px; border-radius: var(--radius-sm); }
.ctrl-empty { color: var(--muted); text-align: center; padding: var(--s5) var(--s4); }
#library {
  flex: 0 0 auto; max-height: 42%; overflow-y: auto;
  border-top: 1px solid var(--line); margin: var(--s3) calc(-1 * var(--s3)) calc(-1 * var(--s3));
  padding: var(--s2) var(--s3) var(--s3);
}
/* Sources/Effects moved to on-demand pickers — the docked shelf is gone, so the
   container collapses entirely when empty (no stray top hairline / padding). */
#library:empty { display: none; }
/* (The old top-level Design/Output/System section switch + sub-tabs were removed —
   every panel is now an always-visible docked group; see the DOCK WORKSPACE block.) */
#comp-settings { margin-bottom: 6px; }
[hidden] { display: none !important; }
/* 'h' / Tab hides all GUI to view the canvas full-screen: drop the side columns + the
   timeline, strip the Stage chrome, and pull the dock up under the (collapsed) top
   bar so the canvas reclaims the whole window. */
body.gui-hidden #dock-left,
body.gui-hidden #dock-right,
body.gui-hidden #timeline-island { display: none; }
body.gui-hidden #dock { top: 0; padding: 0; gap: 0; }
body.gui-hidden #stage-island { border: none; }
body.gui-hidden #stage-island .island-head { display: none; }
/* Full-bleed shows the CANVAS regardless of which canvas-tab was active. */
body.gui-hidden #stagewrap { display: flex !important; }
body.gui-hidden #mapping-pane, body.gui-hidden #control-pane { display: none !important; }

/* Menu-style button (window menus in the top bar). */
.g-btn {
  background: transparent; color: var(--muted); border: 1px solid var(--line); border-radius: 0;
  height: var(--ctrl-h); padding: 0 10px; font: var(--fs-body) var(--sans); cursor: pointer;
}
/* Zoom readout — a plain clickable number, no box, centred in the (taller) bar. */
#corner-toggles .g-btn { border: none; background: transparent; height: auto; padding: 0 6px; }
#corner-toggles .g-btn:hover { color: var(--accent-text); }
#corner-toggles .g-btn.on { color: var(--accent); }
/* Fixed width so toggling "Hide UI" ⇄ "Show UI" (proportional font) doesn't shift
   the cluster. */
#g-hide { min-width: 74px; text-align: center; }
/* Thin rule splitting the top bar into groups (menus | view-toggles). */
.corner-sep { flex: 0 0 auto; align-self: center; width: 1px; height: 16px; background: var(--line-2); margin: 0 2px; }
/* 3D-only footer controls (Fields ghosts + Reset view, moved off the stage corner) —
   hidden in 2D like the retired #proj-row. Both are icon buttons now (ic-fields / ic-rotate). */
body:not(.mode-3d) .mode3d-only { display: none; }
#topbar button {
  background: var(--panel); border: 1px solid var(--line-2); border-radius: 0;
  height: var(--ctrl-h); padding: 0 11px; color: var(--muted); font: var(--fs-body) var(--sans); cursor: pointer;
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto; white-space: nowrap;   /* never shrink/wrap a glyph+label onto two lines */
}
/* Capitalise the canvas toggles (fixtures/snap/grid/color/live). */
#corner-toggles button { text-transform: capitalize; }
/* Layout-view switcher — 3 connected glyph buttons (Canvas / Split / Editor). */
#topbar .view-seg { display: inline-flex; gap: 2px; }
#topbar .view-seg button { border: none !important; background: none !important; box-shadow: none !important;
  padding: 0 6px; display: inline-flex; align-items: center; justify-content: center; text-transform: none; color: var(--muted); }   /* plain icons, no dark box */
#topbar .view-seg button .ic { width: 22px; height: 22px; fill: currentColor; display: block; }   /* match the other footer icons */
#topbar .view-seg button.on { color: var(--accent); }
#topbar .view-seg button:hover { color: var(--text); }
/* Active toggle / open menu = a clearly FILLED pill (the fill IS the on/off signal). */
#topbar button.on, #topbar button.open {
  color: var(--accent-text); background: var(--accent-soft); border-color: var(--accent-line);
}
#topbar button:disabled { opacity: .4; cursor: default; }                    /* e.g. snap is moot when the overlay is off */
/* UI hidden = full-bleed canvas; the top bar goes too ('H' brings everything back). */
body.gui-hidden #topbar { display: none; }
/* The "Show UI" pill: hidden normally, floats top-left only while the UI is hidden,
   so H is never a one-way trapdoor. */
/* Output-master HUD: shows FROZEN / PANIC top-centre so the operator knows the live state. */
#out-hud {
  position: fixed; top: calc(var(--topbar-h) + 12px); left: 50%; transform: translateX(-50%);
  z-index: 70; padding: 4px 12px; border-radius: var(--radius-sm);
  font: var(--fw-med) var(--fs-body) var(--sans); letter-spacing: var(--ls-caps); text-transform: uppercase;
  background: color-mix(in srgb, var(--accent) 20%, var(--panel)); color: var(--accent-text);
  border: 1px solid var(--accent-line); pointer-events: none;
}
#out-hud[hidden] { display: none; }
#out-hud.is-panic { background: color-mix(in srgb, var(--danger) 28%, var(--panel)); color: color-mix(in srgb, var(--danger) 35%, #ffffff); border-color: var(--danger); }
#show-ui-pill {
  position: fixed; top: var(--s6); left: var(--s6); z-index: 40; display: none;
  font: var(--fw-med) var(--fs-micro) var(--sans); letter-spacing: var(--ls-caps);
  text-transform: uppercase; color: var(--muted); cursor: pointer;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: var(--s3) var(--s4);
}
#show-ui-pill:hover { color: var(--accent-text); border-color: var(--line-2); }
body.gui-hidden #show-ui-pill { display: block; }
/* The '?' help button reads as a glyph, not an icon — size it like the sprite icons. */
.topbar-help { font: var(--fw-med) var(--fs-glyph) var(--sans); }


/* Layer header: B/S toggles + click-to-select. */
.lh-clickable { cursor: pointer; }
/* B · S · ✕ : larger, cleaner letters (Resolume vibe). */
.lh-tog {
  padding: 0; border-radius: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-ctrl); font-weight: var(--fw-med); letter-spacing: var(--ls-ui);
  color: var(--muted); background: var(--field-bg); border: 1px solid var(--line);
}
.lh-tog:hover { color: var(--text); background: var(--hover); }
.lh-tog.on { color: var(--accent-text); background: var(--accent-soft); border-color: var(--accent-line); }
/* Autopilot (play/loop) block in the Layer inspector. */
.autopilot { display: flex; align-items: center; gap: 10px; margin: 4px 0; }

/* Library column hosts the Sources/Effects rail full-width (no side border). */
#library .composer-rail { flex: 1 1 auto; border-left: none; padding-left: 0; gap: 4px; }

.fx-panel { display: flex; flex-direction: column; gap: 0; }
.fx-title { font-size: var(--fs-title); font-weight: var(--fw-med); color: var(--text); }
.fx-section { margin: 12px 0 4px; }   /* recipe above; faint uppercase, no border */

.fx-ok { color: var(--accent); font-size: var(--fs-body); }
.fx-err { color: var(--danger); font-size: var(--fs-body); white-space: pre-wrap; }

.fx-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr);   /* min 0 → rows shrink to the panel; inputs don't overflow */
  gap: 0;
  background: transparent; border: none; border-radius: 0;
  border-top: 1px solid var(--line); padding: var(--s2) 0 0;
}
/* The FIRST card sits directly under a panel/inspector title — no leading rule
   (the title already separates it); keeps dividers BETWEEN stacked cards. */
.fx-card:first-child { border-top: none; padding-top: 0; }
/* ONE group-header recipe — hierarchy via uppercase micro caps + whitespace, NOT
   a box/rule. Section colour (cyan sources / amber effects) is the only delta. */
.fx-pts, .fx-section, .composer-label, .fx-menu-label, .lh-blend-label {
  font: var(--fw-med) var(--fs-micro)/var(--lh-tight) var(--sans);
  letter-spacing: var(--ls-caps); text-transform: uppercase; color: var(--faint);
}
/* TWO header tiers only (consistency): the collapsible Section header below is the
   single "group header" (accent caps + underline). These inline mini-headers
   (patch / geometry / wiring / show file …) are the quiet SUB-label tier — faint
   micro-caps + whitespace, NO accent, NO rule — so they don't compete. */
.fx-pts { grid-column: 1 / -1; margin: 12px 0 5px; }
.fx-pts:first-child { margin-top: 0; }
/* Library section header with a right-pinned "+ …" action (Output-style). Flex here
   so the plain-label (standalone popout) case matches the embedded teal-bar case, which
   is already flex via the injected HEAD_BAR. */
.fx-pts.lib-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); }

/* Collapsible inspector sections — accent header + an accent-tinted rule under it. */
.insp-sec:first-child { border-top: none; }
.insp-sec-head {
  display: flex; align-items: center; gap: 6px; width: 100%; height: 24px;
  padding: 0 2px 4px; background: none; border: none; border-radius: 0; cursor: pointer;
  border-bottom: 1px solid var(--accent-underline); margin-bottom: var(--s2);
}
.insp-sec-title {
  font: var(--fw-med) var(--fs-micro)/1 var(--sans); letter-spacing: var(--ls-caps);
  text-transform: uppercase; color: var(--accent);
}
.insp-tri {
  width: 0; height: 0; flex: 0 0 auto;
  border-left: 4px solid var(--accent); border-top: 3px solid transparent; border-bottom: 3px solid transparent;
  transition: transform var(--t-fast);
}
.insp-sec.is-open .insp-tri { transform: rotate(90deg); }
/* Unassigned: a plain heading (no triangle) — indent the title to line up with the
   foldable controller headers, and no pointer (it's not collapsible). */
.insp-sec-head.out-unassigned { padding-left: 12px; cursor: default; }
/* Locked-open section (e.g. an empty Effects group): no collapse affordance. */
.insp-sec.is-locked .insp-sec-head { cursor: default; }
.insp-sec.is-locked .insp-sec-head:hover { background: none; }
.insp-sec.is-locked .insp-tri { visibility: hidden; }
/* Per-group reset — always visible (touch-first), one step up from faint so it
   reads as a live control, with a warm hover so it announces itself before the
   press. Glyph-sized (it was an 11px speck) with a real hit area. */
.insp-sec-reset {
  margin-left: auto; flex: 0 0 auto; padding: 2px 7px; min-width: 0; line-height: 1;
  background: none; border: none; border-radius: var(--radius-sm); color: var(--muted);
  font-size: var(--fs-glyph); cursor: pointer;
}
.insp-sec-reset:hover { color: var(--accent); background: var(--accent-soft); }
.insp-sec-reset:active { color: var(--text); background: var(--hover); }
.insp-sec-reset:disabled { opacity: .2; cursor: default; background: none; }
.insp-sec-reset:disabled:hover { color: var(--faint); background: none; }
.insp-sec-body { display: none; padding: 2px 0 6px; }
.insp-sec.is-open .insp-sec-body { display: block; }
/* Subtle table separation so a run of rows reads as a ledger (Resolume-like). */
.insp-sec-body .ly-param.ly-row, .insp-sec-body .anim-param {
  border-top: 1px solid color-mix(in srgb, var(--line) 50%, transparent);
}
.insp-sec-body .ly-param.ly-row:first-child, .insp-sec-body .anim-param:first-child { border-top: none; }

/* Master-detail Fixtures tab: compact selectable list rows + one detail editor. */
.fx-list { display: flex; flex-direction: column; gap: 0; margin: var(--s1) 0; }
.fx-badge {
  flex: 0 0 auto; font-size: var(--fs-micro); color: var(--muted);
  background: var(--field-bg); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 1px 6px;
}
.fx-badge.out-over { color: var(--warn); }   /* output over its fps budget — advisory, not an error (border stays quiet) */
/* Devices list = Inventory-style sections (one per controller). The title grows so
   its pixel badge right-aligns; the header is a drop target + selects the controller. */
.out-sec .insp-sec-title { flex: 1 1 auto; color: var(--text); }   /* white for contrast against the accent-soft selection */
.out-sec .insp-sec-head { border-bottom: none; height: 24px; }   /* no hairline; matches the inspector section-header height */
.insp-sec-head.out-unassigned { height: 24px; }
.out-sec .insp-sec-head.is-sel { background: color-mix(in srgb, var(--dev-color, var(--accent)) 30%, var(--panel-2)); }
.fx-detail { margin-top: 2px; }
/* Wiring (Distribution) picker: a tiled 4×4 glyph grid. The 1px gap shows the
   panel line through as hairline seams (no floating cells / unnecessary gaps). */
.dist-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px;
  background: var(--line); border: 1px solid var(--line); margin: 2px 0 6px; }
.dist-cell { display: flex; align-items: center; justify-content: center; padding: 4px;
  background: var(--field-bg); border: none; color: var(--muted); cursor: pointer; }
.dist-cell:hover { color: var(--text); }
.dist-cell.on { background: var(--accent-soft); color: var(--accent-text); border-color: var(--accent-line); }
/* "Advanced" disclosure inside a device editor — a quiet, marker-free summary so
   niche fields (sync delay) stay tucked away without cluttering the common case. */
.fx-advanced > summary { cursor: pointer; user-select: none; list-style: none; padding: 4px 0; color: var(--faint); font-size: var(--fs-micro); letter-spacing: var(--ls-ui); }
.fx-advanced > summary::-webkit-details-marker { display: none; }
.fx-advanced > summary::before { content: '▸ '; }
.fx-advanced[open] > summary::before { content: '▾ '; }
.fx-budget { font-size: var(--fs-micro); color: var(--faint); padding: 4px 2px 2px; font-variant-numeric: tabular-nums; }
/* Scan section: a divider above the "Scan" heading, and its rows share the list gutter. */
.scan-head { border-top: 1px solid var(--line); margin-top: var(--s3); }
.scan-block { display: flex; flex-direction: column; gap: var(--s1); padding: 0 var(--s4); }
.scan-row .ctrl-btn { margin-left: auto; }
/* Scan result chips: tabular digits + a fixed IP slot so the rows column-align;
   long Art-Net names truncate instead of pushing the add button around. */
.scan-row .fx-badge { font-variant-numeric: tabular-nums; max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.scan-row .scan-ip { min-width: 96px; text-align: right; }
/* Scan in progress — a quiet status block while the two probes resolve, so SCAN
   never looks frozen. Each probe row flips done (✓, accent mark) as its leg lands. */
.scan-progress { gap: 4px; margin-top: 4px; }
.scan-status { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: var(--fs-body); }
.scan-spinner { width: 9px; height: 9px; border: 1px solid var(--line-2); border-top-color: var(--accent); border-radius: 50%; animation: scan-spin 700ms linear infinite; }
@keyframes scan-spin { to { transform: rotate(360deg); } }
.scan-probe { display: flex; align-items: center; gap: 6px; font-size: var(--fs-micro); color: var(--faint); }
.scan-probe.is-done { color: var(--muted); }
.scan-probe .scan-mark { width: 10px; text-align: center; }
.scan-probe.is-done .scan-mark { color: var(--accent); }
.scan-probe .fx-badge { margin-left: auto; }
@media (prefers-reduced-motion: reduce) { .scan-spinner { animation: none; } }
.fx-detail-actions { grid-column: 1 / -1; display: flex; gap: 14px; }
/* Rotate-90° / flip row in the Output editor — full-width, equal shares (action
   buttons either fill the row or split it evenly; never intrinsic-width chips). */
.out-transform { display: flex; width: 100%; margin-top: 4px; }
.out-transform .dir-btn { flex: 1 1 0; min-width: 0; }
.fx-del-link {
  grid-column: 1 / -1; justify-self: start; background: none; border: none; padding: 4px 0 0;
  color: var(--muted); font: var(--fs-body) var(--sans); cursor: pointer;
}
/* Fixtures panel scrolls within the viewport for big shows (scrollbar hidden). */
#fixtures-view { max-height: calc(100vh - 56px); overflow-y: auto; }

/* Shared horizontal row module: label left (fixed column) · value right. */
.fx-field { display: flex; flex-direction: row; align-items: center; gap: 10px; min-width: 0; min-height: var(--row-h); font-size: var(--fs-body); color: var(--muted); }
/* Fixed label column so the name stays readable (text/number inputs otherwise
   claim their intrinsic width and squeeze the label down to an ellipsis). */
.fx-field > span:first-child { flex: 0 0 var(--col-label, 84px); color: var(--muted); letter-spacing: var(--ls-ui); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fx-field > input, .fx-field > select { flex: 1 1 0; min-width: 0; width: auto; text-align: right; }   /* values right-align against the label column */
.fx-field input, .fx-field select {
  background: var(--field-bg); color: var(--text); border: 1px solid var(--field-border);
  border-radius: var(--radius); height: var(--ctrl-h); padding: 0 7px; font: var(--fs-ctrl) var(--mono); width: 100%; box-sizing: border-box;
}
.fx-field select { font-family: var(--sans); }   /* selects are chrome, not data → sans */
/* "Mixed" field in a multi-selection: the value differs across the selected
   fixtures, so the row is dimmed and shows a "— mixed —" placeholder; editing it
   commits that value to every selected fixture, clearing the mixed state. */
.fx-field.is-mixed { opacity: .45; }
/* Name field with a greyed, non-editable size suffix ("(6ch)" / "(60px)") — the wrapper
   is the field box; the input inside is borderless so it reads as one control. */
.fx-field > .name-suffixed { flex: 1 1 0; min-width: 0; display: flex; align-items: center; overflow: hidden;
  background: var(--field-bg); border: 1px solid var(--field-border); border-radius: var(--radius); height: var(--ctrl-h); box-sizing: border-box; }
.fx-field .name-suffixed input { flex: 1 1 0; min-width: 0; border: 0; background: transparent; height: auto; }
.name-suffix { flex: 0 0 auto; color: var(--faint); padding: 0 7px 0 2px; font: var(--fs-ctrl) var(--mono); white-space: nowrap; }
/* Greyed size tag appended INSIDE a list-row name, so it reads as one name. */
.lr-suffix { color: var(--faint); }
/* Derived, non-editable value (e.g. computed pixel count) — reads as data, no field
   chrome, but sits on the SAME control height so mixed rows stay one rhythm. */
.fx-field > .fx-readonly { flex: 1 1 0; min-width: 0; display: flex; align-items: center; min-height: var(--ctrl-h); color: var(--text); font: var(--fs-ctrl) var(--mono); padding: 0 8px; }
/* Parameter row: name takes the slack, kind/value sit narrow, remove stays square. */
.fx-param-row { gap: 6px; }
.fx-param-row > input { flex: 2 1 0; }
.fx-param-row > select { flex: 1 1 0; }
.fx-param-row > .fx-act { flex: 0 0 auto; }
.fx-param-row > .fx-ch-n { flex: 0 0 14px; text-align: right; color: var(--faint); font: var(--fs-micro) var(--mono); }
/* DMX param rows: a slim drag handle, the name fills the row, a small count box. */
.fx-ch-row { gap: 5px; }
.fx-ch-row > select { flex: 1 1 0; min-width: 0; }
.fx-ch-row > input[type=text] { flex: 1 1 0; min-width: 0; }
.fx-ch-row > .fx-ch-count { flex: 0 0 40px; min-width: 0; text-align: center; }
.fx-ch-row > .fx-ch-count:disabled { opacity: 0.45; }
/* Colour-range rows (per-pixel-range channel-order overrides): start · count · order ·
   remove. A grid, not the .fx-field flex row, so the faint column header and every row
   line up on the same four columns; .fx-field is kept for the input/select chrome. */
.fx-range-row { display: grid; grid-template-columns: 1fr 1fr 1.4fr var(--ctrl-h); gap: var(--s3); align-items: center; }
.fx-range-head { margin-bottom: var(--s2); color: var(--faint); font-size: var(--fs-micro); letter-spacing: var(--ls-caps); text-transform: uppercase; }
/* What the normaliser did to the rows you just authored — advisory, not an error:
   the rows above already show the result, this only names the change. */
.fx-range-note { color: var(--warn); font-size: var(--fs-micro); padding: var(--s2) 0; }
.fx-ranges .fx-add { margin-top: var(--s2); }
/* Power-droop taper: the depth slider is a normal param row; the sparkline is the only
   place the curve is visible (it's applied daemon-side, never in the canvas preview). */
.fx-spark { flex: 0 0 auto; width: 44px; height: var(--ctrl-h); color: var(--accent); }
.fx-spark.is-flat { color: var(--line); }   /* no attenuation anywhere — a dead flat rule */
/* Inherited / override / off — the word that says whether clearing brings anything back. */
.fx-field > .fx-readonly.fx-taper-state { color: var(--faint); font-size: var(--fs-micro); padding: 0 var(--s3); }
.fx-ch-drag { flex: 0 0 7px; width: 7px; cursor: grab; color: var(--faint); text-align: center; user-select: none; line-height: 1; font-size: 8px; padding: 0; }
.fx-ch-drag:active { cursor: grabbing; }
.fx-ch-drag:hover { color: var(--accent); }
.fx-ch-row.drop-hover { outline: 1px dashed var(--accent-line); outline-offset: -1px; }
/* Dashboard: a wrapping row of global link knobs (Resolume-style). */
/* Fixed 4×4 bank of dashboard links. */
.dash-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px 6px; padding: 4px 0 8px; }
.dash-cell { display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 0; }
.knob { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.knob-dial { cursor: ns-resize; touch-action: none; line-height: 0; }
.knob-track { stroke: var(--line); stroke-width: 3.5; stroke-linecap: round; }
.knob-fill { stroke: var(--accent); stroke-width: 3.5; stroke-linecap: round; }
.knob-label { font: var(--fs-micro) var(--sans); color: var(--faint); }
.dash-name { width: 100%; text-align: center; background: transparent; border: 0; color: var(--muted);
  font: var(--fs-micro) var(--sans); padding: 1px 0; min-width: 0; }
.dash-name:hover, .dash-name:focus { color: var(--text); background: var(--panel-2); border-radius: 0; outline: none; }
.dash-name.dash-auto { color: var(--accent); }   /* auto-named from what it drives */
/* The pixel/colour block shown inline among the parameters (params order around it). */
.fx-param-pixels { min-height: var(--row-h); display: flex; align-items: center; padding: 0 8px;
  color: var(--muted); font: var(--fs-micro) var(--mono); letter-spacing: var(--ls-ui);
  background: var(--panel-2); border-left: 2px solid var(--accent-line); }
/* Placement list grouped by CONTROLLER → output. The controller header is STICKY
   so you always know which device you're scrolling in; the output sub-headers
   nest under it (indented, not sticky). A colour swatch on each ties to the
   canvas (controller hue + per-output tint). */
.out-group { position: sticky; top: 0; z-index: 2; background: var(--panel);
  display: flex; align-items: center; gap: 6px; margin: 0 0 3px; padding: 6px 0 3px; border-top: 1px solid var(--line); cursor: pointer; }
.out-group:first-child { border-top: none; margin-top: 0; }
/* Drag-to-assign: a fixture row is draggable onto a device/output header. */
.output-row[draggable="true"] { cursor: grab; }
.output-row[draggable="true"]:active { cursor: grabbing; }
/* Dropping a fixture onto a controller section: honour that controller's identity colour
   (--dev-color set on the section) — a ring + faint tint, consistent with its selection. */
.out-sec.drop-hover {
  background: color-mix(in srgb, var(--dev-color, var(--accent)) 14%, transparent) !important;
  box-shadow: inset 0 0 0 2px var(--dev-color, var(--accent)) !important;
  border-color: transparent !important;
}
.out-swatch { flex: 0 0 auto; width: 9px; height: 9px; border-radius: var(--radius); box-shadow: inset 0 0 0 1px rgba(0,0,0,.35); }
.out-caret { color: var(--faint); font-size: var(--fs-micro); width: 10px; }
.out-group-dev { flex: 1 1 auto; min-width: 0; color: var(--accent); font: var(--fs-body) var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.out-group-dev:hover { text-decoration: underline; }

/* Per-device patch ruler — proportional fixture segments over the address space. */
.patch-bar { display: flex; gap: 1px; height: 18px; margin: 2px 0 6px; border-radius: 0; overflow: hidden; background: var(--line); }
.patch-seg { flex: 1 1 0; min-width: 2px; display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--accent) 22%, var(--panel-2)); overflow: hidden; }
.patch-seg:nth-child(even) { background: color-mix(in srgb, var(--accent) 34%, var(--panel-2)); }
.patch-seg > span { font: var(--fs-micro) var(--mono); color: var(--text); white-space: nowrap; opacity: .85; }

/* Live controller status line (WLED monitor). */
.fx-devstatus { font: var(--fs-micro) var(--mono); color: var(--muted); padding: 2px 0 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fx-devstatus.is-on { color: var(--success); }
.fx-devstatus.is-off { color: var(--danger); }

/* Controller controls — a button row + a brightness slider. */
/* The colour fills the whole control (no inner black margin) — just a hairline. */
.fx-color { width: 100%; height: var(--ctrl-h); padding: 0; background: none; border: 1px solid var(--field-border); border-radius: 0; cursor: pointer; overflow: hidden; }
/* Make the swatch fill the control so the colour actually reads. */
.fx-color::-webkit-color-swatch-wrapper { padding: 0; }
.fx-color::-webkit-color-swatch { border: none; border-radius: 0; }
.fx-color::-moz-color-swatch { border: none; border-radius: 0; }
.ctrl-block { margin-top: 4px; display: flex; flex-direction: column; gap: 6px; }
/* Controller status as a multi-line key/value list (clear, uses vertical space). */
.ctrl-stat { margin: 2px 0 6px; }
.ctrl-stat-row { display: flex; justify-content: space-between; gap: 12px; font: var(--fs-micro) var(--mono); padding: 1px 0; }
.ctrl-stat-k { color: var(--faint); text-transform: uppercase; letter-spacing: var(--ls-caps); }
.ctrl-stat-v { color: var(--readout); }
.ctrl-push { display: flex; flex-direction: column; gap: 3px; }
/* In this COLUMN flex, .ctrl-btn's `flex: 1 1 0` would zero its height-basis and
   collapse it to text height — pin it so it matches the row buttons (--ctrl-h). */
.ctrl-push > .ctrl-btn { flex: 0 0 auto; }
.ip-open { flex: 0 0 auto; display: inline-flex; align-items: center; box-sizing: border-box; height: var(--ctrl-h); color: var(--accent); text-decoration: none; padding: 0 6px; font-size: var(--fs-ctrl); border: 1px solid var(--field-border); border-radius: var(--radius); }
.ip-open:hover { background: var(--hover); }
.ctrl-row { display: flex; gap: 5px; }
.ctrl-btn { flex: 1 1 0; height: var(--ctrl-h); padding: 0 6px; font: var(--fs-body) var(--sans); color: var(--text);
  text-transform: uppercase; letter-spacing: .06em;
  background: var(--field-bg); border: 1px solid var(--line-2); border-radius: var(--radius); cursor: pointer; }
.ctrl-btn:hover { border-color: var(--accent); color: var(--accent); }
.ctrl-btn:active { background: var(--panel-2); }
.ctrl-btn.is-added { color: var(--accent); border-color: var(--accent-line); opacity: 1; }   /* scan: already added ✓ */
/* Live controls are disabled until the controller is confirmed online (CHECK). */
.ctrl-btn:disabled { opacity: .35; cursor: default; }
.ctrl-btn:disabled:hover { border-color: var(--line-2); color: var(--text); }
.ctrl-range:disabled { opacity: .35; cursor: default; }
.ctrl-bri { display: flex; align-items: center; gap: 8px; min-height: var(--row-h); }
.ctrl-bri > span:first-child { flex: 0 0 var(--col-label, 84px); color: var(--muted); font: var(--fs-body) var(--mono); }
.ctrl-range { flex: 1 1 0; min-width: 0; accent-color: var(--accent); }
.ctrl-val { flex: 0 0 30px; text-align: right; font: var(--fs-ctrl) var(--mono); color: var(--text); }

button {
  background: var(--panel-2); color: var(--text); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 4px 8px; cursor: pointer; font: var(--fs-ctrl) var(--sans);
}
/* No blue browser focus rings on controls (keyboard focus shows on the thumb). */
input[type=range]:focus, button:focus, select:focus { outline: none; }
input[type=range]:focus-visible { outline: 1px solid var(--accent-line); outline-offset: 1px; }   /* select handled by the accent focus rule below */

/* Custom checkbox to match the dark theme. */
input[type=checkbox] {
  appearance: none; -webkit-appearance: none; margin: 0;
  width: 14px; height: 14px; flex: 0 0 auto; vertical-align: middle;
  border: 1px solid var(--line-2); border-radius: var(--radius); background: var(--field-bg);
  cursor: pointer; position: relative;
}
input[type=checkbox]:checked { background: var(--accent); border-color: var(--accent); }
input[type=checkbox]:checked::after {
  content: ''; position: absolute; left: 4px; top: 1px; width: 3px; height: 7px;
  border: solid var(--accent-dark); border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.fx-del { grid-column: 1 / -1; margin-top: 4px; color: var(--danger); border-color: var(--line); }
/* Primary action button — matches .ctrl-btn (check / save to device): full width,
   uppercase, hairline. Used for + device / scan network / + controller / apply / fit. */
.fx-add { width: 100%; height: var(--ctrl-h); padding: 0 6px; font: var(--fs-body) var(--sans); color: var(--text);
  text-transform: uppercase; letter-spacing: .06em;
  background: var(--field-bg); border: 1px solid var(--line-2); border-radius: var(--radius); cursor: pointer; }
.fx-add:hover { border-color: var(--accent); color: var(--accent); }
.fx-add:active { background: var(--panel-2); }
/* Danger variant — the Library detail editors' "Delete …" affordance. */
.fx-add.fx-delete { color: var(--danger); margin-top: 8px; }
.fx-add.fx-delete:hover { border-color: var(--danger); color: var(--danger); }
.fx-add:disabled { opacity: .4; cursor: not-allowed; color: var(--muted); border-color: var(--line); }
.fx-add:disabled:hover { border-color: var(--line); color: var(--muted); }
.fx-io { display: flex; gap: 6px; margin-top: 12px; }
/* Action rows: buttons SHARE the row width equally (never intrinsic-width chips). */
.fx-io > button { flex: 1 1 0; min-width: 0; height: var(--ctrl-h); }

/* --- Composition (canvas resolution) panel --- */
.cmp-panel { margin-bottom: var(--s4); }
.cmp-presets { display: flex; flex-wrap: wrap; gap: 4px; }
.cmp-presets .cmp-preset { flex: 1 1 0; min-width: 0; height: var(--ctrl-h); padding: 0 8px; }
.cmp-grid { grid-template-columns: 1fr; margin-top: 4px; }
/* First card sits right under the subtabs' own border — drop its top rule so the
   two parallel lines don't read as a doubled separator. */
.cmp-panel .cmp-grid { border-top: none; padding-top: 0; }
.cmp-fit { color: var(--text); border-color: var(--line); }
.cmp-readout {
  font-size: var(--fs-body); color: var(--readout); font-variant-numeric: tabular-nums;
  margin: 2px 0; letter-spacing: 0;
}
.cmp-apply { color: var(--accent); border-color: var(--line); }
/* Apply + Fit to fixtures, side by side. */
.cmp-actions { display: flex; gap: 6px; margin-top: 8px; }
.cmp-actions .fx-add { flex: 1 1 0; width: auto; margin: 0; }

/* --- Kagora import / assign-IP panel --- */
.imp-panel { margin-bottom: var(--s4); }
.imp-banner { white-space: pre-wrap; margin-top: 4px; }
.imp-autofill { display: flex; gap: 6px; margin: 4px 0; }
.imp-autofill > button { flex: 0 0 auto; height: var(--ctrl-h); }
.imp-base {
  flex: 1 1 auto; background: var(--field-bg); color: var(--text); border: 1px solid var(--field-border);
  border-radius: var(--radius); height: var(--ctrl-h); padding: 0 7px; font: var(--fs-ctrl) var(--mono); box-sizing: border-box;
}
/* Assign-IP rows reflow: IP field + colour-order sit side by side when there's room,
   and stack when the popout is narrow — so no column is ever clipped off the edge. */
.imp-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--s2) var(--s3); align-items: center; }
.imp-row .fx-field > span:first-child { flex-basis: auto; max-width: 150px; }
.imp-apply { color: var(--accent); border-color: var(--line); }
.imp-apply:disabled { color: var(--faint); border-color: var(--line); opacity: 0.5; cursor: not-allowed; }
.imp-cancel { color: var(--danger); border-color: var(--line); }

/* Preview summary block (I7). */
.imp-preview { margin: 4px 0 6px; padding: 6px 8px; background: var(--field-bg); border: 1px solid var(--line); border-radius: var(--radius); }
.imp-prev-line { font: var(--fw-med) var(--fs-ctrl) var(--mono); color: var(--text); }
.imp-prev-dev { font: var(--fs-micro) var(--mono); color: var(--muted); margin-top: 2px; }

/* Importer warnings (I4) — muted amber, one line each. */
.imp-warn { margin: 4px 0 6px; padding: 6px 8px; background: var(--accent-soft); border: 1px solid var(--accent-line); border-radius: var(--radius); }
.imp-warn-line { font: var(--fs-micro) var(--mono); color: var(--accent-text); }
.imp-warn-line + .imp-warn-line { margin-top: 2px; }

/* Live IP-validity status (I7). */
.imp-ipstatus { color: var(--danger); }
.imp-ipstatus.imp-ipstatus-ok { color: var(--muted); }

/* No-op import (C2) + success banner (C3). */
.imp-empty { font: var(--fs-ctrl) var(--mono); color: var(--muted); margin: 4px 0 6px; }
.imp-success { margin: 4px 0 6px; padding: 6px 8px; background: var(--accent-soft); border: 1px solid var(--accent-line); border-radius: var(--radius); font: var(--fs-ctrl) var(--mono); color: var(--accent-text); white-space: pre-wrap; }

/* --- Layer-stack panel --- */
.ly-panel { margin-bottom: var(--s4); }
.ly-hint { font-size: var(--fs-micro); color: var(--faint); margin: -2px 0 4px; }
.ly-card { display: block; }
.ly-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.ly-name { font-weight: var(--fw-med); color: var(--accent); font-size: var(--fs-ctrl); }
.ly-btns { display: flex; gap: 3px; }
.ly-btns button { padding: 1px 6px; line-height: var(--lh-ctrl); min-width: 22px; }
.ly-btns button:disabled { opacity: .35; cursor: default; }

.ly-param .ly-pkey { display: flex; justify-content: space-between; }
.ly-readout { color: var(--readout); font-variant-numeric: tabular-nums; }
.ly-param input[type=range] { width: 100%; accent-color: var(--accent); }

/* Per-parameter animation: a cog opens Basic/Timeline/Audio (Resolume-style). */
.anim-param { display: flex; flex-direction: column; gap: 4px; padding: 0; }
/* Header row of an animated param: name · live value (fills) · cog at far right. */
.anim-head { display: flex; align-items: center; gap: 8px; min-height: var(--row-h); }
.anim-head .ly-readout { flex: 0 0 var(--col-val); text-align: right; min-width: var(--col-val); }
.anim-head .anim-cog-wrap { margin-left: 0; }
.anim-cog-wrap { flex: 0 0 auto; }
.anim-cog {
  flex: 0 0 auto; padding: 0; min-width: 0; width: 14px; height: 14px; font-size: 13px;
  display: flex; align-items: center; justify-content: center; line-height: 1;   /* glyph vertically centred */
  color: var(--faint); border: none !important; background: none !important; outline: none !important; box-shadow: none !important;   /* just the glyph, no box */
  opacity: 0; transition: opacity var(--t-fast); cursor: pointer;   /* appears on row hover, beside the name */
}
.anim-cog:focus-visible { outline: 1px solid var(--accent-line) !important; outline-offset: 1px; }
.anim-cog svg.ic { width: 12px; height: 12px; fill: currentColor; display: block; }   /* pixel gear, centred */
.ly-plabel-mod { cursor: pointer; }   /* the name opens the modulation picker too */

/* Modulatable COLOUR param: a swatch head + (when expanded) HSB/RGB component rows. */
.color-param .color-head { display: flex; align-items: center; gap: 0; min-height: var(--row-h); position: relative; padding-left: var(--col-gutter); }
.color-param .color-head .anim-cog-wrap { position: absolute; left: 0; top: 0; bottom: 0; width: var(--col-gutter); display: flex; align-items: center; justify-content: center; }
.color-param .color-head .ly-plabel { flex: 0 0 var(--col-label); text-align: right; padding-right: 8px; color: var(--muted); }
.color-param .color-head .fx-color { flex: 1 1 0; height: var(--row-h); min-width: 0; }
.color-param .color-head:hover .color-cog, .color-param .color-cog.on, .color-param.is-open .color-cog { opacity: 1; }
.color-param.is-open .color-cog { color: var(--accent); }
.color-param:not(.is-open) .color-body { display: none; }

/* Parameter groups don't fold — always open, no fold arrow. AIRY header (accent caps +
   thin accent underline), matching the Output/Library/device-pop section headers — the
   filled muted-teal bar was the one divergent header language left. */
#grp-props .insp-sec:not(.out-sec) > .insp-sec-body { display: block; }
#grp-props .insp-sec:not(.out-sec) > .insp-sec-head { cursor: default; }
#grp-props .insp-sec:not(.out-sec) > .insp-sec-head:hover { background: none; }
#grp-props .insp-sec:not(.out-sec) .insp-tri { display: none; }
/* Every inspector field/row shares the 14px label gutter so labels align across plain
   fields (Title/BPM/Width), slider rows (Master, params) and the Color row. */
#grp-props .fx-field { padding-left: var(--col-gutter); }
/* Row divider lines also run full-bleed. */
#grp-props .insp-sec-body > .ly-param.ly-row, #grp-props .insp-sec-body > .anim-param { margin-left: calc(-1 * var(--s3)); margin-right: calc(-1 * var(--s3)); padding-right: var(--s3); }
#grp-props .insp-sec-body > .ly-param.ly-row { padding-left: calc(var(--col-gutter) + var(--s3)); }   /* direct row: own 14px gutter + bleed offset */
#grp-props .insp-sec-body > .anim-param { padding-left: var(--s3); }   /* wrapper: the inner .ly-row already carries the 14px gutter */
#grp-props .island-body { padding-top: 3px; }   /* less dead space at the top */
/* Reveal on row hover, or whenever the param is actually modulated (so an active
   modulation is always visible), or on keyboard focus. */
.anim-param:hover .anim-cog, .anim-cog.on, .anim-cog.audio, .anim-cog:focus-visible { opacity: 1; }
.anim-cog.on { color: var(--accent); }
.anim-cog.audio { color: var(--accent); }
/* MODE PICKER FLYOUT (#anim-pop): the cog opens a small singleton panel docked
   at the RIGHT EDGE of the left sidebar, vertically aligned with the clicked
   param's row — a flyout sitting beside the sidebar (replaces the in-row slide
   strip). Solid island chrome (a picker, not glass); positioned in JS. Rows
   hosted outside #dock-left anchor at the cog instead (kit placement). */
#anim-pop {
  position: fixed; z-index: 60; min-width: 132px;
  background: var(--panel-2); border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: 3px; display: flex; flex-direction: column; gap: 1px;
  box-shadow: var(--shadow-pop);
  animation: anim-pop-in 120ms ease;
  /* Robustness: if the panel ever grows taller than the viewport (it can't with
     today's ~7 rows, but future modes could), cap it and scroll rather than
     overflowing the bottom edge — the JS top-clamp pins to top:6 in that case. */
  max-height: calc(100vh - 12px); overflow-y: auto;
}
@keyframes anim-pop-in { from { opacity: 0; transform: translateX(-6px); } }
@media (prefers-reduced-motion: reduce) { #anim-pop { animation: none; } }
/* Header: the param's name, in the quiet caps style of .fx-menu-label. */
.anim-pop-head {
  font-size: var(--fs-micro); color: var(--muted); padding: 3px 7px 3px;
  text-transform: uppercase; letter-spacing: var(--ls-caps);
  border-bottom: 1px solid var(--line); margin-bottom: 2px;
}
/* One-line items: mode icon + label. Descriptions live on the hover title. */
.anim-pop-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  background: none; border: none; text-align: left; padding: 5px 8px; cursor: pointer;
  border-radius: var(--radius); font-size: var(--fs-body); color: var(--text);
}
.anim-pop-ic { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; color: var(--muted); }
.anim-pop-ic svg { width: 16px; height: 16px; display: block; }
.anim-pop-item:hover { background: var(--hover); }
/* Current mode: accent + a restrained dot mark at the right edge. */
.anim-pop-item.is-current { color: var(--accent); }
.anim-pop-item.is-current .anim-pop-ic { color: var(--accent); }
.anim-pop-item.is-current::after { content: '●'; margin-left: auto; font-size: 7px; align-self: center; opacity: .85; }
/* Control publish tick (themed checkbox + label) below a hairline. */
.anim-pop-tick { display: inline-flex; align-items: center; gap: 6px; border-top: 1px solid var(--line); margin-top: 2px; border-radius: 0; }
.anim-pop-tick .fx-tick-box { width: 11px; height: 11px; }
.anim-pop-tick.is-on .fx-tick-box { background: var(--accent); border-color: var(--accent); }
.anim-pop-tick.is-on .fx-tick-box::after { content: ''; position: absolute; left: 3px; top: 0;
  width: 3px; height: 6px; border: solid var(--accent-dark); border-width: 0 2px 2px 0; transform: rotate(45deg); }
/* Modulation rail: any DRIVEN param (timeline/audio/external) wears an inset accent
   bar down its left edge, so it's obvious at a glance which params are animated vs
   static — no badge or label. Inset shadow ⇒ zero layout shift. */
/* (The left accent rail was removed — the in/out track + controls already make it clear
   a param is animated; the rail read as an unwanted vertical border.) */
/* BPM field + TAP button (Composition inspector). */
.cmp-bpm > input { flex: 1 1 0; min-width: 0; }
.cmp-tap {
  flex: 0 0 auto; padding: 0 10px; height: var(--ctrl-h); cursor: pointer;
  background: var(--field-bg); color: var(--accent); border: 1px solid var(--line-2);
  border-radius: var(--radius); font: var(--fw-med) var(--fs-ctrl) var(--sans); letter-spacing: var(--ls-ui);
}
.cmp-tap:active { transform: scale(0.94); }
/* TAP pulses on the beat (animation-duration = one beat, set inline as --beat).
   Restarting the animation on each tap re-phases the flash to your taps; the
   on-beat flash at 0% also serves as the click reaction. */
/* Subtle on-beat tick: only the BORDER flashes to the accent (no background fill
   or glow), so it reads as a quiet metronome rather than a strobing button. */
@keyframes cmp-tap-beat {
  0%        { border-color: var(--accent); }
  22%       { border-color: var(--accent); }
  55%, 100% { border-color: var(--line-2); }
}
.cmp-tap.pulsing { animation: cmp-tap-beat var(--beat, 500ms) linear infinite; }
/* Timeline duration unit toggle (s ⇄ ♪ beat-synced). */
.anim-unit {
  flex: 0 0 auto; min-width: 22px; padding: 0 5px; height: var(--ctrl-h); cursor: pointer;
  background: var(--field-bg); color: var(--muted); border: 1px solid var(--line-2);
  border-radius: var(--radius-sm); font-size: var(--fs-ctrl);
}
.anim-unit.on { color: var(--accent-text); border-color: var(--accent-line); background: var(--accent-soft); }
.anim-ctrls { display: flex; align-items: center; gap: 6px; padding: 0 0 2px 4px; flex-wrap: wrap; }
.anim-ctrls select {
  flex: 0 0 auto; background: var(--field-bg); color: var(--text); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 2px 4px; font: var(--fs-micro) var(--sans);
}
.anim-mini { display: flex; align-items: center; gap: 3px; font-size: var(--fs-micro); color: var(--faint); }
.anim-mini span { white-space: nowrap; }
.anim-num {
  width: 42px; background: var(--field-bg); color: var(--text); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 1px 3px; font: var(--fs-micro) var(--mono); box-sizing: border-box;
}
/* Free-text channel address for an External binding (sits next to the select). */
.anim-chan-text {
  width: 84px; background: var(--field-bg); color: var(--text); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 1px 4px; font: var(--fs-micro) var(--mono); box-sizing: border-box;
}
/* The param's CANONICAL always-active OSC address — a muted click-to-copy chip. */
.osc-addr {
  background: none; color: var(--faint); border: 1px dashed var(--line-2);
  border-radius: var(--radius); padding: 1px 5px; font: var(--fs-micro) var(--mono);
  cursor: copy; white-space: nowrap;
}
.osc-addr:hover { color: var(--text); border-color: var(--accent); }
/* Dual-handle in/out range track for an animated param (Resolume-style). Two
   overlaid native sliders share one track; the thumbs are draggable, a fill spans
   in→out, and a green marker shows the live animated value. */
.range-track { position: relative; width: 100%; min-width: 0; height: 18px; }
.range-track .rt-base { position: absolute; top: 50%; left: 0; right: 0; height: 3px; transform: translateY(-50%); background: var(--field-bg); border: 1px solid var(--line-2); border-radius: var(--radius-sm); }
.range-track .rt-fill { position: absolute; top: 50%; height: 3px; transform: translateY(-50%); background: var(--accent); opacity: .55; border-radius: var(--radius-sm); pointer-events: none; }
/* Beat-sync tick marks dividing the loop into N beats. */
.range-track .rt-ticks { position: absolute; inset: 0; pointer-events: none; }
.range-track .rt-tick { position: absolute; top: 50%; width: 1px; height: 9px; transform: translate(-50%, -50%); background: color-mix(in srgb, var(--text) 32%, transparent); }
/* Live value = a ▽ that TRAVELS along the track (its left% is set per frame). It
   sits ABOVE the track line with a small gap, tip pointing down at it. */
.range-track .anim-live { position: absolute; top: 50%; left: 0; width: 11px; height: 9px; transform: translate(-50%, calc(-100% - 3px)); background: var(--accent); clip-path: polygon(0 0, 100% 0, 50% 100%); pointer-events: none; z-index: 5; }
.range-track input[type=range] {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%; margin: 0;
  -webkit-appearance: none; appearance: none; background: none; pointer-events: none;
}
.range-track input[type=range]::-webkit-slider-runnable-track { background: none; border: none; }
/* End grips: pennant flags hanging BELOW the line at each end of the span — the
   in-grip points down-left, the out-grip down-right (Resolume trim style). */
.range-track input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; pointer-events: auto; cursor: ew-resize;
  width: 12px; height: 13px; border: none; border-radius: 0; background: var(--accent);
}
.range-track input[type=range]::-moz-range-thumb {
  pointer-events: auto; cursor: ew-resize; width: 12px; height: 13px; border: none; border-radius: 0; background: var(--accent);
}
.range-track input[type=range]::-moz-range-track { background: none; border: none; }
.range-track .rt-in::-webkit-slider-thumb { clip-path: polygon(0 0, 100% 0, 100% 55%, 0 100%); transform: translate(-3px, 4px); }
.range-track .rt-out::-webkit-slider-thumb { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 55%); transform: translate(3px, 4px); }
.range-track .rt-in::-moz-range-thumb { clip-path: polygon(0 0, 100% 0, 100% 55%, 0 100%); transform: translate(-3px, 4px); }
.range-track .rt-out::-moz-range-thumb { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 55%); transform: translate(3px, 4px); }
/* Distinguish the END (out) handle from the START (in) handle. */
/* Handle value bubble: a tiny editable readout that pops above a grabbed thumb
   and follows it — type into it to set an exact in/out value. */
.range-track .rt-bubble {
  position: absolute; top: -19px; transform: translateX(-50%);
  width: 44px; height: 17px; padding: 0 2px; text-align: center; z-index: 6;
  font: var(--fs-micro) var(--mono); color: var(--text);
  background: var(--panel-solid); border: 1px solid var(--line-2); border-radius: var(--radius-sm);
}
.range-track .rt-bubble:focus { border-color: var(--accent); outline: none; }
.range-track .rt-bubble[hidden] { display: none; }
.range-track .rt-out::-webkit-slider-thumb { background: var(--accent); }
.range-track .rt-out::-moz-range-thumb { background: var(--accent); }
.is-audio .range-track .rt-fill { background: var(--accent); }
.is-audio .range-track input[type=range]::-webkit-slider-thumb { background: var(--accent); }
.is-audio .range-track input[type=range]::-moz-range-thumb { background: var(--accent); }

/* Sweep direction as a 3-button segmented control (→ ← ⇄). */
.dir-btns { display: inline-flex; flex: 0 0 auto; }
/* In a field row (e.g. Autopilot › Direction) the segmented control takes the
   FULL control column — four equal, easy-to-hit buttons instead of a tiny pill. */
.fx-field .dir-btns { display: flex; flex: 1 1 auto; }
.fx-field .dir-btn { flex: 1 1 0; }
.dir-btn {
  height: var(--ctrl-h); padding: 0 6px; min-width: 0; font-size: var(--fs-body); line-height: var(--lh-ctrl);
  color: var(--faint); background: var(--field-bg);
  border: 1px solid var(--line-2); border-right-width: 0; border-radius: 0;
}
.dir-btn:first-child { border-radius: var(--radius) 0 0 var(--radius); }
.dir-btn:last-child { border-radius: 0 var(--radius) var(--radius) 0; border-right-width: 1px; }
.dir-btn.on { color: var(--accent-text); background: var(--accent-soft); border-color: var(--accent-line); }   /* was neutral — now the shared segmented-on look */
/* ±90° steppers inline in the Rotation field: compact, after the number input. */
.fx-field > .rot-step { flex: 0 0 auto; border-right-width: 0; }
.fx-field > .rot-step:last-child { border-right-width: 1px; border-radius: 0 var(--radius) var(--radius) 0; }
.fx-field > input + .rot-step { border-radius: 0; margin-left: 6px; }

/* Bool param: label left, checkbox right (matches the slider rows). */
.bool-row { flex-direction: row; align-items: center; justify-content: space-between; gap: 8px; min-height: var(--row-h); }
.bool-row .ly-plabel { flex: 1 1 auto; color: var(--muted); }

/* Matte-style param row: label fills the LEFT, then the value, then a fixed
   slider, and (for animatable params) the contextual cog on the FAR RIGHT.
   Flex `order` reorders the DOM [label, value, slider, cog]. */
/* Value · − · + · slider read as ONE connected segmented control (no gaps,
   shared borders); the label gaps before it and the cog sits apart after. */
/* Reserve a fixed cog gutter on EVERY row (with or without a cog) so labels always
   start at the same x — Duration and modulated params line up identically. */
.ly-param.ly-row { flex-direction: row; align-items: center; gap: 0; min-height: var(--row-h); position: relative; padding-left: var(--col-gutter); }
/* Controls fill the FULL row height — no top/bottom breath. */
.ly-row input.ly-readout, .ly-row .ly-step, .ly-row input[type=range] { height: var(--row-h); }
.ly-plabel { flex: 0 0 var(--col-label); order: 0; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding-right: 8px; text-align: right; }   /* Resolume-style: names right-aligned, hugging the value column */
.ly-row .ly-readout { order: 1; flex: 0 0 var(--col-val); min-width: var(--col-val); text-align: right; border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
/* − / + step buttons; + drops its left border so − and + join as one stepper. */
.ly-row .ly-step {
  order: 2; flex: 0 0 auto; width: 22px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center; line-height: 1;
  background: none; color: var(--muted); border: none;   /* plain +/- glyphs, no box (Resolume) */
  font-size: var(--fs-glyph); cursor: pointer;
}
.ly-row .ly-step-plus { order: 3; border-left: 0; }
.ly-row .ly-step:hover { color: var(--accent); }
/* Cog sits in the ~14px left gutter (the section-triangle position) — absolute so it
   never shifts the label; appears next to the name on row hover. */
.ly-row .anim-cog-wrap { position: absolute; left: 0; top: 0; bottom: 0; width: var(--col-gutter); display: flex; align-items: center; justify-content: center; margin: 0; }

/* --- Control tactility (professional pass): press feedback, clearer selection,
   accent focus rings, one transition curve. Additive — no layout change. --- */
button, .ctrl-btn, .fx-add, .seg-2-btn, .ly-step, .dir-btn, .tab, .island-tab {
  transition: color 120ms ease, background 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
}
button:active, .ctrl-btn:active, .fx-add:active, .dir-btn:active { box-shadow: inset 0 1px 2px rgba(0,0,0,.45); }
.ly-row .ly-step:active { color: var(--accent); }
.seg-2-btn.is-on { box-shadow: inset 0 0 0 1px var(--accent-line), inset 0 1px 0 rgba(0,0,0,.3); }
button:focus-visible, .ctrl-btn:focus-visible, .seg-2-btn:focus-visible, .ly-step:focus-visible, .dir-btn:focus-visible, select:focus-visible {
  outline: 1px solid var(--accent-line); outline-offset: 1px;
}
/* A param moved off its default reads in a soft accent tint (vs the muted default),
   so changed values stand out at a glance. Focus still wins with the bright accent. */
.ly-param.is-modified .ly-plabel { color: var(--accent-text); }
.ly-param.is-modified .ly-readout { color: var(--accent-text); }
/* Selected param: clicking the label/row focuses it → arrow keys nudge. */
.ly-row:focus { outline: none; }
.ly-row:focus .ly-plabel { color: var(--accent); }
.ly-row:focus .ly-readout { border-color: var(--accent-line); }
/* Editable readout: a borderless number field that reads as plain text until you
   hover/focus it (then a subtle recessed fill). No spinner arrows (type=text). */
/* Always reads as an editable field (touch-first — a faint recessed fill so you
   know to tap it, no hover required); brighter on focus. */
input.ly-readout {
  font: inherit; box-sizing: border-box; background: transparent; color: var(--readout);
  border: 1px solid transparent; border-radius: var(--radius-sm); padding: 0 8px; margin: 0; cursor: ew-resize;
}
input.ly-readout:hover { background: color-mix(in srgb, var(--field-bg) 55%, transparent); }
input.ly-readout:focus { outline: none; background: var(--field-bg); border-color: var(--line-2); color: var(--text); cursor: text; }   /* the box appears only on click/focus */
/* Param-row slider (Resolume style): a chunky dark track with a DIM accent fill
   to --fill, capped by a 2px BRIGHT accent edge line at the value. No round thumb
   — the thumb is a transparent grab strip; the edge line tracks the value exactly. */
.ly-row input.ly-readout { flex: 0 0 var(--col-val); text-align: right; }   /* fixed value column → sliders align */
/* The fill gradient lives on the INPUT box (not the native track) so it spans
   edge-to-edge with no thumb inset — the dim fill is flush to the left. */
.ly-row input[type=range] {
  order: 4; flex: 1 1 0; width: auto; min-width: 0;   /* fill the row — no dead space before the cog */
  -webkit-appearance: none; appearance: none; cursor: pointer; box-sizing: border-box;   /* height from the shared 30px row rule */
  border: 1px solid var(--line); border-radius: var(--radius-sm);   /* own box (steppers are borderless now) */
  /* Full-height fill to the value (dim accent) capped by a 2px bright accent edge. */
  background: linear-gradient(to right,
    color-mix(in srgb, var(--accent) 30%, var(--field-bg)) calc(var(--fill, 50%) - 2px),
    var(--accent) calc(var(--fill, 50%) - 2px),
    var(--accent) var(--fill, 50%),
    var(--field-bg) var(--fill, 50%));
}
.ly-row input[type=range]::-webkit-slider-runnable-track { height: 100%; background: transparent; border: none; }
.ly-row input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 10px; height: var(--ctrl-h); margin-top: 0; background: transparent; border: none;
}
.ly-row input[type=range]::-moz-range-track { height: 100%; background: transparent; border: none; }
.ly-row input[type=range]::-moz-range-progress { background: transparent; }
.ly-row input[type=range]::-moz-range-thumb { width: 10px; height: var(--ctrl-h); border: none; background: transparent; }

.ly-fx { border: none; border-top: 1px solid var(--line); border-radius: 0; padding: var(--s3) 0 0; margin: var(--s2) 0 0; background: transparent; }
/* Selected effect (click its header) — Backspace deletes it. */
.ly-fx.is-sel .ly-fxname { color: var(--accent); }   /* selected effect = accent name only, no left bar */
.ly-fxhead { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; cursor: grab; }
.ly-fxhead:active { cursor: grabbing; }
.ly-fxname { color: var(--accent); font-size: var(--fs-body); }

/* Head options as small inline glyph buttons (save / duplicate / reset / remove),
   pushed to the right of the clip/effect name. */
.fx-acts { display: flex; align-items: center; gap: 1px; margin-left: auto; flex: 0 0 auto; }
.fx-act {
  padding: 0 5px; line-height: var(--lh-ctrl); font-size: var(--fs-glyph); min-width: 0;
  background: none; border: none; border-radius: var(--radius-sm); color: var(--faint); cursor: pointer;
}
.fx-act:hover { color: var(--accent); }
.fx-act-danger:hover { color: var(--danger); }
.fx-act:disabled { opacity: .25; cursor: default; }
/* The clear/delete × sprite inside act buttons + deck clear buttons. */
.fx-act .ic, .lh-clear .ic, .insp-sec-reset .ic { width: 12px; height: 12px; fill: currentColor; display: block; }
.dir-btn .ic { width: 14px; height: 14px; fill: currentColor; display: block; margin: 0 auto; }   /* autopilot transport sprites */
.lh-clear { display: inline-flex; align-items: center; justify-content: center; }
.fx-act:disabled:hover { color: var(--faint); }
.fx-acts .fx-menu-wrap { margin-left: 0; }

/* Effect options "⋯" popover menu. */
.fx-menu-wrap { position: relative; flex: 0 0 auto; }
.fx-menu-btn { padding: 0 7px; line-height: var(--lh-ctrl); font-size: var(--fs-glyph); min-width: 0; }
.fx-menu {
  position: absolute; right: 0; top: 100%; margin-top: 3px; z-index: 20;
  background: var(--panel-2); border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: 3px; min-width: 126px; display: flex; flex-direction: column; gap: 1px;
  box-shadow: var(--shadow-pop);
}
.fx-menu-label { font-size: var(--fs-micro); color: var(--muted); padding: 3px 7px 1px; text-transform: uppercase; letter-spacing: var(--ls-caps); }
.fx-menu-sep { height: 1px; background: var(--line); margin: 2px 0; }
.fx-menu-item { background: none; border: none; text-align: left; padding: 5px 8px; border-radius: var(--radius); font-size: var(--fs-body); color: var(--text); }
/* "Control" publish tick — a real themed checkbox box (matches input[type=checkbox]),
   not the tiny ☑ glyph it replaced. Lives in the anim mode flyout (#anim-pop). */
.fx-tick-box { width: 14px; height: 14px; flex: 0 0 auto; box-sizing: border-box; position: relative;
  border: 1px solid var(--line-2); border-radius: var(--radius); background: var(--field-bg); }
.fx-menu-danger { color: var(--danger); }
.ly-rmfx { color: var(--danger); border-color: var(--line); }
.ly-addfx { margin-top: 4px; }
.ly-addfx select {
  background: var(--field-bg); color: var(--text); border: 1px solid var(--field-border);
  border-radius: var(--radius); height: var(--ctrl-h); padding: 0 7px; font: var(--fs-ctrl) var(--sans); width: 100%; box-sizing: border-box;
}

/* Layer name as an inline editable field in the header. */
.ly-nameedit {
  flex: 1 1 auto; min-width: 0; margin-right: 6px;
  background: var(--field-bg); color: var(--text); font-weight: var(--fw-med); font-size: var(--fs-ctrl);
  border: 1px solid transparent; border-radius: var(--radius); padding: 2px 5px;
  font-family: var(--sans); box-sizing: border-box;
}
.ly-nameedit:focus { border-color: var(--line-2); outline: none; background: var(--panel); }

/* FX section labels: clip FX vs layer FX colour-coded to read the param split. */
.ly-fxlabel { grid-column: 1 / -1; }
.ly-fxlabel-clip { color: var(--accent); }
.ly-fxlabel-layer { color: var(--accent); }
.ly-fx-clip { border-color: var(--line); }
.ly-fx-layer { border-color: var(--line); }

/* --- Layer row (control block + horizontal slot row), Resolume-style --- */
/* align-self:flex-start → the row shrinks to its content (head + clip columns)
   instead of stretching across the panel, so the bottom border stops at the last
   clip column rather than running out into the pasteboard. */
.deck-layer { display: flex; align-items: stretch; gap: 0; margin: 0; align-self: flex-start; border-bottom: 1px solid var(--line); }
/* The composition-group box supplies the outer top/bottom edges, so the first row
   takes its top line from the header and the last row drops its bottom line (no
   doubled hairline against the group border). */
.deck-layer:last-child { border-bottom: none; }
/* Solo active elsewhere → this layer isn't rendering. Dim the whole row so the
   deck mirrors the output; the row stays clickable (its S still un-solos it). */
.deck-layer.is-muted { opacity: .4; transition: opacity var(--t-fast); }
/* MASTER row above the layers: ✕ · B (each the layer-button width) + a horizontal
   opacity fader filling the rest — same look as the vertical layer fader, on its side. */
.deck-master { align-self: stretch; }
.master-head { display: flex; align-items: stretch; width: 100%; height: var(--ctrl-h); }
/* ✕ + B match the layer-button width; the "Master" label fills the rest of the head
   column so the slider STARTS exactly at the first clip column (X + B + label = --layer-w). */
.master-head .lh-tog, .master-head .lh-clear { flex: 0 0 calc(var(--layer-w) / 4); min-width: 0; }
.master-label { flex: 0 0 calc(var(--layer-w) / 2); box-sizing: border-box; display: flex; align-items: center; justify-content: center;
  font: var(--fw-med) var(--fs-micro) var(--sans); text-transform: uppercase; letter-spacing: var(--ls-caps);
  color: var(--muted); border-right: 1px solid var(--line); }
.master-op { position: relative; flex: 1 1 auto; min-width: 0; cursor: pointer; box-sizing: border-box;
  background: var(--line-2); border: 1px solid var(--line); border-left: 0; overflow: hidden; }
.master-op-fill { position: absolute; top: 0; bottom: 0; left: 0; width: var(--fill, 100%);
  background: color-mix(in srgb, var(--accent) 30%, var(--field-bg)); pointer-events: none; }
/* Bright accent handle line at the value; clamp 1px inside so it stays visible at 100%. */
.master-op-handle { position: absolute; top: 0; bottom: 0; width: 2px; left: min(var(--fill, 100%), calc(100% - 1px));
  transform: translateX(-50%); background: var(--accent); pointer-events: none; }
/* "fx" marker next to a layer / clip name when it carries an effect chain.
   Inherits the bar's text colour (dimmed) so it stays legible on the accent
   highlight of a selected row too. */
.deck-fx { margin-left: 6px; font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .05em; opacity: .6; }
/* (The in-deck composition group/header was removed — the deck renders the layer
   rows directly into the Timeline island, which supplies the frame + title.) */
/* No horizontal row separators — there are no clip grids that need them. */
.deck-layer.is-min .layer-head-box { flex-direction: column; gap: 0; padding: 0; justify-content: space-between; align-items: stretch; }
.deck-layer.is-min .lh-body { padding: 4px 6px; }
.deck-layer.is-min .lh-name {
  width: 100%; box-sizing: border-box; margin: 0; border-radius: 0;
  text-align: left; padding: 3px 7px; font-size: var(--fs-micro); font-weight: var(--fw-med);
  background: var(--surface-2); color: var(--text); border: none; border-top: 1px solid var(--line);
}
.deck-layer.is-min.is-sel .lh-name { background: var(--accent); color: var(--accent-dark); }

/* Multi-layer stack: flush rows, thin separators (flat Resolume grid). */
/* Unlimited layers: the stack scrolls once it would exceed the viewport (the
   "+ layer" button sits below, always reachable). */
.deck-layers { display: flex; flex-direction: column; gap: 0; border: none;
  flex: 1 1 auto; min-height: 0; overflow: auto; }
.deck-addlayer { margin-top: 4px; align-self: flex-start; width: auto; padding: 5px 12px; color: var(--accent); }
.lh-blend {
  width: 100%; margin-top: 3px; box-sizing: border-box;
  background: var(--field-bg); color: var(--text); border: 1px solid var(--line-2);
  border-radius: 0; padding: 2px 4px; font: var(--fs-micro) var(--sans);
}
/* Selected layer (its inspector is open) = an accent NAME fill, matching clips. */
.deck-layer.is-sel .lh-name { background: var(--accent); color: var(--accent-dark); }
.layer-head-box.drop-hover { outline: 2px dashed var(--accent); outline-offset: -2px; }
.layer-head-box {
  flex: 0 0 var(--layer-w); box-sizing: border-box; display: flex; flex-direction: column; gap: 0;
  background: var(--panel-2); border: none; border-right: 1px solid var(--line); border-radius: 0; padding: 0;
}
/* Head body = four equal quarters (B · S · ✕ · opacity), filling the height. */
/* No top/side inset — the B·S·✕ bar reaches the head-box edges; a small bottom
   gap keeps it off the name label. */
.lh-body { display: flex; gap: 0; flex: 1 1 auto; padding: 0 0 4px; align-items: stretch; }
/* All FOUR quarters are equal width: ✕ · B · S · opacity. */
.lh-body > .lh-tog, .lh-body > .lh-clear, .lh-body > .lh-op { flex: 1 1 0; min-width: 0; }
/* Read as ONE segmented bar — flush, sharing borders (every segment after the
   first drops its left border). */
.lh-body > *:not(:first-child) { border-left: 0; }
/* Opacity quarter = a custom vertical fader: a segment box (matching the B·S·✕
   chrome) with a bottom-up accent fill to --fill and a full-width rounded handle
   bar riding at the value. Built from divs (not a native range) for a clean,
   cross-browser full-width handle. */
.lh-op {
  position: relative; box-sizing: border-box; cursor: pointer;
  background: var(--line-2); border: 1px solid var(--line);
}
.lh-op-fill {
  position: absolute; left: 0; right: 0; bottom: 0; height: var(--fill, 100%);
  background: color-mix(in srgb, var(--accent) 30%, var(--field-bg)); pointer-events: none;
}
/* Handle: a full-width BRIGHT accent edge line at the value (matches the param
   sliders), riding the top of the bottom-up dim fill. */
.lh-op-handle {
  position: absolute; left: 0; right: 0; height: 2px;
  bottom: var(--fill, 100%); transform: translateY(50%);
  background: var(--accent); pointer-events: none;
}
.lh-op-val { font-size: var(--fs-micro); color: var(--readout); font-variant-numeric: tabular-nums; }
.lh-clear {
  padding: 0; border-radius: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-ctrl);
  color: var(--muted); background: var(--field-bg); border: 1px solid var(--line);
}
.lh-clear:hover { color: var(--text); background: var(--hover); }
.lh-blend-label { color: var(--muted); margin-top: 2px; }   /* size/caps/tracking from the shared header recipe */
.lh-ctrls select {
  background: var(--field-bg); color: var(--text); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 2px 4px; font: var(--fs-micro) var(--sans); width: 100%; box-sizing: border-box;
}
/* Layer name bar — matches .clip-label-bar exactly (same padding/type) so the
   layer label and clip labels share one baseline across the deck row. */
.lh-name {
  background: var(--surface-2); color: var(--text); font-weight: var(--fw-med); font-size: var(--fs-body);
  text-align: left; border: none; border-radius: 0; padding: 4px 7px;
  font-family: var(--sans); box-sizing: border-box; width: 100%;
}
.lh-name:focus { outline: none; border-color: var(--line-2); }

/* --- Clip slot row: flush, no horizontal gaps (shared borders) --- */
.clip-deck {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-wrap: nowrap; gap: 0; margin: 0; overflow-x: auto; padding-bottom: 0;
}
.clip-deck > .clip-cell { border-radius: 0; }
.clip-deck > .clip-cell + .clip-cell { margin-left: -1px; }       /* collapse the shared border */
/* keep the active/selected outline above neighbouring cells */
.clip-active, .clip-selected, .clip-playhead { z-index: 1; }
/* Empty placeholder slot: dashed drop target, dimmer than a filled clip. */
.clip-empty {
  display: flex; align-items: center; justify-content: center;
  border-style: dashed; border-color: var(--line); background: var(--surface); opacity: .6;
}
.clip-empty-plus { font-size: var(--fs-glyph); color: var(--faint); line-height: 1; }

/* Clip slot: thumbnail on top, a label bar UNDERNEATH (Resolume-style). */
.clip-thumbwrap { position: relative; flex: 1 1 auto; min-height: 48px; width: 100%; background: #000; }
.clip-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }
.clip-label-bar {
  padding: 4px 7px; font-size: var(--fs-body); font-weight: var(--fw-med); color: var(--text);
  background: var(--surface-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.clip-fxcount {
  position: absolute; top: 3px; right: 4px; font-size: var(--fs-micro); color: var(--accent);
  background: var(--badge-bg); padding: 0 4px; border-radius: var(--radius);
}
/* Triggerable source (e.g. Pulse) → a ⚡ badge in the opposite corner. */
.clip-trig {
  position: absolute; top: 3px; left: 4px; font-size: var(--fs-micro); line-height: 1;
  color: var(--accent); background: var(--badge-bg); padding: 1px 3px; border-radius: var(--radius);
}
.deck-layer.is-min .clip-trig { display: none; }
/* Volumetric source → a "3D" badge (bottom-left of the thumb): lights LEDs at
   their world xyz, never drawn on the canvas. Sits under a ⚡ when both apply. */
.clip-vol {
  position: absolute; bottom: 3px; left: 4px; font-size: var(--fs-micro); line-height: 1;
  color: var(--accent); background: var(--badge-bg); padding: 1px 3px; border-radius: var(--radius);
}
.deck-layer.is-min .clip-vol { display: none; }
/* Modulation badges (top-right of the thumb): A = audio-driven, E = external-
   driven — which clips are being adjusted from outside, at a glance. */
.clip-mods { position: absolute; top: 3px; right: 4px; display: flex; gap: 2px; }
.clip-mod {
  font-size: var(--fs-micro); line-height: 1; color: var(--accent);
  background: var(--badge-bg); padding: 1px 3px; border-radius: var(--radius);
}
.deck-layer.is-min .clip-mods { display: none; }
/* SELECTED (single click, edit target) = a colour FILL on the label bar.
   LIVE/ACTIVE (double click, playing) = a colour OUTLINE around the cell.
   Inset ring hugs the cell's exact shape and never bleeds onto the flush deck. */
/* Same accent colour for both: SELECTED = a colour FILL on the label bar,
   LIVE = a colour OUTLINE around the cell (overlay so the thumbnail/label don't
   paint over it). A clip that's both shows the fill + the outline. */
.clip-selected .clip-label-bar { background: var(--accent); color: var(--accent-dark); }
.clip-active::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
  box-shadow: inset 0 0 0 1px var(--accent); z-index: 2;
}

/* Library item: thumbnail + label (sources), label-only (effects). */
.lib-item { display: flex; align-items: center; gap: 7px; }
.lib-thumb { width: 30px; height: 19px; flex: 0 0 auto; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--line); }
.lib-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clip-cell {
  position: relative; flex: 0 0 auto; width: var(--clip-w); height: var(--clip-h);
  background: var(--surface); border: 1px solid var(--field-border); border-top: 0; border-bottom: 0; border-radius: 0;
  overflow: hidden; cursor: pointer; user-select: none; display: flex; flex-direction: column;
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.clip-add {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; cursor: default; border-style: dashed;
}
.clip-add select {
  background: var(--field-bg); color: var(--text); border: 1px solid var(--field-border);
  border-radius: var(--radius); padding: 2px 4px; font: var(--fs-micro) var(--sans); width: 100%; box-sizing: border-box;
}

/* --- Single-layer composer (deck + draggable Sources/Effects rail) --- */
.cmp2-panel { margin-bottom: var(--s4); }
.composer-body { display: flex; gap: 0; align-items: stretch; }
.composer-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.composer-rail {
  flex: 0 0 116px; display: flex; flex-direction: column; gap: 0;
  border-left: 1px solid var(--border-2); padding-left: 8px; margin-left: 8px;
}
.composer-label { color: var(--accent); margin-top: var(--s2); }   /* recipe handles font/casing */
.composer-label-fx { color: var(--accent); margin-top: var(--s3); }

.lib-list { display: flex; flex-direction: column; gap: 0; }
.lib-item {
  font-size: var(--fs-body); color: var(--text); background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 0; padding: 3px 7px; cursor: grab; user-select: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lib-item + .lib-item { margin-top: -1px; }   /* collapse shared borders into a 1px hairline (tile flush) */
.lib-item:active { cursor: grabbing; }
.lib-video { color: var(--accent); border-style: dashed; justify-content: center; cursor: pointer; }

/* Drop affordance shared by clip cells, the "+" cell, and the effect drop zone. */
.drop-hover {
  border-color: var(--accent) !important;
  box-shadow: inset 0 0 0 1px var(--accent) !important;
  background: var(--accent-soft) !important;
}
/* Pointer-drag (deck clips + layer heads): the grabbed item dims, and a small
   chip rides the cursor as the ghost. */
.deck-dragging { opacity: .4; }
.deck-ghost {
  position: fixed; left: 0; top: 0; z-index: 60; pointer-events: none;
  max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 3px 8px; font: var(--fs-body) var(--sans); color: var(--accent-text);
  background: var(--accent-soft); border: 1px solid var(--accent-line); border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
}
.composer-drop {
  grid-column: 1 / -1; text-align: center; font-size: var(--fs-body); color: var(--faint);
  border: 1px dashed var(--field-border); border-radius: var(--radius); padding: 8px; margin-top: 4px;
}
/* "+ effect" / "+ source" affordance — opens the picker popover. */
.composer-add {
  width: 100%; margin-top: 4px; padding: 6px 0; text-align: center;
  background: none; border: 1px dashed var(--field-border); border-radius: var(--radius);
  color: var(--faint); font: var(--fs-body) var(--sans); cursor: pointer;
}
.composer-add:hover { color: var(--accent); border-color: var(--accent-line); }
/* Picker popover (sources / effects), anchored to its "+" trigger. */
.pick-pop {
  position: fixed; z-index: 50; min-width: 190px; max-height: 60vh; overflow-y: auto;
  background: var(--panel-solid); border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: 4px; display: flex; flex-direction: column; gap: 1px; box-shadow: var(--shadow-pop);
  /* Long option names scroll inside the popover (per-group, below) instead of
     widening it past the window edge. */
  max-width: min(300px, calc(100vw - 16px));
}
.pick-item {
  display: flex; align-items: center; gap: 8px; padding: 5px 7px; border-radius: var(--radius-sm);
  font-size: var(--fs-body); color: var(--text); cursor: pointer;
}
.pick-item:hover { background: var(--hover); color: var(--accent); }
.pick-video { color: var(--faint); }
/* Effect picker (ungrouped) stays a compact single-column list. */
.pick-grid { display: flex; flex-direction: column; gap: 0; overflow-x: auto; }
.pick-grid .pick-item { width: max-content; min-width: 100%; box-sizing: border-box; }
.pick-grid .lib-label { overflow: visible; text-overflow: clip; }
/* Source browser: a WIDE multi-column grid of preview cards (thumbnail over label),
   showing all categories at once. Overrides the compact list rules above via specificity. */
.pick-pop.pick-grouped { width: min(680px, calc(100vw - 16px)); max-width: none; }
.pick-pop.pick-grouped .pick-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
  gap: var(--s2); overflow-x: visible;
}
.pick-pop.pick-grouped .pick-item {
  flex-direction: column; align-items: stretch; text-align: left;
  width: auto; min-width: 0; gap: 4px; padding: 5px;
}
.pick-pop.pick-grouped .lib-thumb { width: 100%; height: 58px; }
.pick-pop.pick-grouped .lib-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-group { font: var(--fw-med) var(--fs-micro)/var(--lh-tight) var(--sans); letter-spacing: var(--ls-caps);
  text-transform: uppercase; color: var(--faint); padding: 7px 7px 3px; }
.pick-group:first-child { padding-top: 2px; }
.comp-fx { margin-top: 8px; }   /* section provides its own top hairline */

/* Sources tab (Output island): hosts the TD-style source browser (below). */
#patch-sources { overflow-y: auto; padding: 2px 4px; }

/* Source browser (Sources tab): one scrolling list of thumbnail cards, grouped by
   2D / 3D / Shaders section headers, with a description line. */
.src-browser { display: flex; flex-direction: column; min-height: 0; height: 100%; }

/* One scroll; the 2D / 3D / Shaders subgroups are section HEADERS (a small category dot
   + a faint uppercase label), not tabs. */
.src-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 2px; }
/* Airy group header (2D / 3D / Shaders), matching the Output/Library headers: accent
   caps + a thin accent-24% underline, with a small category-colour dot. */
.src-group-head { display: flex; align-items: center; gap: var(--s3); padding: 0 var(--s1) var(--s2); margin: var(--s4) 0 var(--s3);
  font: var(--fw-med) var(--fs-micro) var(--sans); letter-spacing: var(--ls-caps); text-transform: uppercase;
  color: var(--accent); border-bottom: 1px solid var(--accent-underline); }
.src-group:first-child > .src-group-head { margin-top: 0; }
.src-group-head::before { content: ''; width: var(--s3); height: var(--s3); border-radius: 50%; background: var(--cat, var(--faint)); flex: none; }
/* A compact PALETTE — responsive columns instead of one long single-column scroll (the
   set is large). Adapts: ~2 cols in the narrow sidebar, more in the wide clip picker.
   --src-col = the min card width that triggers a new column (one-off layout constant). */
.src-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--src-col, 150px), 1fr)); gap: 1px var(--s1); align-content: start; }
.src-card { position: relative; display: flex; flex-direction: row; align-items: center; gap: var(--s6); padding: var(--s2) var(--s6); cursor: grab; border: 1px solid transparent; }
.src-card:hover { background: var(--hover); }
.src-card:active { cursor: grabbing; }
.src-thumb { width: 100%; height: 46px; object-fit: cover; display: block; background: #000; }
.src-thumb-add { display: flex; align-items: center; justify-content: center; color: var(--faint); font: 20px/1 var(--mono); background: var(--panel-2); }
.src-label { font: var(--fs-ctrl) var(--mono); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.src-card:hover .src-label { color: var(--text); }
/* Source + effect NAMES read as uppercase (mono/OCR aesthetic) — browser cards,
   picker items, clip label bar, and the effect-chain rows. */
/* Content names read in Title Case (labelOf already title-cases) — caps is reserved
   for section-header eyebrows only, so it actually signals structure. */
.src-label, .lib-label, .ly-fxname, .clip-label-bar span { text-transform: none; }
.src-isf .src-label, .src-video .src-label { color: var(--faint); }

.src-desc { flex: none; min-height: 16px; padding: 4px 2px 0; border-top: 1px solid var(--line);
  color: var(--faint); font: var(--fs-micro) var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Segmented toggle (Segmented in controls.js): the house control for a small fixed
   choice set (2–3), instead of a <select>. Flat + hairline; accent tint when on. */
.seg-field > .seg-2 { flex: 1 1 0; min-width: 0; }
.seg-2 { display: flex; min-width: 0; height: var(--ctrl-h); border: 1px solid var(--field-border); border-radius: var(--radius); overflow: hidden; }
.seg-2-btn { flex: 1 1 0; min-width: 0; height: 100%; background: transparent; border: none; color: var(--muted);
  padding: 0 8px; font: var(--fs-ctrl) var(--sans); letter-spacing: var(--ls-ui); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.seg-2-btn + .seg-2-btn { border-left: 1px solid var(--field-border); }
.seg-2-btn:hover { color: var(--text); background: var(--hover); }
.seg-2-btn.is-on { color: var(--accent-text); background: var(--accent-soft); }   /* shared segmented-on tint (was a one-off 14% mix) */

/* Transport bar (play-through of the clip timeline). */
.composer-deckhead { display: flex; align-items: center; justify-content: flex-end; margin-bottom: 4px; }
.transport { display: flex; align-items: center; gap: 8px; }
.transport-play { height: var(--ctrl-h); padding: 0 12px; font-size: var(--fs-body); font-weight: var(--fw-med); color: var(--accent); border-color: var(--line); }
.transport-play.is-playing { color: var(--danger); border-color: var(--line); background: color-mix(in srgb, var(--danger) 8%, transparent); }
.transport-loop { display: flex; align-items: center; gap: 3px; font-size: var(--fs-micro); color: var(--muted); }
.transport-loop input { width: auto; }
.transport-fire { height: var(--ctrl-h); padding: 0 10px; font-size: var(--fs-body); font-weight: var(--fw-med); color: var(--accent); border-color: var(--line); }
.transport-fire:active { background: color-mix(in srgb, var(--accent) 28%, transparent); }

/* Playhead highlight: the slot the transport is currently on. */
.clip-playhead {
  box-shadow: 0 0 0 1px var(--accent);
  border-color: var(--accent) !important;
}

/* Active-clip editor: a flat single-column block so collapsible sections stack
   cleanly (no grid for the Section wrappers to fight). */
.clip-editor {
  display: block; border: none; border-radius: 0; padding: 0; margin: 0; background: transparent;
}
.clip-editor-head {
  grid-column: 1 / -1; font-size: var(--fs-body); color: var(--accent); font-weight: var(--fw-med);
  margin-bottom: 2px; display: flex; align-items: center; gap: 6px;
}
.clip-editor-head .ly-nameedit { flex: 1 1 auto; min-width: 0; }
.ly-fxhead .fx-acts, .clip-editor-head .fx-acts { margin-left: auto; }
.clip-trigger {
  grid-column: 1 / -1; width: 100%; margin: 2px 0 4px; font-weight: var(--fw-med);
  color: var(--accent); border-color: var(--line);
}
.clip-trigger:active { background: color-mix(in srgb, var(--accent) 30%, transparent); }

/* --- Settings: a traditional modal preferences dialog ---------------------- */
.modal { display: none; position: fixed; inset: 0; z-index: 60; align-items: flex-start; justify-content: center; }
.modal:not([hidden]) { display: flex; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,.55); }
.modal-card {
  position: relative; margin: 7vh 0; width: min(620px, 92vw); max-height: 86vh;
  display: flex; flex-direction: column; background: var(--panel);
  border: 1px solid var(--line-2); border-radius: var(--radius); box-shadow: var(--shadow-modal);
}
.modal-head {
  display: flex; align-items: center; justify-content: space-between; flex: 0 0 auto;
  padding: 12px 16px; border-bottom: 1px solid var(--line);
}
.modal-title { font-size: var(--fs-title); font-weight: var(--fw-med); color: var(--text); letter-spacing: 0.02em; }
.modal-close {
  background: none; border: none; color: var(--muted); font-size: 14px; line-height: 1; cursor: pointer;
  padding: 3px 7px; border-radius: var(--radius-sm);
}
/* Symmetric breath, aligned with the head padding. */
.modal-body { overflow-y: auto; padding: 14px 16px 16px; }

/* (The old Settings tab styles were removed; project file actions live in the
   corner File menu.) */

/* --- System › Settings ----------------------------------------------------- */
.fx-add.on { color: var(--accent); border-color: var(--accent-line); }   /* toggle: enabled */
.accent-swatches { display: flex; gap: 6px; margin: 4px 0 4px; }
.accent-swatch {
  flex: 1 1 0; min-width: 0; height: 28px; padding: 0; border-radius: var(--radius);
  border: 1px solid var(--line-2); cursor: pointer;
  opacity: .4; transition: opacity var(--t-fast);   /* inactive colours read dimmed */
}
.accent-swatch:hover { opacity: .75; }
.accent-swatch.is-on { opacity: 1; box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--text); }

/* --- UPPERCASE chrome (C6) --------------------------------------------------
   App-authored chrome reads uppercase (design-system casing: buttons/nav/section
   eyebrows are caps); user-entered names stay as typed. Tabs, section titles, group
   labels and the top-bar captions are already uppercase above — this covers the rest. */
button { text-transform: uppercase; }              /* all app-authored button labels */
.fx-menu-item { text-transform: none; }            /* EXCEPT preset menus — user-authored names */
.lr-name { text-transform: none; }                 /* fixture/clip display names — user-authored */
.menu-item { text-transform: uppercase; }          /* dropdown menu entries */
/* Parameter/field labels stay Title Case (labelOf/prettyParam already title-case) —
   consistent with the modulatable param rows; caps is reserved for section headers. */
.fx-badge { text-transform: uppercase; }           /* count/range badges ('px' → 'PX' is fine) */
.knob-label { text-transform: uppercase; }         /* dashboard knob captions ('Link 1' …) */

/* Update-available banner — a small fixed chip (bottom-right), dismissible. Fed by
   the daemon's GitHub Releases check (notify-only; it links to the release, never
   self-installs). See showUpdateBanner in app.js. */
.update-banner { position: fixed; right: var(--s6); bottom: var(--s6); z-index: 9999;
  display: flex; align-items: center; gap: var(--s5);
  background: var(--panel); color: var(--text); border: 1px solid var(--line);
  border-radius: var(--radius); padding: var(--s4) var(--s5);
  font: var(--fs-ctrl) var(--ui-font); box-shadow: 0 6px 20px rgba(0, 0, 0, .35); }
.update-banner .ub-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex: none; }
.update-banner .ub-text { letter-spacing: var(--ls-ui); }
.update-banner .ub-link { color: var(--accent-text); text-decoration: none; border-bottom: 1px solid var(--accent-underline); }
.update-banner .ub-link:hover { color: var(--accent); }
.update-banner .ub-x { background: none; border: none; color: var(--muted); cursor: pointer; font: inherit; font-size: 1.1em; line-height: 1; padding: 0 0 0 var(--s3); }
.update-banner .ub-x:hover { color: var(--text); }
/* Save and output status stay visible until the underlying condition is resolved. */
#runtime-notices{position:fixed;z-index:10000;top:12px;left:50%;transform:translateX(-50%);width:min(760px,calc(100vw - 32px));display:grid;gap:8px;pointer-events:none}
.runtime-notice{display:flex;align-items:center;flex-wrap:wrap;gap:10px;padding:12px 16px;background:var(--bg,#181818);color:var(--fg,#eee);border:1px solid var(--accent,#e09a4a);box-shadow:0 4px 24px #0006;pointer-events:auto;font-size:12px;line-height:1.5}
.runtime-notice>span{flex:1 1 320px}
.runtime-notice button{padding:6px 10px;cursor:pointer;color:var(--fg,#eee);border:1px solid var(--accent,#e09a4a);background:transparent}
