  /* Inter variable font, self-hosted (Google Fonts v20 latin subset).
     Previously imported from fonts.googleapis.com — the @import was
     render-blocking, which left the Electron window black until the
     network responded (problematic on flaky WiFi or offline stages).
     Single .woff2 covers weights 300–600 via variable-axis interpolation. */
  @font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('fonts/Inter-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  }
  /* Urbanist variable font, self-hosted (Google Fonts v18 latin subset).
     Added 2026-04-22 for the SolarEdge-inspired main page redesign —
     used via --font-display only for hero / section-title type. Inter
     remains the body UI font; Roboto Mono remains the numeric readout
     font. Self-hosted to keep the app usable offline (same rationale as
     Inter above). ~27 KB on first load. */
  @font-face {
    font-family: 'Urbanist';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('fonts/Urbanist-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  }
  * { margin: 0; padding: 0; box-sizing: border-box; }

  /* ── Dark scrollbars (global) ────────────────────────────────────────────
     Chrome/Safari use ::-webkit-scrollbar; Firefox uses scrollbar-width +
     scrollbar-color. Thin translucent-white thumb on a transparent track
     so scrollbars recede into whatever panel or chrome they appear in. */
  * {
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, 0.14) transparent;
  }
  *::-webkit-scrollbar        { width: 8px; height: 8px; }
  *::-webkit-scrollbar-track  { background: transparent; }
  *::-webkit-scrollbar-thumb  {
    background: rgba(255, 255, 255, 0.12);
    border-radius: 4px;
    transition: background 0.15s ease;
  }
  *::-webkit-scrollbar-thumb:hover  { background: rgba(255, 255, 255, 0.22); }
  *::-webkit-scrollbar-thumb:active { background: rgba(255, 255, 255, 0.30); }
  *::-webkit-scrollbar-corner { background: transparent; }

  button { outline: none; }
  button:focus-visible { outline: 1px solid var(--tint-lock-strong); outline-offset: 1px; }
  html, body {
    height: 100dvh;
    max-height: 100dvh;
    overflow: hidden;
  }
  /* Base font-size scales with viewport width so everything grows when the window widens.
     clamp(min, preferred, max) — at 1400px wide: 1vw = 14px → font-size ≈ 14px (0.875rem equiv).
     All rem-based children scale automatically. */
  /* The rem base. Everything in the app is sized off this, so it is the one
     lever that grows the whole instrument together — type, padding, column
     widths — while px-based hairlines, the 2px slider tracks and the 20px
     glyphs stay crisp. Raised 15 → 16 on 2026-08-29 alongside a 12% lift in
     the type scale itself (tokens.css): the boxes grow a little, the text
     grows into them more, which is what "the text can afford to be bigger"
     actually asks for. uiScale still multiplies this. */
  html { font-size: calc(var(--ui-base-px, 16) * 1px); }
  body {
    /* Pure black body — the top-bar and bottom-bar explicitly set their
       own --bg-app grey, so they sit as grey chrome on the black frame.
       The inter-panel gutter (main-layout gap) inherits this black too,
       which matches the canvas bg and lets the grey panels visually
       "float" on a true black surround. */
    background: #000000;
    color: var(--text-primary);
    font-family: var(--font-sans);
    /* Since Urbanist is the single UI face (no monospace sibling), tabular
       figures keep numeric readouts column-aligned where Roboto Mono used
       to do the job. Proportional digits are rare in UI copy, so applying
       this globally has no visible downside in text. */
    font-variant-numeric: tabular-nums;
    display: flex;
    flex-direction: column;
    /* Zero vertical padding + zero gap so .top-bar / .bottom-bar sit flush
       against the viewport top/bottom edges. Their hairline border is then
       the sole visual separator from the main content — the sleek
       minimal convention (Linear / Vercel / Figma) where chrome feels
       structural rather than floating. Horizontal padding kept so panels
       still have breathing room from the viewport sides. Vertical breathing
       room for the panels now lives in .main-layout's own padding. */
    /* No padding on body at all — bars go edge-to-edge (horizontally and
       vertically), and the main-layout owns its own padding for the
       content gutter. */
    padding: 0;
    gap: 0;
    height: 100dvh;
    overflow: hidden;
    overscroll-behavior: none;
  }
  /* ── Top bar (title + ? button) ─────────────────────────────────────────── */
  /* Black background — merges with the body so the top-bar reads as part
     of the frame rather than a piece of floating chrome. Only the footer
     stays grey, since it carries active monitoring (levels + sensors) and
     earns the chrome treatment; the top-bar is just labels and switches. */
  .top-bar {
    background: #000000;
    display: flex;
    flex-direction: row;
    /* flex-end pins both columns to the bottom edge — since the left column
       has the brand stacked above its button row and the right column is
       just a button row, aligning at the bottom puts the right row on the
       same horizontal line as the left's row (below the brand). */
    align-items: flex-end;
    justify-content: space-between;
    flex-shrink: 0;
    border-bottom: none;
    /* Align the header content to the panel-card left/right edges.
       Panel edge lives at: .main-layout padding (0.75rem) + .right-panel
       inner padding (0.4rem) = 1.15rem from the viewport. Previously
       only matched the main-layout padding, which left the header 0.4rem
       left of the panels. */
    padding: 0.4rem 1.15rem 0.5rem;
  }
  .top-bar-left {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.55rem;
    flex-shrink: 0;
  }
  /* Brand block: "mubone" logo stacked over the version line. */
  .top-bar-brand {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    line-height: 1;
  }
  .top-bar-logo {
    font-family: var(--font-display);
    font-size: 1.55rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--text-light);
    text-transform: lowercase;
    line-height: 1;
  }
  .top-bar-version {
    font-size: var(--fs-xxs);
    opacity: 0.4;
    letter-spacing: var(--ls-wide);
    white-space: nowrap;
    margin-top: 0.2rem;
  }
  /* Multi-station instance badge — brighter than the version so the three
     windows are tellable at a glance */
  .top-bar-instance {
    font-size: var(--fs-xxs);
    color: var(--text-light);
    opacity: 0.85;
    letter-spacing: var(--ls-wide);
    white-space: nowrap;
    margin-top: 0.2rem;
  }
  .top-bar-right {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 0.1rem;
    flex-shrink: 0;
  }
  .top-bar-row {
    display: flex;
    align-items: center;
    gap: 0.1rem;
  }
  .top-bar-btn svg {
    vertical-align: middle;
    margin-right: 0.2rem;
  }
  /* #spatialModeBtn replaced by the #cameraModeSeg picker — see "Top-bar segmented picker" */

/* ── .mu-btn--bare — a button with no box ─────────────────────────────────
   The second SHAPE, not a second face: no border, no background, no radius,
   and NO FIXED HEIGHT. The elastic 23.8–28.8px the top bar's buttons render at
   is correct, not broken — there is no box, so there is no box edge to line up
   with, and the only thing that reads is the word. Hover changes COLOUR ONLY.

   Same split the settings kit already has between a bordered button and the
   bare ✕ glyph. Carried by preset-btn (20) and top-bar-btn (11); each keeps its
   own padding and its own resting colour, which is all that is left of them.

   --fs-md is 15.68px. (The ruling said "--fs-body (15.68)"; --fs-body is
   12.48 and --fs-md is the 15.68 both of these already used, so the number is
   what was meant.) */
.mu-btn--bare {
  /* STANDALONE — it does not take .mu-btn. That was tried first and moved 1013
     elements: .mu-btn is a box (width 100%, display flex, fixed height, padding,
     border, background) and --bare is the ABSENCE of a box, so as a modifier it
     would have to undo nine properties to say one thing. A shape that has to
     unset the shape it modifies is not a modifier. */
  background: none; border: none; border-radius: 0;
  font-family: var(--font-sans); font-size: var(--fs-md); cursor: pointer;
  transition: color var(--trans-base);
}
.mu-btn--bare:hover { background: none; border-color: transparent; }

  /* Shape comes from .mu-btn--bare; this is only what is particular to it. */
  .top-bar-btn {
    color: var(--text-muted);
    padding: 0.15rem 0.55rem;
    letter-spacing: var(--ls-wider);
  }
  .top-bar-btn:hover { color: var(--text-primary); border-color: transparent; }
  .top-bar-btn.active { color: var(--text-highlight); border-color: transparent; }
  /* ── OSC bridge indicator — inline in sensor group ─────────────────────── */
  /* Renamed from .max-indicator — the bridge carries any OSC peer (Max,       */
  /* mubone-joycon-gui, foot-pedal, etc.), not just Max/MSP.                   */
  .osc-indicator {
    display: none;            /* hidden until the OSC bridge connects */
    font-size: var(--fs-micro);
    font-family: var(--font-mono);
    letter-spacing: var(--ls-widest);
    color: var(--accent-lock);
    padding-right: 0.3rem;
    line-height: 1;
    user-select: none;
  }
  .osc-indicator.visible { display: inline; }
  /* ── Sensor group — label + separator ──────────────────────────────────── */
  .sensor-group-label {
    font-size: var(--fs-2xs);
    color: var(--text-muted);
    letter-spacing: var(--ls-uppercase);
    text-transform: uppercase;
    padding-right: 0.2rem;
    border-right: 1px solid var(--surface-5);
    margin-right: 0.15rem;
    line-height: 1;
  }
  .sensor-group-status {
    font-size: var(--fs-nano);
    margin-left: 0.15rem;
  }
  .sensor-group.no-sensor .sensor-group-status::before { content: '(not connected)'; }
  /* --accent-lock, not `#7abc`. That was one character short of #7abcbc, the
     pre-August teal, and a four-digit hex is valid CSS: it rendered #77aabbcc,
     teal at 80% alpha — a colour nobody chose. The intent is the lock accent
     (this is the sensor reading CONNECTED), and that has a token. */
  .sensor-group:not(.no-sensor) .sensor-group-status { color: var(--accent-lock); }
  /* ── Action buttons — amber toggle style (cursor zero, sensor on/off, tare) */
  .top-bar-btn.action-btn {
    color: var(--text-dim);
    font-size: var(--fs-base);
    padding: 0.15rem 0.35rem;
    border: 1px solid transparent;
    border-radius: var(--r-2);
    transition: color var(--trans-base), background var(--trans-base), border-color var(--trans-base);
  }
  .top-bar-btn.action-btn:hover { color: var(--accent-action); border-color: rgba(232,160,48,0.2); }
  .top-bar-btn.action-btn.active {
    color: var(--accent-action);
    background: var(--tint-action-soft);
    border-color: var(--tint-action-border);
  }
  /* Legacy tare-btn: alias to action-btn for backward compat */
  
  
  
  /* ── mic + mute as inline top-bar buttons ─────────────────────────────── */
  .top-bar #micEnableBtn,
  .top-bar #muteBtn {
    display: inline-flex;
    flex-direction: row;
    align-items: center;
    gap: 0.3rem;
    background: transparent;
    border: none;
    border-bottom-width: 0px;
    border-radius: 0;
    color: var(--text-muted);
    font-family: var(--font-sans);
    font-size: var(--fs-md);
    padding: 0.15rem 0.55rem;
    letter-spacing: var(--ls-wider);
    cursor: pointer;
    flex: 0 0 auto !important;
    width: auto !important;
    position: static;
    transition: color var(--trans-base), border-color var(--trans-base), background var(--trans-base);
  }
  .top-bar #micEnableBtn:hover { color: var(--text-primary); border-color: transparent; background: transparent; }
  .top-bar #muteBtn:hover     { color: var(--text-primary); border-color: transparent; background: transparent; }
  /* mic-dot: inline red circle */
  .top-bar #micEnableBtn .mic-dot {
    position: static;
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--mic-idle-bg);
    border: 1px solid var(--mic-idle-border);
    flex-shrink: 0;
    transition: background var(--trans-base), box-shadow var(--trans-base), border-color var(--trans-base);
  }
  /* mic ready (permission granted) */
  .top-bar #micEnableBtn.mic-ready { color: var(--mic-ready); border-color: var(--mic-idle-bg); cursor: default; }
  .top-bar #micEnableBtn.mic-ready .mic-dot {
    background: var(--mic-live-bg); border-color: var(--mic-live-border);
    animation: rec-glow 1.5s ease-in-out infinite;
  }
  /* mic denied */
  .top-bar #micEnableBtn.mic-denied { color: var(--text-secondary); border-color: var(--chrome-border-dim); cursor: default; }
  .top-bar #micEnableBtn.mic-denied .mic-dot { background: var(--chrome-border-dim); border-color: var(--text-secondary); }
  /* mute icon: small inline */
  
  .top-bar #muteBtn .mute-icon { width: 13px; height: 13px; flex-shrink: 0; opacity: 1; }
  /* muted state */
  .top-bar #muteBtn.muted { color: var(--accent-action); border-color: transparent; background: transparent; }
  /* ── Main area: canvas + right panel side by side ─────────────────────── */
  .main-layout {
    display: flex;
    flex-direction: row;
    gap: 0.5rem;
    align-items: stretch;
    flex: 1;
    min-height: 0;
    overflow: hidden;
    /* Horizontal breathing room only — vertical gap dropped so the panel
       cards butt flush against the top-bar and bottom-bar with no black
       strip between them. */
    padding: 0 0.75rem;
  }
  /* ── Bottom bar (pinned footer) ─────────────────────────────────────────
     Sibling of .main-layout inside the body flex-column, so it pins to the
     bottom of the viewport the same way .top-bar pins to the top. Houses
     the audio levels (mute, noise gate, input/house/phones/dry meters) and
     sensor controls (OSC/LED/gesture/mapping/sensors) — both are
     always-on monitoring widgets that shouldn't compete with reorderable
     panels for vertical space. */
  .bottom-bar {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    flex-shrink: 0;
    gap: 0.75rem;
    /* Align the footer content to the panel-card left/right edges —
       main-layout (0.75rem) + .right-panel inner padding (0.4rem). */
    padding: 0.7rem 1.15rem;
    background: var(--bg-app);
  }
  .bottom-bar-left {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 1.1rem;
    flex: 1;
    min-width: 0;
    overflow: hidden;
    padding-right: 0.5rem;
  }
  .bottom-bar-right {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 0.15rem;
    flex-shrink: 0;
  }
  /* Subdued lowercase labels — matches the .device-label convention
     (Urbanist display, low-saturation grey, small). No uppercase, no
     chunky tags — the label should whisper, not announce. */
  .bottom-bar-label {
    font-size: var(--fs-2xs);
    letter-spacing: var(--ls-wide);
    text-transform: lowercase;
    color: var(--text-muted);
    white-space: nowrap;
    font-weight: 400;
  }
  /* Mute: big square tile that fills the footer's content-height. Icon
     stacks on top, label in the middle, "M" keyhint badge on the bottom —
     three tiers vertically centered, so the whole square reads as one
     tidy unit rather than a stretched pill. Fills ~65px inner content
     height (meter column = 48px meter + ~17px label-and-gap). */
  .bottom-bar-mute {
    width: 64px !important;
    height: 64px !important;
    min-width: 0 !important;
    min-height: 0 !important;
    padding: 0.3rem 0.4rem !important;
    font-size: var(--fs-sm) !important;
    color: var(--text-secondary) !important;
    background: transparent !important;
    border: 1px solid var(--border-soft) !important;
    border-radius: var(--r-1);
    flex-direction: column !important;
    justify-content: center !important;
    align-items: center !important;
    gap: var(--sp-2) !important;
    flex-shrink: 0;
  }
  .bottom-bar-mute .session-icon {
    position: static !important;
    width: 18px !important;
    height: 18px !important;
    opacity: 1 !important;
    margin: 0 !important;
  }
  .bottom-bar-mute > span:not(.paint-key-badge) {
    line-height: 1;
  }
  .bottom-bar-mute .paint-key-badge {
    display: inline-block !important;
    position: static !important;
    margin: 0 !important;
    font-size: var(--fs-micro);
  }
  /* Muted state — .active is toggled by ui-sweep.js syncMute() when
     S.isMuted changes. Light the whole tile red so the footer reads
     as "output silenced" at a glance. Covers icon stroke (currentColor),
     label text, border, background, and the M badge outline. */
  .bottom-bar-mute.active {
    color: var(--accent-danger) !important;
    border-color: var(--tint-danger-border) !important;
    background: var(--tint-danger-soft) !important;
  }
  .bottom-bar-mute.active .paint-key-badge {
    color: var(--accent-danger);
    border-color: var(--tint-danger-border);
  }

  /* Gate: vertical bar matching the level-meter columns. Label on top,
     bare narrow canvas below — same shape language as the input/house/
     phones/dry meters so the whole rail reads as one family of monitors. */
  .bottom-bar-gate {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-1);
    flex: 0 0 auto;
    min-width: 0;
  }
  /* The base .main-gate-meter rule sets flex:1 for the horizontal modal
     context — inside a flex-column (our footer), flex:1 grows the canvas
     to fill the column, overriding the height we want. Force a fixed
     box instead so the draw routine gets a tall-narrow rect it can render
     the vertical variant into. */
  .bottom-bar-gate .main-gate-meter {
    flex: 0 0 auto !important;
    /* Matches the level bars beside it (#264) — the gate is a different
       reading (level against a threshold, not level against clip), but a
       12px box in a row of 6px bars read as a different KIND of thing. */
    width: 6px !important;
    height: 48px !important;
    min-width: 0 !important;
    border: none !important;
    border-radius: 1px;
    background: rgba(255, 255, 255, 0.04);
    cursor: default;
  }
  .bottom-bar-gate .as-val { display: none; }

  /* Level meters: drop the framed well look. Each column is a lowercase
     label atop a bare vertical bar — compact, no placeholder cruft, no
     uppercase tags. Empty = silent; no "no signal" label needed. */
  .bottom-bar-meters {
    display: flex !important;
    flex-direction: row !important;
    flex-wrap: nowrap !important;
    align-items: flex-end;
    /* Breathing room between the labelled meter groups — wider than a
       typical row gap because each meter column is narrow and needs
       negative space to read as its own unit. */
    gap: 1.25rem !important;
    flex-shrink: 0;
  }
  .bottom-bar-meters .levels-meter-col {
    flex-direction: column;
    align-items: center;
    gap: var(--sp-1);
    min-width: 0;
  }
  /* .main-meter-dom (on the same element) sets height via !important for
     the old levels panel context; we have to match that here or it wins. */
  /* ── The footer levels rail (#264) ─────────────────────────────────────
     Thin bars, tight gaps — Ableton's proportions. A meter is read by its
     HEIGHT and its colour; width buys nothing and costs footer, and the thin
     version is what made this rail look right. */
  body .bottom-bar-meters .as-vchan { flex: 0 0 6px; gap: var(--seam); }
  body .bottom-bar-meters .as-vchan canvas { border-radius: 0; }
  body .bottom-bar-meters .as-vchan-clip { height: 2px; border-radius: 0; }
  body .bottom-bar-meters .as-multi-meter-wrap { gap: var(--sp-0); }
  .bottom-bar-meters .as-multi-meter-wrap {
    background: transparent !important;
    border: none !important;
    padding: 0 !important;
    margin: 0 !important;
    height: 48px !important;
    min-width: 12px;
    gap: var(--sp-0);
    overflow: hidden;     /* never scroll horizontally in the footer */
  }
  /* "No signal" placeholder would wedge a text blob into a 10px column —
     hide it; empty == silent is already obvious. */
  .bottom-bar-meters .as-multi-meter-wrap:empty::before { content: none; }
  /* Channel bar width — proportioned to the 48px height so the bars
     read as clean vertical strips, not stubby marks. */
  .bottom-bar-meters .as-vchan {
    flex: 0 0 12px;
    gap: 0;
    /* Transparent by default so selected and unselected bars stay the same
       height — the border eats into the canvas, not into the rail. */
    border-bottom: 2px solid transparent;
  }
  /* Selected input channel: an underline under the bar, not a box around it.
     At 12px wide with no label, an outline reads as a stray rectangle floating
     in the rail; a tick on the baseline sits with the other meters. Non-selected
     channels dim so the live one is obvious at a glance. */
  .bottom-bar-meters .as-vchan.is-selected {
    outline: none;
    border-radius: 0;
    border-bottom-color: var(--accent-lock);
  }
  .bottom-bar-meters .as-multi-meter-wrap:has(.is-selected) .as-vchan:not(.is-selected) {
    opacity: 0.45;
  }
  /* Channel-number labels inside the meter ("in", "dry", "L", "R", etc.)
     are redundant with the group label in the footer context — hide them
     to keep the rail visually quiet. */
  .bottom-bar-meters .as-vchan-label { display: none; }
  /* The clip indicator normally sits on top as a 4px dot; in the footer
     it eats into the already-short 22px rail. Hide it unless actively
     clipping — the canvas bar still paints red/bright when hot so we're
     not losing the overload signal. */
  .bottom-bar-meters .as-vchan-clip {
    height: 0;
    padding: 0;
    background: transparent;
  }
  .bottom-bar-meters .as-vchan-clip.clipping {
    height: 2px;
  }
  /* Hide the bottom bar whenever the UI chrome is hidden — matches the
     existing behaviour of the top-bar in these modes. */
  body.electron-fullscreen .bottom-bar { display: none !important; }
  body.mobile-mode .bottom-bar         { display: none !important; }
  .canvas-wrapper {
    position: relative;
    flex: 1;
    min-width: 0;
    min-height: 0;
    background: var(--bg-canvas);
    border: 1px solid var(--surface-4);
    border-radius: var(--r-card);
    overflow: hidden;
    cursor: none;
  }
  .canvas-wrapper:fullscreen,
  .canvas-wrapper:-webkit-full-screen {
    width: 100vw;
    height: 100vh;
    aspect-ratio: unset;
    border: none;
    border-radius: 0;
    background: var(--bg-canvas);
  }
  .canvas-wrapper:fullscreen::backdrop,
  .canvas-wrapper:-webkit-full-screen::backdrop {
    background: var(--bg-canvas);
  }
  /* ── Electron fullscreen: viz-only mode (hides GUI, matches browser behaviour) ── */
  body.electron-fullscreen { padding: 0 !important; gap: 0 !important; }
  body.electron-fullscreen .top-bar       { display: none !important; }
  body.electron-fullscreen .right-panel   { display: none !important; }
  
  body.electron-fullscreen .main-layout   { gap: 0; }
  body.electron-fullscreen .canvas-wrapper {
    width: 100vw;
    height: 100vh;
    aspect-ratio: unset;
    border: none;
    border-radius: 0;
    background: var(--bg-canvas);
  }

  /* ── Projector mode: compact laptop layout + popup mirror ────────────── */
  /* Full viewport for panels; canvas is a small mini-monitor tile. */
  
  /* Projector-mode overrides — keep the 1.15rem horizontal padding so
     the header + footer content stays aligned with the panel-card left
     edge (main-layout 0.75rem + right-panel 0.4rem). Only the vertical
     sizing and wrapping differ from the base rules. */
  
  
  
  /* Panels fill the viewport in a three-column flex layout:
       [left column wrapper] [canvas] [right column wrapper]
     with widths 1fr 2fr 1fr. Each column wrapper is its own flex-column,
     so left and right track their own heights independently — tiles pack
     tightly top-down with no dead space from a taller sibling column.
     Grid would force row heights to the tallest-in-row (the canvas at
     ~468px), leaving empty vertical space in the short tiles next to it.
     Column wrappers are created in events.js → _repartitionProjectorPanels,
     which also re-runs on every panel-reorder arrow press. */
  
  
  /* Center region = canvas on top + its sub-columns below. The center's grow
     factor is the canvas SPAN (2–4 slots, set inline as --canvas-span by
     _arrangeProjectorColumns in events.js), and the 5 - span root columns stay
     at flex 1, so the center always takes exactly span/5 of the rail. At the
     default span of 2:
       [leftCol 1fr] [centerWrap 2fr] [cRightCol 1fr] [rightCol 1fr]
     = five equal-width column slices, canvas over the 2nd and 3rd.
     Because the center splits its width `span` ways, every column is rail/5
     wide at ANY span — widening never reflows panels sideways, it only costs
     vertical space. Root-level columns stay siblings of centerWrap (not nested)
     so they get full viewport height rather than being squeezed below the
     canvas. Narrow tiers below 1160px flatten this and set their own canvas
     proportion, so --canvas-span does not apply there. */
  
  
  
  
  

  

  
  
  
  /* The mini-canvas tile inherits .device, which would wrap the canvas in
     0.4rem of card padding + the card's raised-grey background — leaving a
     visible "frame" around the render. Kill the padding so the canvas goes
     edge-to-edge, and let the tile's own bg be the canvas bg rather than
     the card grey. */
  
  
  
  
  

  /* no hide rule needed — fullscreen overlay already covers other elements */
  canvas {
    display: block;
    width: 100%;
    height: 100%;
  }
  .canvas-wrapper canvas {
    cursor: none;
  }
  /* ── Surface mode "click to re-enter" overlay ────────────────────────────── */
  /* The wash belongs to the STAGE, not to the screen. Alt-lock's whole use is
     "freeze the sphere so I can go and tinker with the rails", so the overlay
     has to sit ABOVE the canvas and BELOW everything that floats over it —
     the two left rails, the pinned rail and the palette, all z-index 4 in
     .main-layout's grid cell (#291). At its old z-index of 10 it greyed and
     swallowed clicks on the very panels the lock exists to reach: the wrapper
     is `position: relative` with `z-index: auto`, so it is NOT a stacking
     context and its children compete with the rails directly. */
  #surfaceLockOverlay {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    background: rgba(0, 0, 0, 0.55);
    cursor: pointer;
    user-select: none;
    transition: background var(--trans-base);
  }
  #surfaceLockOverlay:hover { background: rgba(0, 0, 0, 0.4); }
  .surface-overlay-main {
    color: rgba(255,255,255,0.8);
    font-size: var(--fs-md);
    letter-spacing: var(--ls-wide);
  }
  #surfaceLockOverlay:hover .surface-overlay-main { color: rgba(255,255,255,0.95); }
  .surface-overlay-hint {
    color: rgba(255,255,255,0.35);
    font-size: var(--fs-md);
    letter-spacing: var(--ls-tight);
  }

  /* The master mute's overlay (events.js _syncMuteOverlay): the lock's type,
     framed in the muted glyph's amber, and click-through — the sphere stays
     playable while the output is off. NO WASH (Ek, 2026-09-25: "too dark, I
     still want to be able to play the viz"): the frame and the word carry it,
     and the marks stay at full strength. Under a lock overlay its words move
     to the top, so the two messages never sit on each other. */
  #muteOverlay {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    box-shadow: inset 0 0 0 3px var(--accent-action);
    pointer-events: none;
    user-select: none;
  }
  #muteOverlay .surface-overlay-main { color: var(--accent-action); }
  :has(> #surfaceLockOverlay) > #muteOverlay { justify-content: flex-start; padding-top: 4.5rem; }

  /* The surface-mode entry banner (#surfaceEntryHint) is gone (Ek, 2026-09-12):
     the lock overlay is the one message about the pointer. */

  .hud {
    position: absolute;
    top: calc(18px * var(--hud-scale, 1) + 0.75rem * var(--hud-scale, 1));
    left: 1rem;
    right: 1rem;
    font-size: calc(1.1rem * var(--hud-scale, 1));
    color: var(--text-muted);
    pointer-events: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    line-height: 1;
  }
  .hud-left, .hud-right {
    display: flex;
    align-items: center;
    gap: calc(1rem * var(--hud-scale, 1));
    flex: 1;
  }
  .hud-left { justify-content: flex-start; }
  .hud-right { justify-content: flex-end; }
  .hud-center {
    flex: 0 0 auto;
    text-align: center;
  }  #coordinates { white-space: pre; font-variant-numeric: tabular-nums; }
  /* ── Commit dots — minimalist slot indicator in HUD ────────────────────── */
  .vm-commit-dots {
    display: flex;
    align-items: center;
    gap: calc(3px * var(--hud-scale, 1));
  }
  .vm-commit-dot {
    width: calc(7px * var(--hud-scale, 1));
    height: calc(7px * var(--hud-scale, 1));
    border-radius: 50%;
    flex-shrink: 0;
  }
  /* ── Mobile mode ─────────────────────────────────────────────────────────── */
  body.mobile-mode {
    gap: 0 !important;
    /* Strip all desktop padding so the canvas fills edge-to-edge.
       safe-area-inset-* handles notch / gesture-bar insets on any device. */
    padding-top:    env(safe-area-inset-top,    0px) !important;
    padding-right:  env(safe-area-inset-right,  0px) !important;
    padding-bottom: env(safe-area-inset-bottom, 0px) !important;
    padding-left:   env(safe-area-inset-left,   0px) !important;
  }
  body.mobile-mode .top-bar,
  body.mobile-mode .right-panel { display: none !important; }
  /* THE PHONE IS THE SPHERE (Ek, 2026-09-12: "a really dumb simple version"):
     the gyro steers, a touch is the spacebar, the ⚙ panel is the whole
     settings. The rig view's chrome — bar, rails, palette — is desktop. */
  body.mobile-mode #tcBar, body.mobile-mode #toolRail, body.mobile-mode #propRail,
  body.mobile-mode #tcRail { display: none !important; }
  /* The palette stays as the hand tile alone (Ek: "the palette bar can be
     reduced to just the spacebar, since there's no key assignments"): a
     touch on it plays the hand, like the sphere. No strip, no stickers. */
  body.mobile-mode #paletteDock .tile[data-pal],
  body.mobile-mode #paletteDock .tile-binds { display: none !important; }
  body.mobile-mode .main-layout  { gap: 0; padding: 0; }
  body.mobile-mode .canvas-wrapper {
    border: none; border-radius: 0;
    cursor: default !important;
  }
  body.mobile-mode canvas { cursor: default !important; }

  /* Move HUD to bottom in mobile so it clears the selfie camera (top-right).
     calc() stacks the fixed offset on top of the safe-area inset. */
  body.mobile-mode .hud {
    top: auto;
    bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
    left: 1rem;
    right: 1rem;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.9rem;
  }
  /* In mobile, hide desktop-only HUD items that clutter the bottom bar */
  body.mobile-mode #coordinates,
  body.mobile-mode #altLockIndicator { display: none !important; }

  /* Mobile fullscreen toggle button (bottom-right of HUD row) */
  #mobileFullscreenBtn {
    display: none;
    background: none; border: none;
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: var(--fs-md);
    padding: 0; cursor: pointer;
    pointer-events: all;
    opacity: 0.7;
    line-height: 1;
    flex-shrink: 0;
    margin-left: auto; /* push to right end of the HUD row */
  }
  #mobileFullscreenBtn:active { opacity: 1; color: var(--text-primary); }
  #mobileOverlay {
    position: absolute; inset: 0; z-index: 20;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    gap: 1rem; pointer-events: none;
  }
  #mobileRecalibBtn {
    position: absolute; bottom: calc(3rem + env(safe-area-inset-bottom, 0px)); left: 1rem;
    background: rgba(70,70,70,0.85); border: none;
    color: var(--text-primary); font-family: var(--font-sans);
    font-size: var(--fs-md); padding: 0.4rem 0.7rem;
    border-radius: var(--r-1); cursor: pointer; pointer-events: all;
    display: none; z-index: 21;
  }
  #mobileRecalibBtn:active { color: var(--text-highlight); border-color: var(--text-primary); }

  /* ── Full-screen entry button ────────────────────────────────────────────── */
  #mobileEnterBtn {
    position: absolute; inset: 0; z-index: 22;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    background: transparent; border: none; cursor: pointer;
    pointer-events: all; gap: 1.2rem;
  }
  .enter-ring {
    width: 110px; height: 110px; border-radius: 50%;
    border: 1.5px solid rgba(255,255,255,0.45);
    display: flex; align-items: center; justify-content: center;
    background: var(--surface-4);
    animation: mob-pulse 2.2s ease-in-out infinite;
  }
  .enter-ring svg { opacity: 0.75; }
  .enter-label {
    color: rgba(255,255,255,0.5);
    font-family: var(--font-sans);
    font-size: var(--fs-md); letter-spacing: 0.09em; text-transform: uppercase;
  }
  @keyframes mob-pulse {
    0%,100% { box-shadow: 0 0 0 0 rgba(255,255,255,0.07); border-color: rgba(255,255,255,0.3); }
    50%     { box-shadow: 0 0 26px 8px var(--surface-3); border-color: rgba(255,255,255,0.7); }
  }
  #mobileTapHint {
    color: rgba(255,255,255,0.45);
    font-family: var(--font-sans);
    font-size: var(--fs-readout); letter-spacing: 0.1em; text-align: center; line-height: 1.7;
  }

  #mobileSettingsBtn {
    position: fixed; /* viewport-level — never clipped by canvasWrapper overflow:hidden */
    bottom: calc(2.8rem + env(safe-area-inset-bottom, 0px));
    right: 1rem;
    display: none; /* shown immediately by initMobileMode */
    background: rgba(70,70,70,0.92); border: 1px solid var(--text-dim);
    color: var(--text-primary); font-family: var(--font-sans);
    font-size: var(--fs-xl); padding: 0.45rem 0.65rem;
    border-radius: var(--r-3); cursor: pointer; pointer-events: all;
    z-index: 150; /* above canvas, below modals */
    -webkit-tap-highlight-color: transparent;
  }
  #mobileSettingsBtn:active { color: var(--text-primary); border-color: var(--text-primary); background: rgba(40,40,40,0.95); }

  #mobileSettingsPanel {
    position: fixed; inset: 0; z-index: 199;
    background: rgba(55,55,55,0.98);
    display: none; flex-direction: column;
    align-items: stretch; padding: 1.2rem 1rem calc(1.4rem + env(safe-area-inset-bottom, 0px));
    gap: 0.75rem; overflow-y: auto;
    font-family: var(--font-sans); color: var(--text-primary); font-size: var(--fs-md);
    pointer-events: all;
  }
  #mobileSettingsPanel.open { display: flex; }
  .mob-panel-title {
    font-size: var(--fs-md); letter-spacing: var(--ls-display); text-transform: uppercase;
    color: var(--text-primary); margin-top: 0.4rem;
  }
  .mob-row {
    display: flex; align-items: center; gap: 0.5rem;
    justify-content: space-between;
  }
  .mob-label { color: var(--text-secondary); min-width: 4.5rem; }
  .mob-readout {
    font-size: var(--fs-md); color: var(--text-primary); font-variant-numeric: tabular-nums;
    min-width: 5rem; text-align: right; letter-spacing: var(--ls-normal);
  }
  .mob-sel {
    background: #585858; border: 1px solid var(--text-dim); color: var(--text-primary);
    font-family: var(--font-sans); font-size: var(--fs-md);
    padding: 0.25rem 0.4rem; border-radius: 1px; flex: 1;
  }
  .mob-sign {
    background: #585858; border: 1px solid var(--text-dim); color: var(--text-primary);
    font-family: var(--font-sans); font-size: var(--fs-md);
    padding: 0.25rem 0.5rem; border-radius: 1px; cursor: pointer;
    min-width: 2.5rem; text-align: center;
  }
  .mob-sign.neg { border-color: #a66; color: #f99; }
  .mob-big-btn {
    background: rgba(70,70,70,0.6); border: 1px solid var(--text-dim); color: var(--text-primary);
    font-family: var(--font-sans); font-size: var(--fs-md);
    padding: 0.55rem; border-radius: var(--r-1); cursor: pointer; width: 100%;
    letter-spacing: var(--ls-uppercase);
  }
  .mob-big-btn:active { color: var(--text-primary); border-color: var(--text-primary); }
  .mob-divider { border: none; border-top: 1px solid rgba(0,0,0,0.1); margin: 0.2rem 0; }

  /* ── Device strip (Ableton-style bottom bar) ─────────────────────────────── */
  /* Panels now read as separate cards on the app background — the strip adds
     inter-panel gap and a touch of padding instead of using dividers between
     tiles. Jumble-inspired: no borders between cards, breathing room instead. */
  
  /* Each device block */
  .device {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--chrome-bg-raised);
    border: none;
    border-radius: 6px;
    padding: 0.4rem 0.65rem 0.45rem;
    min-width: 0;
    position: relative;
  }
  .device-label {
    font-family: var(--font-display);
    font-size: var(--fs-readout);
    font-weight: 500;
    letter-spacing: var(--ls-uppercase);
    color: var(--text-secondary);
    text-transform: lowercase;
    margin: -0.4rem -0.65rem 0.5rem;
    padding: 0.4rem 0.65rem 0.35rem;
    background: var(--surface-2);
    border: none;
    border-radius: 6px 6px 0 0;
    flex-shrink: 0;
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 0.35rem;
  }
  /* Make empty columns visible/droppable while a drag is in flight */
  
  /* Canvas-block drag: slim grab handle, hidden until the tile is hovered.
     z-index clears #surfaceLockOverlay (10) — that overlay is inset:0 over the
     same tile with pointer-events auto, so at the handle's old z-index of 5 a
     lost surface lock silently made the whole viz block unmovable: no hover
     reveal, no pointerdown. Anything meant to be grabbable on this tile has to
     sit above 10. */
  
  
  
  /* Canvas-block resize: full-height grab strip on the tile's right edge.
     Faintly visible AT REST, unlike the move pill — resize was a shift-modifier
     on that pill and nobody found it, because a modifier gesture advertises
     itself nowhere. The edge is where a resize is looked for, so put it there
     and let the cursor say what it does. */
  
  
  
  
  /* Target-span preview while dragging the canvas block */
  

  .device-label::before {
    content: '▾';
    font-size: var(--fs-2xs);
    transition: transform var(--trans-base) ease;
    display: inline-block;
  }
  .device.collapsed .device-label::before {
    transform: rotate(-90deg);
  }
  .device.collapsed .device-label {
    margin-bottom: -0.45rem;
    border-radius: 6px;
    color: var(--text-muted);
  }
  .device.collapsed {
    flex: 0 0 auto !important;
    min-height: 0 !important;
  }
  .device-body {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    flex: 1;
    min-height: 0;
    justify-content: flex-start;
    overflow: hidden;
  }
  .device.collapsed .device-body {
    display: none !important;
  }

  /* ── Device: session panel ─────────────────────────────────────────────── */
  /* Session body follows the commit-body / play-body / meters-body-v2
     convention: no padding, no gap. Each .seq-section inside owns its own
     padding (7px 6px) so vertical rhythm matches the commit panel exactly. */
  .session-body {
    flex-direction: column !important;
    padding: 0 !important;
    gap: 0 !important;
  }

  /* Sized to match the "big" buttons in the cursor (.mu-btn) and
     commits panels — same min-height, padding, and font-size. */
  
  /* Share a row evenly when two session buttons sit inside a seq-section--row
     (mirrors the .seq-section--row .mu-btn rule for big buttons). */
  .seq-section--row .mu-btn {
    flex: 1 1 0;
    min-width: 0;
    width: auto;
  }
  /* Icon slot — matches the big .mu-btn layout (absolute left) so the
     label stays visually centered even when a key badge is pinned right.
     Icon color inherits from the button (currentColor) so it follows
     hover/active/flash state transitions automatically. */
  .session-icon {
    position: absolute;
    left: 0.7rem;
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    opacity: 0.7;
    transition: opacity var(--trans-base);
  }
  .mu-btn:hover .session-icon,
  .mu-btn.active .session-icon,
  .mu-btn.flashing .session-icon,
  .mu-btn.sweep-flash .session-icon { opacity: 1; }
  .mu-btn:hover {
    color: var(--text-bright);
    border-color: var(--border-emphasis);
    background: var(--surface-4);
  }
/* ── The faces (round twenty-one) ─────────────────────────────────────────
   `on`      — ember border, NO fill. The control is engaged.
   `on--held`— adds fill and glow. THE HELD TEST, and it is the whole reason
               the variant exists: CAN YOU AFFORD TO MISS THIS STATE DURING A
               SHOW? A border you have to look for is enough for "this is
               selected"; it is not enough for "the instrument is silent". Mute
               alone qualifies today. Do not add a third without answering the
               question out loud. RECORD-ARM DOES NOT QUALIFY: you just armed
               it, so you already know. Mute is different because the thing that
               tells you it is on is silence, and silence is also what a broken
               cable sounds like.
   `flash`   — ONE transient modifier whose colour comes from a variable: ember
               for an action on that control, sage for a sweep.

   The state classes the JS already toggles (.active, .muted, .flashing,
   .sweep-flash) share these rules rather than being renamed, so nothing in
   erase.js / events.js / midi.js had to change. Every value is the one it was:
   this is a rename of behaviour, not a change to it, and the diff is 0.

   NOTE THE ASYMMETRY, which is NOT collapsed here because collapsing it would
   move a colour: the ember flash borders at --tint-action-strong (0.55 alpha),
   the sage flash at --tint-sweep-border (0.36), and there is no
   --tint-sweep-strong. One shared token would change one of the two. Flagged
   rather than decided. */
.mu-btn.on,
.mu-btn.active {
  color: var(--accent-action); border-color: var(--tint-action-border);
}
.mu-btn.muted {
  color: var(--accent-action); border-color: var(--accent-action);
  background: var(--tint-action-soft); box-shadow: 0 0 8px var(--tint-action-medium);
}
.mu-btn.flash,
.mu-btn.flashing,
.mu-btn.sweep-flash {
  color: var(--mu-flash); border-color: var(--mu-flash-border); background: var(--mu-flash-soft);
}
.mu-btn.flash,
.mu-btn.flashing {
  --mu-flash: var(--accent-action); --mu-flash-border: var(--tint-action-strong);
  --mu-flash-soft: var(--tint-action-soft);
}
.mu-btn.sweep-flash {
  --mu-flash: var(--accent-sweep); --mu-flash-border: var(--tint-sweep-border);
  --mu-flash-soft: var(--tint-sweep-soft);
}
  .mu-btn--danger:hover {
    color: var(--accent-danger);
    border-color: var(--tint-danger-border);
    background: var(--tint-danger-soft);
  }
  .mu-btn.flashing {
    color: var(--accent-action);
    border-color: var(--tint-action-strong);
    background: var(--tint-action-soft);
  }
  .mu-btn.sweep-flash {
    color: var(--accent-sweep);
    border-color: var(--tint-sweep-border);
    background: var(--tint-sweep-soft);
  }

  /* ── Erase brush panel ──────────────────────────────────────────────── */
  .erase-body {
    flex-direction: column !important;
    padding: 0 !important;
    gap: 0 !important;
  }
  .mu-btn.erase-active {
    color: var(--accent-danger);
    border-color: var(--tint-danger-border);
    background: var(--tint-danger-soft);
  }
  .erase-status {
    font-size: var(--fs-readout);
    color: var(--text-muted);
    letter-spacing: var(--ls-wider);
    padding: 0.35rem 0.1rem 0;
    text-align: center;
  }
  .mu-btn .paint-key-badge {
    position: absolute;
    right: 0.7rem;
    margin-left: 0;
    vertical-align: baseline;
  }

  /* ── Device: live input ─────────────────────────────────────────────────── */
  
  /* device body is row: mic button left, fader+meter right */
  
  /* mic button fills height, fixed width */
  
  /* right side: narrow column, canvas fills full height */
  
  #micEnableBtn {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    background: var(--bg-app);
    border: none;
    border-radius: var(--r-1);
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: var(--fs-md);
    padding: 0.3rem 0.4rem 0.25rem;
    cursor: pointer;
    letter-spacing: var(--ls-wide);
    transition: border-color var(--trans-base), color var(--trans-base), background var(--trans-base);
    width: auto;
    position: relative;
  }
  #micEnableBtn:hover { border-color: transparent; color: var(--text-primary); background: var(--chrome-bg-hover); }
  /* mic SVG icon */
  
  /* red status dot — top-right corner */
  #micEnableBtn .mic-dot {
    position: absolute;
    top: 5px;
    right: 6px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--mic-idle-bg);
    border: 1px solid var(--mic-idle-border);
    transition: background var(--trans-base), box-shadow var(--trans-base), border-color var(--trans-base);
  }
  #micEnableBtn:hover .mic-dot { background: var(--mic-hover-bg); border-color: var(--mic-hover-border); }
  #micEnableBtn.mic-ready .mic-dot {
    background: var(--mic-live-bg);
    border-color: var(--mic-live-border);
    animation: rec-glow 1.5s ease-in-out infinite;
  }
  #micEnableBtn.mic-denied .mic-dot { background: var(--chrome-border-dim); border-color: var(--text-secondary); }
  #micEnableBtn.mic-ready { color: var(--mic-ready); border-color: var(--mic-idle-bg); cursor: default; }
  #micEnableBtn.mic-denied { color: var(--text-secondary); cursor: default; }
  @keyframes rec-glow {
    0%, 100% { filter: drop-shadow(0 0 3px rgba(204, 34, 34, 0.53)); }
    50%       { filter: drop-shadow(0 0 6px rgba(238, 51, 51, 0.8)); }
  }
  
  
  
  
   /* clip now shown inside fader canvas */

  /* ── Device: search params ───────────────────────────────────────────────── */
  /* ── Search device — uniform viz cards ──────────────────────────────────── */
  .search-body {
    display: flex !important;
    flex-direction: column;
    align-items: stretch;
    padding: 0 !important;
    gap: 0;
  }

  #loadIndicator { font-size: var(--fs-md); color: var(--text-muted); letter-spacing: var(--ls-normal); }
  #loadIndicator.warn { color: var(--accent-warn); }

  /* ── Perf monitor (P key) ──────────────────────────────────────────────── */
  #perfMonitor {
    position: absolute;
    top: calc(18px + 3rem);
    left: 1rem;
    font-family: var(--font-mono);
    font-size: var(--fs-base);
    color: var(--text-primary);
    pointer-events: none;
    background: rgba(0,0,0,0.65);
    padding: 0.5rem 0.65rem;
    border-radius: 4px;
    min-width: 200px;
  }
  .pm-head {
    color: var(--text-dimmer);
    font-size: var(--fs-body);
    letter-spacing: var(--ls-uppercase);
    text-transform: uppercase;
    margin-bottom: 0.45rem;
  }
  .pm-key { color: var(--chrome-border-dim); }
  .pm-row {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin-bottom: 0.3rem;
  }
  .pm-label {
    width: 2.6rem;
    color: var(--text-dim);
    font-size: var(--fs-body);
    flex-shrink: 0;
  }
  .pm-track {
    position: relative;
    width: 80px;
    height: 3px;
    background: rgba(255,255,255,0.07);
    border-radius: var(--r-1);
    overflow: visible;
    flex-shrink: 0;
  }
  .pm-fill {
    display: block;
    height: 100%;
    width: 0%;
    border-radius: var(--r-1);
    background: var(--accent-lock);
    transition: width 0.2s ease, background-color 0.2s ease;
  }
  .pm-num {
    font-size: var(--fs-xs);
    color: var(--accent-lock);
    min-width: 4rem;
  }
  .pm-num-peak {
    min-width: 3.2rem;
    color: var(--accent-action);
    font-size: var(--fs-2xs);
    opacity: 0.8;
  }
  .pm-peak {
    position: absolute;
    top: -1px;
    width: 2px;
    height: 5px;
    background: var(--accent-action);
    border-radius: 1px;
    left: 0%;
    transition: left var(--trans-base) ease;
  }
  .pm-num-edit {
    background: none;
    border: none;
    border-bottom: 1px solid rgba(122,188,188,0.2);
    font: inherit;
    color: var(--accent-lock);
    padding: 0;
    width: 3.5rem;
    cursor: text;
    outline: none;
    /* #perfMonitor is pointer-events:none so it never swallows a canvas
       click, and that inherits to every child.  The handful of controls
       that ARE meant to be interactive have to opt back in individually —
       without this the numbox shows a text cursor and cannot be focused. */
    pointer-events: auto;
  }
  .pm-num-edit:focus {
    border-bottom-color: var(--accent-lock);
  }
  .pm-row-clickable { cursor: pointer; pointer-events: auto; }
  .pm-row-clickable:hover .pm-label { color: var(--text-secondary); }
  .pm-info {
    color: var(--text-faint);
    font-size: var(--fs-body);
    margin-top: 0.4rem;
    padding-top: 0.3rem;
    border-top: 1px solid var(--surface-2);
  }
  .pm-warn {
    color: var(--accent-danger);
    font-size: var(--fs-xs);
    margin-top: 0.2rem;
  }

  /* ── Right panel: search + grain stacked beside viewer ──────── */
  .right-panel {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    width: 22.7rem;
    flex-shrink: 0;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 0.4rem;
  }
  .right-panel .device {
    width: 100%;
    min-width: 0;
    flex-shrink: 0;
    border: none;
    border-radius: 6px;
    background: var(--chrome-bg-raised);
  }
  .right-panel .device--search  { flex: none; }
  .right-panel .device--grain   { flex: none; min-width: 0; }  /* ── Device: grain controls ─────────────────────────────────────────────── */
  .device--grain { flex: 2; min-width: clamp(480px, 40vw, 700px); }
  .grain-body {
    flex-direction: column !important;
    gap: 0;
    overflow-y: auto;
    overflow-x: hidden;
  }
   /* flatten cols — rows become direct children */
  /* Grain-body seq-sections skip horizontal padding — the .device card
     already owns the inner gutter, and grain-rows want to span the full
     card width so the long sliders don't get clipped. Vertical padding
     shrinks too since grain-rows each carry their own 0.4rem of vertical
     space — adding 7px on top makes groups feel too airy in what is
     otherwise a dense parameter rack. */
  .grain-body .seq-section {
    padding: var(--sp-2) 0;
  }
  .grain-body .seq-section + .seq-section:has(> .seq-section-label) {
    margin-top: 10px;
    padding-top: 6px;
  }
  /* Align the subheading with the grain-label column — the grain-row
     indents its label by ~68px (the reserved label column), but the
     subheading should sit at the actual left edge of the panel's content
     area so it reads as a group header above the rows, not nested under
     them. Subtle bottom margin for breathing room before the first row. */
  .grain-body .seq-section-label {
    margin-bottom: 4px;
  }
  .grain-row {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 0.65rem;
    padding: 0.4rem 0;
    border-bottom: none;
    flex-shrink: 0;
  }
  .grain-row:last-child { border-bottom: none; }
  /* Not available: the control dims, the label does not — the same rule as
     .set-row--off in settings-gui.css, in the kit this row belongs to.
     The SLIDER is not listed: `html body .grain-slider:disabled` already dims
     it to 0.45 and outranks this (0,2,2 against 0,2,0), and the one writer sets
     `.disabled` and this class together, so a `.grain-row--off .grain-slider`
     clause could never win. The numbox is a readonly text input, not a disabled
     one, so it has no such rule and needs this. */
  .grain-row--off .grain-numbox { opacity: 0.4; pointer-events: none; }
  /* Visual group break — adds top spacing without a label */
    .grain-label {
    font-family: var(--font-sans);
    font-size: var(--fs-sm);
    color: #5e5e5e;
    letter-spacing: var(--ls-normal);
    text-transform: none;
    width: 68px;
    flex-shrink: 0;
    text-align: right;
  }
  /* Slider track */
  .grain-slider {
    -webkit-appearance: none;
    appearance: none;
    flex: 1;
    height: 2px;
    background: var(--surface-4);
    border-radius: 1px;
    outline: none;
    cursor: ew-resize;
    min-width: 0;
  }
  /* Ableton-style thumb: thin vertical bar */
  .grain-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 2px;
    height: 12px;
    border-radius: 0;
    background: #606060;
    cursor: ew-resize;
    border: none;
    transition: background var(--trans-quick), height var(--trans-quick);
  }
  .grain-slider:hover::-webkit-slider-thumb { background: var(--text-hover); height: 14px; }
  /* Log-mapped sliders: very subtle tick marks — barely visible */
  .grain-slider--log {
    background: linear-gradient(90deg,
      var(--surface-3) 0%,
      var(--surface-3) calc(8.3%  - 1px), var(--surface-5) 8.3%,  var(--surface-3) calc(8.3%  + 1px),
      var(--surface-3) calc(29.8% - 1px), var(--surface-5) 29.8%, var(--surface-3) calc(29.8% + 1px),
      var(--surface-3) calc(45.1% - 1px), var(--surface-5) 45.1%, var(--surface-3) calc(45.1% + 1px),
      var(--surface-3) calc(68.8% - 1px), var(--surface-5) 68.8%, var(--surface-3) calc(68.8% + 1px),
      var(--surface-3) calc(77.1% - 1px), var(--surface-5) 77.1%, var(--surface-3) calc(77.1% + 1px),
      var(--surface-3) calc(85.4% - 1px), var(--surface-5) 85.4%, var(--surface-3) calc(85.4% + 1px),
      var(--surface-3) 100%
    );
  }
  /* Numbox */
  .grain-numbox {
    font-family: var(--font-sans);
    font-size: var(--fs-sm);
    color: var(--text-tertiary);
    background: transparent;
    border: none;
    border-radius: 0;
    padding: 0 var(--sp-0) 0 var(--sp-3);
    width: 62px;
    text-align: right;
    cursor: text;
    flex-shrink: 0;
    line-height: 1.6;
    font-variant-numeric: tabular-nums;
    transition: color var(--trans-base);
    outline: none;
    appearance: textfield;
  }
  .grain-numbox::-webkit-outer-spin-button,
  .grain-numbox::-webkit-inner-spin-button { -webkit-appearance: none; }
  .grain-numbox:hover { color: var(--text-emphasis); }
  .grain-numbox:focus {
    color: var(--text-primary);
    background: var(--surface-0);
  }
  /* Slot-count dropdown inside grain rows */
  .grain-select {
    font-family: var(--font-sans);
    font-size: var(--fs-xs);
    color: var(--text-secondary);
    background: var(--surface-2);
    border: 1px solid var(--surface-5);
    border-radius: var(--r-1);
    padding: 0.25rem var(--sp-2);
    height: 1.5rem;
    flex: 1 1 auto;
    min-width: 0;
    outline: none;
    cursor: pointer;
  }
  .grain-select:hover { border-color: var(--border-hover); color: var(--text-primary); }
  .grain-select:focus { border-color: var(--border-focus); color: var(--text-emphasis); }

  /* ── Parameter lock buttons ──────────────────────────────────────────── */  /* Subtle locked row tint */
  /* Fade row: the label doubles as the %/ms unit toggle.  Marked with a dotted
     underline so it reads as interactive without adding a control that would
     eat the slider's travel. */
  .grain-label-toggle {
    cursor: pointer;
    text-decoration: underline dotted;
    text-underline-offset: 2px;
  }
  .grain-label-toggle:hover { color: var(--text-primary); }

  /* Octave shortcut buttons */
  /* Matches .grain-seg-btn — consistent pill treatment with the other
     segmented toggles. The three buttons behave as a connected pill group
     via the parent .grain-seg gap. */
  /* They own a dedicated row rather than sharing the pitch slider's row:
     .grain-seg is flex:0 0 auto and .oct-btn is flex-shrink:0, so the
     slider (flex:1; min-width:0) was the only element that could absorb a
     narrowing panel and collapsed to zero width. Unlabeled and pulled up
     tight so it still reads as part of the pitch row above; the numbox
     gutter comes from the .grain-row:not(:has(.grain-slider)) rule below. */
  .grain-row--oct {
    padding-top: 0;
    margin-top: -0.2rem;
  }
  /* .oct-btn DELETED (round seventeen). Measured byte-identical to an unselected
   .grain-seg-btn — rgb(80,80,80) on rgba(255,240,224,0.03) — so the three
   octave buttons ARE segments and always were. Pure rename: no colour moved. */
  /* Segmented buttons (dir / curve) */
  /* Segmented toggles sit as a compact group pushed to the right.
     `margin-left: auto` right-aligns the group; on rows that also have a
     slider the slider's flex:1 already fills the gap and the auto-margin
     is a no-op. On toggle-only rows (see rule below) we also reserve an
     empty numbox-column on the right so the seg's right edge lines up
     with where sliders end — creates a consistent right-edge column
     across slider rows and toggle rows. */
  .grain-seg {
    display: flex;
    flex: 0 0 auto;
    gap: var(--sp-0);
    margin-left: auto;
    min-width: 0;
  }
  /* Rows with no slider: reserve numbox-column width (62px) + row-gap
     (0.65rem) on the right of the seg, so the seg's right edge aligns
     with the right edge of sliders on neighbouring rows. This creates
     a visual numbox column that runs clean top-to-bottom even when a
     given row doesn't have a numeric readout. */
  .grain-row:not(:has(.grain-slider)) .grain-seg {
    margin-right: calc(62px + 0.65rem);
  }
  .grain-seg-btn {
    flex: 0 0 auto;
    /* 2rem, not the 2.4rem this block used to say: a second bare
       `.grain-seg-btn { min-width: 2rem }` sat ~3900 lines below and won on
       position alone. The two rules disagreed on this ONE property; every other
       declaration here was already the live one. Merged, loser deleted
       (round eleven). */
    min-width: 2rem;
    font-family: var(--font-sans);
    font-size: var(--fs-body);
    line-height: 1;
    /* --text-faint, not #505050 (round twenty-one). The literal was a cool grey
       in a warm interface — the pre-August palette — and it was the UNSELECTED
       state of the app's most-used control, worn by 29 elements. The token
       already means quiet/unavailable and is warm. It reads slightly lighter:
       2.45:1 against the panel ground becomes 2.72:1. */
    color: var(--text-faint);
    background: var(--surface-0);
    border: none;
    border-radius: var(--r-1);
    padding: 0.25rem 0.5rem;
    height: 1.5rem;
    cursor: pointer;
    text-align: center;
    transition: color var(--trans-quick), background var(--trans-quick);
    white-space: nowrap;
  }
  .grain-seg-btn:hover { color: var(--text-hover); background: var(--surface-3); }
  .grain-seg-btn.active { color: var(--text-emphasis); background: var(--surface-5); }
  /* auto dry monitor, ducked for a granular take (#245): the setting is still
     auto, the sound is off — dim the pill rather than move it. */
  .grain-seg.ducked .grain-seg-btn.active { opacity: 0.45; }

  /* ── Device: spacer ─────────────────────────────────────────────────────── */
  

  /* ── Device: output ──────────────────────────────────────────────────────── */
  
  
  /* mute button — mirrors mic button exactly */
  #muteBtn {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    background: var(--bg-app);
    border: none;
    border-radius: var(--r-1);
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: var(--fs-md);
    padding: 0.3rem 0.4rem 0.25rem;
    cursor: pointer;
    letter-spacing: var(--ls-wide);
    transition: border-color var(--trans-base), color var(--trans-base), background var(--trans-base);
    position: relative;
    width: auto;
    min-width: 0;
  }
  #muteBtn:hover { border-color: transparent; color: var(--text-primary); background: var(--chrome-bg-hover); }
  #muteBtn .mute-icon {
    width: 26px; height: 26px; flex-shrink: 0; opacity: 0.4;
    transition: opacity var(--trans-base), visibility 0s;
  }
  #muteBtn:hover .mute-icon { opacity: 0.65; }
  #muteBtn.muted .mute-icon-off { opacity: 0.9; }
  #muteBtn.muted {
    background: #2a1e0a;
    border-color: #7a5a1a;
    color: var(--accent-action);
  }
  #muteBtn.muted .mute-icon { opacity: 0.9; }
  /* show/hide speaker icons based on mute state */
  #muteBtn .mute-icon-on  { display: block; }
  #muteBtn .mute-icon-off { display: none; }
  #muteBtn.muted .mute-icon-on  { display: none; }
  #muteBtn.muted .mute-icon-off { display: block; }
  
  
  /* output fader+meter column — mirrors liveinput-body */
  
  /* Multi-channel output meter strip — row of fixed-width bar slots.
  .meter-row, .meter-wrap { display: none; }

  /* ── Improv mode device (right panel) — LEGACY, kept for compat ────────── */
  

  /* ── Unified commit panel ──────────────────────────────────────────────── */
  .right-panel .device--commit {
    flex: 0 0 auto;
    width: 100%;
  }
  .commit-body {
    flex-direction: column !important;
    padding: 0 !important;
    gap: 0 !important;
  }

  /* ── Trigger panel ─────────────────────────────────────────────────────
     Same stacking rules as the commit panel — the !important matches the
     .session-body / .commit-body convention noted below, not a preference. */
  .right-panel .device--trigger {
    flex: 0 0 auto;
    width: 100%;
  }
  .trigger-body {
    flex-direction: column !important;
    padding: 0 !important;
    gap: 0 !important;
  }
  /* Dimmed rows fade rather than jump — the tool switch is a mode change and
     should read as one, not as the panel redrawing. */
  #triggerArmedSection, #triggerMuteSection, #triggerParamsSection, #trigReleaseRow {
    transition: opacity 120ms ease;
  }
  /* Status line under the armed count. Deliberately NOT .erase-status, which is
     12.3px and centred — sized for a panel where it is the only caption. Here it
     sits above seven labelled rows, and at that size it became the loudest thing
     in the panel. Below the 10.8px row labels and left-aligned to their column
     so it reads as a footnote to the count, not as a heading. */
  .trigger-hint {
    font-size: var(--fs-2xs);
    color: var(--text-dimmer);
    text-align: left;
    padding: var(--sp-0) 0 0 0.7rem;
    line-height: 1.3;
  }

  /* Unified slot bank — canvas + count label */
  .commit-section--bank {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-0);
  }
  .commit-slots-canvas {
    width: 100%;
    height: 36px;                /* single row; JS bumps to 68px when >8 slots */
    border-radius: var(--r-2);
  }
  .commit-count-label {
    font-family: var(--font-sans);
    font-size: var(--fs-body);
    color: var(--text-dim);
    min-width: 14px;
    text-align: center;
  }

  /* Commit action buttons — icon swap by mode */
  /* Default: cloud mode — show cloud icon, hide loop icon */
  .commit-action-btn .commit-icon--loop { display: none; }
  .commit-action-btn .commit-icon--cloud { display: block; }
  /* Loop mode: swap */
  .commit-action-btn.commit-mode--loop .commit-icon--cloud { display: none; }
  .commit-action-btn.commit-mode--loop .commit-icon--loop { display: block; }

  /* Resting tint — cloud mode: light blue */
  .commit-action-btn:not(.commit-mode--loop) {
    color: #7eb8e0;
    border-color: rgba(126, 184, 224, 0.35);
  }
  /* Resting tint — loop mode: pink */
  .commit-action-btn.commit-mode--loop {
    color: var(--status-accent);
    border-color: rgba(255, 107, 157, 0.35);
  }

  /* LEGACY — kept for compat */
  
  

  /* Transport — 2 performance + 1 manage in a row */
  
  /* Color accents — seed uses sky-blue for plant */
  

  /* ── Levels device (right panel) ────────────────────────────────────────── */
  
  .levels-meters-row {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;            /* stack input / house when panel is narrow */
    justify-content: flex-start;
    gap: 0.5rem;
  }
  .levels-meter-col {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-0);
    flex: 0 0 auto;
  }
  /* DOM meter bars — override modal padding/margin for tighter main-window fit */
  .main-meter-dom {
    margin: 0 !important;
    padding: 0.15rem 0.3rem !important;
    height: clamp(70px, 7vw, 110px) !important;
    width: fit-content !important;   /* size to channel count — no trailing space */
    border-radius: var(--r-1);
    overflow: visible;
  }
  /* Main-UI noise gate meter — full width in the levels panel */
  
  
  .main-gate-meter {
    flex: 1;
    min-width: 0;
    height: 28px;
    cursor: default;   /* read-only — drag threshold in audio settings */
  }
  .main-gate-val {
    flex-shrink: 0;
    min-width: 2.5rem;
    text-align: right;
  }

  /* Gate meter inside the audio device panel — it sits in a normal
     .grain-row where the slider used to be, so it inherits the row's
     label + numbox columns for free. Shares the .as-gate-meter base rule
     with the modal canvas; these overrides only shrink it to panel scale.
     Descendant selector because .as-gate-meter is declared later in this
     file and would otherwise win the height on equal specificity. */
  .audio-body .ap-gate-meter {
    height: 22px;
    border-color: var(--surface-4);
    cursor: ew-resize;
  }

  /* ── Play device — scan toggle button ────────────────────────────────────── */
  .right-panel .device--play {
    flex: 0 0 auto;
    width: 100%;
  }
  .play-body {
    display: flex !important;
    flex-direction: column;
    align-items: stretch;
    padding: 0 !important;
    /* !important matches the .session-body / .commit-body convention —
       without it a broader `.device-body` gap rule wins on specificity and
       the cursor panel ends up with wider vertical rhythm than
       session/commit (each .seq-section already owns 7px 6px padding, so
       any additional gap here double-spaces the buttons). */
    gap: 0 !important;
  }
  /* Baseline matches .mu-btn exactly — same surface, same border,
     same text color — so every big button in the app (undo, sweep, trace,
     scan, morph, drop cloud, release, clear all, etc.) has a single
     consistent resting appearance. State-specific overrides below add the
     accent colors (teal = active toggle, orange = scan off / morph on,
     blue/pink = commit cloud/loop mode, red = danger, etc.) — so each
     accent signals "this button is in a semantic state" rather than
     being the default look. */

/* ══ .mu-btn — the instrument's button (round fourteen) ══════════════════════
   Three sizes and a face set, replacing three class names that were one button:
   play-mute-btn (10) · paint-indicator-btn (4) · session-btn (7). They were
   identical on every property; the only measured difference was session-btn
   reading 38.59 against 38.40, which is rounding.

   38, NOT 38.4. The trio never chose 38.4 — it fell out of `min-height: 2.4rem`
   plus 0.55rem of padding plus a 15.68px line. It was arithmetic. 38 is the
   number, and the 0.4px it costs on 21 elements is the whole expected diff of
   the conversion.

   THE SET IS CLOSED AT TWO SIZES PLUS THE BARE ONE. --md (32px) was defined for
   tc-icon and as-btn and was deleted when both turned out not to want it: all
   three .as-btn are inside a hosted dialog and already wear the settings kit,
   and tc-icon is a bare 32px glyph at r4/13.33 that both --md and --bare moved
   for nothing. A size with no users is what the closed-kit rule forbids — the
   same reason the badge binding-state went. The segmented control is not a
   button either (its selection is neutral, not ember). See
   docs/INSTRUMENT-GUI.md § 2. */
.mu-btn {
  display: flex; align-items: center; justify-content: center; position: relative;
  width: 100%; height: 24px; padding: 0 0.7rem;
  font-family: var(--font-sans); font-size: var(--fs-lg);
  letter-spacing: var(--ls-wider); white-space: nowrap; text-align: center;
  color: var(--text-tertiary); background: var(--surface-1);
  border: 1px solid var(--surface-5); border-radius: 4px; cursor: pointer;
  transition: color var(--trans-base), border-color var(--trans-base),
              background var(--trans-base), box-shadow var(--trans-base);
}
.mu-btn--lg { height: 38px; }

/* ── Faces ────────────────────────────────────────────────────────────────
   Every value is a token. `default` is the block above; `on`, `danger` and
   `disabled` are here. `off` is NOT written: nothing in this trio has an off
   state, and inventing a face with no instance is what the badge binding-state
   was deleted for in round thirteen. See the report for the two face questions
   this raised. */
.mu-btn:hover {
  color: var(--text-bright); border-color: var(--border-emphasis); background: var(--surface-4);
}
/* `.on` is defined with the other faces above — ember border, NO fill. Round
   fourteen's version carried a fill, which is `on--held`'s job. */
.mu-btn--danger, .mu-btn.danger {
  color: var(--accent-danger); border-color: var(--tint-danger-border); background: var(--tint-danger-soft);
}
.mu-btn:disabled, .mu-btn[aria-disabled="true"] {
  color: var(--text-faint); border-color: var(--border-subtle); background: none; cursor: default;
}

  
  .mu-btn:hover {
    color: var(--text-bright);
    border-color: var(--border-emphasis);
    background: var(--surface-4);
  }
  .play-mute-icon {
    position: absolute;
    left: 0.7rem;
    width: 18px;
    height: 18px;
    flex-shrink: 0;
  }
  /* Default (unmuted): show speaker-on icon, hide muted icon */
  .play-mute-icon--on  { display: inline-block; }
  .play-mute-icon--off { display: none; }
  .play-mute-label {
    text-align: center;
  }
  /* Muted state: amber, show X icon */
  .mu-btn.muted {
    color: var(--accent-action);
    border-color: var(--accent-action);
    background: var(--tint-action-soft);
    box-shadow: 0 0 8px var(--tint-action-medium);
  }
  .mu-btn.muted .play-mute-icon--on  { display: none; }
  .mu-btn.muted .play-mute-icon--off { display: inline-block; }
  .mu-btn.muted .seq-record-key {
    color: rgba(232,160,48,0.5);
    border-color: rgba(232,160,48,0.25);
  }

  /* ── Two-column row for compact button pairs ── */
  .seq-section--row {
    display: flex;
    gap: var(--sp-2);
  }
  .seq-section--row .mu-btn {
    flex: 1;
    min-width: 0;
  }

  /* ── Scan button — white-ish when on, orange when off ── */
  #scanBtn .play-mute-icon--on  { display: none; }
  #scanBtn .play-mute-icon--off { display: inline-block; }
  #scanBtn:not(.active) {
    color: var(--accent-action);
    border-color: var(--accent-action);
    background: var(--tint-action-soft);
    box-shadow: 0 0 8px var(--tint-action-medium);
  }
  #scanBtn:not(.active) .seq-record-key {
    color: rgba(232,160,48,0.5);
    border-color: rgba(232,160,48,0.25);
  }
  #scanBtn.active {
    color: #c8dce8;
    border-color: rgba(200, 220, 232, 0.5);
    background: rgba(200, 220, 232, 0.08);
    box-shadow: 0 0 8px rgba(200, 220, 232, 0.12);
  }
  #scanBtn.active .play-mute-icon--on  { display: inline-block; }
  #scanBtn.active .play-mute-icon--off { display: none; }
  #scanBtn.active .seq-record-key {
    color: rgba(200, 220, 232, 0.4);
    border-color: rgba(200, 220, 232, 0.2);
  }

  /* ── Triggers on/off — deliberately identical to #scanBtn ──────────────
     Same pale-on / amber-off pair, because it IS the same idea one panel
     over: a global mute that silences a voice without touching what was
     recorded. Different colours here would imply a different kind of
     control. Spacing to the record button above comes from the section
     stack, not from a margin here — same as the cursor panel. */

  /* ── Record/paint indicator button ─────────────────────────────────────── */
  .paint-indicator-btn {
    cursor: default;
    user-select: none;
    pointer-events: auto;
    position: relative; /* anchor for absolutely-centred label */
  }
  /* label: truly centred across full button width regardless of badge widths */
  .paint-indicator-label {
    position: absolute;
    left: 0;
    right: 0;
    text-align: center;
    pointer-events: none;
  }
  /* badge group — pushed to the far right via margin-left: auto */
  .paint-key-badges {
    position: absolute;
    right: 0.7rem;
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    flex-shrink: 0;
  }
  /* key badges — exact match to .seq-record-key */
  .paint-key-badge {
    flex-shrink: 0;
    font-family: inherit;
    font-size: var(--fs-2xs);
    color: var(--text-dimmer);
    border: 1px solid var(--surface-3);
    border-radius: var(--r-2);
    padding: var(--sp-1) var(--sp-3);
    line-height: 1.2;
    transition: color var(--trans-base), border-color var(--trans-base);
  }
  /* painting state — per-button colours */

  /* trace (spacebar/click) — warm red */
  #paintIndicatorBtn.painting {
    color: #e05555;
    border-color: rgba(224, 85, 85, 0.55);
    background: rgba(224, 85, 85, 0.10);
    box-shadow: 0 0 10px rgba(224, 85, 85, 0.20);
  }
  #paintIndicatorBtn.painting .paint-key-badge {
    color: rgba(224, 85, 85, 0.5);
    border-color: rgba(224, 85, 85, 0.25);
  }
  #paintIndicatorBtn.painting:active {
    background: rgba(224, 85, 85, 0.10);
    box-shadow: 0 0 10px rgba(224, 85, 85, 0.20);
  }

  /* Trigger record button — same red record state as trace. Rules are
     id-scoped rather than on .paint-indicator-btn because each button that uses
     the class has its own accent (trace red, commit blue/pink). */
  #trigRecordBtn.painting {
    color: #e05555;
    border-color: rgba(224, 85, 85, 0.55);
    background: rgba(224, 85, 85, 0.10);
    box-shadow: 0 0 10px rgba(224, 85, 85, 0.20);
  }
  #trigRecordBtn.painting .paint-key-badge {
    color: rgba(224, 85, 85, 0.5);
    border-color: rgba(224, 85, 85, 0.25);
  }
  #trigRecordBtn.painting:active {
    background: rgba(224, 85, 85, 0.10);
    box-shadow: 0 0 10px rgba(224, 85, 85, 0.20);
  }
  /* Unlike the trace indicator this one IS pressable, so it needs a cursor. */
  #trigRecordBtn { cursor: pointer; }

  /* Commit action painting flash — cloud mode: bright light blue */
  .commit-action-btn:not(.commit-mode--loop).painting {
    color: #9ed0f0;
    border-color: rgba(158, 208, 240, 0.65);
    background: rgba(158, 208, 240, 0.12);
    box-shadow: 0 0 12px rgba(158, 208, 240, 0.30);
  }
  .commit-action-btn:not(.commit-mode--loop).painting .paint-key-badge {
    color: rgba(158, 208, 240, 0.6);
    border-color: rgba(158, 208, 240, 0.3);
  }
  /* Commit action painting flash — loop mode: bright pink */
  .commit-action-btn.commit-mode--loop.painting {
    color: var(--status-accent);
    border-color: rgba(255, 107, 157, 0.65);
    background: rgba(255, 107, 157, 0.12);
    box-shadow: 0 0 12px rgba(255, 107, 157, 0.30);
  }
  .commit-action-btn.commit-mode--loop.painting .paint-key-badge {
    color: rgba(255, 107, 157, 0.6);
    border-color: rgba(255, 107, 157, 0.3);
  }

  /* suppress button press feel (all indicators) */
  .paint-indicator-btn:active {
    background: rgba(255,255,255,0.015);
    box-shadow: none;
  }

  /* ── Undo button ─────────────────────────────────────────────────────────── */
  
  /* ── Drop loop button ─────────────────────────────────────────────────── */
  
  /* active flash on click or keyboard — pink, loop family */
  

  /* active flash on click — amber, matches scan off / system mute */
  

  /* ── Sequential panel ──────────────────────────────────────────────────── */
  

  /* Section wrapper — consistent spacing between groups */
  .seq-section {
    padding: var(--sp-3) var(--sp-3);
  }
  .seq-section + .seq-section {
    border-top: none;
  }
  /* Labelled groups are separated by whitespace + the brighter label
     itself, not by a hairline. (Tried 1px dividers — too busy against
     dense parameter stacks.) Extra top padding gives the group its
     breathing room. */
  .seq-section + .seq-section:has(> .seq-section-label) {
    padding-top: 10px;
  }

  /* Section labels — sub-headings within a panel ("params", "cloud",
     "loop", "selection", etc.). Brighter + slightly larger than the
     previous tokens so they read as intentional section headers rather
     than dim annotations, while staying uppercase + tracked so they
     don't compete with the bold lowercase .device-label on the panel. */
  .seq-section-label {
    display: block;
    font-size: var(--fs-micro);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-secondary);
    font-weight: 500;
    margin-bottom: 8px;
  }

  /* ── Collapsible sections ── */
  .seq-section-toggle {
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 0.25rem;
  }
  .seq-section-toggle::before {
    content: '▾';
    font-size: var(--fs-nano);
    display: inline-block;
    transition: transform var(--trans-quick);
  }
  .seq-section--collapsible.collapsed .seq-section-toggle::before {
    transform: rotate(-90deg);
  }
  .seq-section--collapsible.collapsed .seq-section-toggle {
    margin-bottom: 0;
    color: var(--text-dimmer);
  }
  .seq-section--collapsible.collapsed .seq-section-body {
    display: none;
  }

  /* Sublabels within grouped collapsible sections */
  
  

  /* ── Record toggle — big, prominent ── */
  
  
  
  .seq-record-key {
    position: absolute;
    right: 0.7rem;
    font-size: var(--fs-2xs);
    color: var(--text-dimmer);
    border: 1px solid var(--surface-3);
    border-radius: var(--r-2);
    padding: var(--sp-1) var(--sp-3);
    line-height: 1.2;
  }

  /* Seed lock button — sage green when active (matches drop cloud color family) */
  

  /* ── Slot bank ── */
  /* Lock row — loop lock + seed lock side by side */
  

  /* ── Transport — performance buttons with icons ── */
  
  /* Color accents on hover */
  
  

  /* ── Manage — destructive actions, smaller ── */
  

  /* ── Loop params ── */
  .seq-controls {
    padding: 0;
  }

  /* Headphone balance controls — inside mixdown meter column */
  /* levels-mixdown-controls removed — headphone mix moved to audio settings */

  /* ── Right panel: seeds + samples ──────────────────────────────────────── */
  /* ── Drop zone overlay ───────────────────────────────────────────────────── */
  .drop-overlay {
    position: absolute;
    inset: 0;
    background: rgba(26, 42, 42, 0.92);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.2s;
    z-index: 10;
  }
  .drop-overlay.visible { opacity: 1; }
  .drop-overlay .drop-icon { font-size: 2rem; color: var(--text-highlight); }
  .drop-overlay .drop-text { font-size: var(--fs-md); color: var(--text-secondary); letter-spacing: var(--ls-wider); }

  /* ── Shared modal overlay + dialog ─────────────────────────────────────── */
  /* Base overlay: every modal overlay gets this */
  .mu-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 300;
    background: rgba(0,0,0,0.75);
    align-items: center;
    justify-content: center;
  }
  .mu-overlay.open { display: flex; }

  /* ── The settings modal (#255) ─────────────────────────────────────────
     Left nav, one section at a time. The section BODY is the real modal's
     own dialog, moved in and moved back out (js/ui-settings.js), so what you
     see here is literally the same DOM the rig view shows — never a copy. */
  /* Specificity note: .mu-dialog is declared AFTER this block and sets
     flex-direction: column, so the nav stacked on top of the body until this
     selector carried both classes. */
  .mu-dialog.settings-dialog { flex-direction: row; gap: 0; padding: 0;
    width: min(1180px, 94vw); height: min(760px, 88vh); max-height: 88vh;
    overflow: hidden; }
  .settings-nav { flex: 0 0 13.5rem; display: flex; flex-direction: column;
    gap: var(--seam); padding: 1rem 0.6rem; overflow-y: auto;
    border-right: 1px solid var(--border-faint); background: var(--surface-1); }
  .set-nav-grp { font-size: var(--fs-nano); letter-spacing: 0.16em; text-transform: uppercase;
    color: var(--text-faint); padding: 0.9rem 0.6rem 0.3rem; }
  .set-nav-grp:first-child { padding-top: 0; }
  .set-nav-item { text-align: left; background: none; border: none; border-radius: 3px;
    font-family: var(--font-sans); font-size: var(--fs-body); color: var(--text-dim);
    padding: 0.42rem 0.6rem; cursor: pointer; }
  .set-nav-item:hover { color: var(--text-bright); background: var(--surface-3); }
  .set-nav-item.active { color: var(--text-emphasis); background: var(--surface-5); }
  .settings-body { flex: 1 1 auto; display: flex; flex-direction: column;
    min-width: 0; min-height: 0; }
  .settings-head { display: flex; align-items: center; gap: 0.8rem;
    padding: 0.9rem 1.2rem; border-bottom: 1px solid var(--border-faint); }
  .settings-head h2 { margin: 0; font-size: var(--fs-control); font-weight: 600;
    letter-spacing: 0.05em; text-transform: lowercase; }
  .settings-head .close-btn { margin-left: auto; position: static; }
  .settings-host { flex: 1 1 auto; min-height: 0; overflow-y: auto;
    overscroll-behavior: contain; padding: 0.2rem 0.2rem 1rem; }
  /* A borrowed dialog stops being a dialog: no frame, no cap on its height,
     and no close button of its own — the shell's ✕ is the only one. */
  .settings-host .in-settings {
    border: none; background: none; max-height: none; height: auto;
    width: 100%; padding: 1rem 1.2rem; box-shadow: none;
    /* A borrowed dialog must stop being a scroll container. `.mu-dialog` sets
       `overflow-y: auto` with `overscroll-behavior: contain` so a modal does
       not scroll the page behind it — but with its max-height removed it has
       nothing left to scroll, and `contain` then SWALLOWS the wheel instead of
       passing it to the host. That is why the keys page, 4043px of table
       inside a 711px host, would not scroll at all (#271). The host is the
       one scroller now. */
    overflow: visible; overscroll-behavior: auto;
  }
  .settings-host .in-settings > h2 { display: none; }
  .settings-host .in-settings > h2 + *,
  .settings-host .in-settings > .close-btn { margin-top: 0; }
  .settings-host .in-settings .close-btn { display: none; }
  /* A borrowed rig DEVICE (#262): it is built to sit in a 246px panel column,
     so in the settings host it drops the column frame and lets its sections
     use the width. Collapsible sections open — a settings page that hides
     half of itself behind disclosure triangles is the thing this page exists
     to replace. */
  .settings-host .device--commit { border: none; background: none; width: 100%;
    max-width: none; padding: 0.4rem 0.2rem; }
  .settings-host .device--commit > .device-label { display: none; }
  .settings-host .device--commit .seq-section--collapsible .seq-section-body {
    display: block !important; }
  .settings-host .device--commit .seq-section-toggle { cursor: default; }
  .settings-host .device--commit .seq-section { max-width: 34rem; }

  /* ── One look for every settings page (#269) ───────────────────────────
     The pages are borrowed from panels and modals that each had their own
     frame, so the shell had boxes inside boxes inside boxes. Here a SECTION
     is a caption and some rows with air around them — no card, no outline. A
     rule appears only where two sections meet, and nothing nests. */
  
  .settings-host .in-settings .seq-section,
  .settings-host .in-settings .device-body > .grain-row {
    border: none !important; background: none !important;
    border-radius: 0 !important; box-shadow: none !important;
    padding: 0 !important; margin: 0 0 1.15rem !important;
  }
  .settings-host .in-settings .seq-section + .seq-section {
    border-top: 1px solid var(--border-faint) !important; padding-top: 1.15rem !important;
  }
  /* Section captions are the eyebrow, everywhere. */
  .settings-host .in-settings .seq-section-label,
  .settings-host .in-settings .device-label {
    font-size: var(--fs-eyebrow, 0.56rem); letter-spacing: var(--ls-eyebrow, 0.16em);
    text-transform: uppercase; color: var(--text-secondary); font-weight: 500;
    display: block; margin-bottom: 0.7rem;
  }
  /* One row rhythm: label left, control right, generous line height. */
  /* label · control · value — three columns, so a numeric readout sits at the
     end of its own line instead of wrapping underneath the label. */
  
  .settings-host .in-settings .grain-row {
    display: grid; grid-template-columns: 9rem minmax(0, 1fr) 4rem;
    align-items: center; gap: 0.9rem; min-height: 1.9rem; padding: 0.1rem 0;
    border: none; background: none;
  }
  .settings-host .in-settings .grain-numbox,
  .settings-host .in-settings .as-val {
    grid-column: 3; justify-self: end; text-align: right;
    font-family: var(--font-mono, monospace); font-size: var(--fs-meta, 0.6rem);
    font-variant-numeric: tabular-nums; color: var(--text-secondary);
    background: none; border: none; width: 100%;
  }
  /* Anything that is not a label or a value takes the middle column. */
  .settings-host .in-settings .grain-row > .grain-seg,
  .settings-host .in-settings .grain-row > input[type=range],
  .settings-host .in-settings .grain-row > select{ grid-column: 2; justify-self: start; }
  /* Choices are pills here too — same rule as everywhere else (#267). */

  /* The device's own name is the page title already. */
  .settings-host .in-settings > .device-label,
  .settings-host .device--commit .device-label { display: none !important; }
  .settings-host .in-settings .grain-label {
    font-size: var(--fs-micro); letter-spacing: 0.01em; text-transform: none;
    color: var(--text-dim); font-weight: 400;
  }
  /* ── The slider, rebuilt (#288) ────────────────────────────────────────
     Every range in here was still the browser's own control: a fat white
     rounded track and a circular thumb, in the middle of a screen where every
     other slider is a 2px line with a 2px tick. Same geometry as `.prow-t`
     now — track, fill, handle — so a settings page and an engine page are
     visibly the same instrument.

     The FILL is the part native ranges cannot do once `appearance: none`
     removes it, so `ui-settings.js` writes `--fill` (0–100) on input and on
     every page show. Without that the gradient is simply flat and the handle
     still reads correctly — it degrades to a plain track, never to nothing. */
  .settings-host .in-settings input[type=range] {
    -webkit-appearance: none; appearance: none;
    background: none; border: none; width: 100%; max-width: none;
    height: 1.6rem; margin: 0; padding: 0; cursor: ew-resize;
  }
  .settings-host .in-settings input[type=range]::-webkit-slider-runnable-track {
    height: 2px; border-radius: 1px;
    background: linear-gradient(to right,
      color-mix(in srgb, var(--set-accent) 75%, transparent) 0 calc(var(--fill, 0) * 1%),
      var(--surface-4) calc(var(--fill, 0) * 1%) 100%);
  }
  .settings-host .in-settings input[type=range]::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none;
    width: 2px; height: 11px; margin-top: -4.5px; border: none;
    border-radius: 1px; background: var(--set-accent);
  }
  .settings-host .in-settings input[type=range]:disabled { opacity: 0.45; }

  /* ── Notes, and the boxes that were left (#288) ───────────────────────
     A hint is a quiet line under the control it belongs to, not an orange
     italic paragraph shouting across the page — it was the loudest thing on
     the audio page and it is the least important. Aligned to the control
     column so it reads as belonging to the row above it. */
  .settings-host .in-settings .as-note {
    font-style: normal; color: var(--text-faint);
    font-size: var(--fs-meta); line-height: 1.45;
    padding-left: 0; margin: -0.35rem 0 0.9rem;
    grid-column: 2 / -1; max-width: 34rem;
  }
  /* Nothing nests: the #269 rule flattened `.as-section` and `.seq-section`,
     but the tables and framed sub-boxes inside them kept their own cards. */
  .settings-host .in-settings .as-io-table {
    border: none !important; background: none !important;
    border-radius: 0 !important; box-shadow: none !important;
  }
  .settings-host .in-settings .as-meter-group-label {
    font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow);
    text-transform: uppercase; color: var(--text-secondary);
  }
  /* Selects and number fields: the app's own field, not the browser's. */
  .settings-host .in-settings select,
  .settings-host .in-settings input[type=number],
  .settings-host .in-settings input[type=text] {
    font-family: var(--font-sans); font-size: var(--fs-control);
    color: var(--text-bright); background: var(--surface-1);
    border: 1px solid var(--border-faint); border-radius: 3px;
    padding: 0.24rem 0.45rem;
  }
  .settings-host .in-settings select:focus,
  .settings-host .in-settings input[type=number]:focus,
  .settings-host .in-settings input[type=text]:focus {
    outline: none; border-color: var(--set-accent);
  }
  .settings-host { --set-accent: var(--grid-teal, #7abcbc); }
  /* A range fills its column so its value lands beside it, the way an engine
     row reads. A select does not — a stretched dropdown is just a wide box. */
  
  .settings-host .in-settings .grain-row > input[type=range] { justify-self: stretch; }
  .settings-host .in-settings .as-select,
  .settings-host .in-settings .grain-select { max-width: 17rem; }
  /* Segmented controls keep the app's pill; buttons keep the app's rectangle. */
  /* The settings window's segmented is NOT a pill. This 999px rule is legacy —
     it is what the old hosted dialogs looked like — and beside an r8 dropdown
     and an r8 field a pill reads as foreign. SETTINGS-GUI § 3 specifies r8
     outer / r6 inner and the spec wins (round twenty-five).
     NOTE THE ASYMMETRY: the INSTRUMENT's chrome segment stays a pill
     (.seg-pill); the settings window's does not. Same control, two scopes,
     different radii — exactly like the two sliders and the two switches
     (DESIGN-SYSTEM § 5). */
  .settings-host .in-settings .grain-seg { border-radius: var(--r-field); }
  .settings-host .in-settings .top-bar-btn,
  .settings-host .in-settings .mu-btn {
    border-radius: 3px; text-transform: uppercase;
    font-size: var(--fs-meta, 0.6rem); letter-spacing: 0.06em;
  }
  /* Collapsibles are open and inert here — a settings page that hides half of
     itself behind a triangle is what this shell replaces. */
  .settings-host .in-settings .seq-section-toggle::before,
  .settings-host .in-settings .seq-section-toggle::after { display: none !important; }

  /* ── The OSC reference ─────────────────────────────────────────────────── */
  .set-toolbar { display: flex; align-items: center; gap: 0.8rem; margin-bottom: 0.9rem; }
  .set-search { flex: 1 1 auto; min-width: 0; background: var(--surface-1);
    border: 1px solid var(--border-faint); border-radius: 3px; color: var(--text-bright);
    font-family: var(--font-sans); font-size: var(--fs-micro); padding: 0.34rem 0.6rem; }
  .set-search:focus { outline: none; border-color: var(--grid-teal, #7abcbc); }
  .set-count { font-family: var(--font-mono, monospace); font-size: var(--fs-meta, 0.6rem);
    color: var(--text-faint); flex: none; }
  .set-empty { color: var(--text-faint); font-size: var(--fs-micro); padding: 1rem 0; }
  #settingsPanels[hidden] { display: none; }

  /* Base dialog: every inner dialog box gets this */
  .mu-dialog {
    position: relative;
    background: var(--bg-app);
    border: 1px solid var(--surface-6);
    border-radius: 0;
    padding: 1.4rem 1.6rem 1.6rem;
    max-height: 88vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    font-size: var(--fs-md);
    color: var(--text-primary);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 0.8rem;
  }
  .mu-dialog h2 {
    font-family: var(--font-display);
    font-size: var(--fs-xl);
    font-weight: normal;
    color: var(--text-highlight);
    letter-spacing: var(--ls-uppercase);
    margin: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-shrink: 0;
  }
  .mu-dialog h2 .close-btn {
    background: none;
    border: none;
    color: var(--text-muted);
    font-family: var(--font-sans);
    font-size: var(--fs-md);
    cursor: pointer;
    padding: 0;
    letter-spacing: var(--ls-wide);
  }
  .mu-dialog h2 .close-btn:hover { color: var(--text-highlight); }

  /* ── Factory reset / export / import dialogs ─────────────────────────── */
  .dlg-overlay {
    position: fixed;
    inset: 0;
    z-index: 10000;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0,0,0,0.75);
  }
  .dlg-dialog {
    background: var(--bg-app);
    border: 1px solid var(--surface-6);
    border-radius: 0;
    padding: 1.4rem 1.6rem 1.6rem;
    max-width: 400px;
    width: min(400px, 90vw);
    box-sizing: border-box;
  }
  .dlg-title {
    font-family: var(--font-display);
    font-size: var(--fs-xl);
    color: var(--text-highlight);
    letter-spacing: var(--ls-uppercase);
    text-transform: uppercase;
    margin-bottom: 0.75rem;
  }
  .dlg-desc {
    font-size: var(--fs-md);
    color: var(--text-primary);
    line-height: 1.5;
    margin-bottom: 1rem;
  }
  
  
  /* Reset dialog — one row per storage category (js/storage-registry.js). */
  .reset-cats {
    margin-bottom: 1rem;
    max-height: min(46vh, 320px);
    overflow-y: auto;
  }
  .reset-cat {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    padding: 0.35rem 0;
    cursor: pointer;
    border-bottom: 1px solid var(--surface-3);
  }
  .reset-cat:last-child { border-bottom: none; }
  .reset-cat:hover .reset-cat-label { color: var(--text-highlight); }
  .reset-cat input[type="checkbox"],
  .reset-cat input[type="radio"] {
    accent-color: var(--accent-action);
    width: 14px;
    height: 14px;
    margin-top: 2px;
    flex: 0 0 auto;
  }
  .reset-cat-text { min-width: 0; }
  .reset-cat-label {
    font-size: var(--fs-md);
    color: var(--text-primary);
    transition: color var(--trans-base);
  }
  .reset-cat-hint {
    display: block;
    font-size: var(--fs-sm);
    color: var(--text-muted);
    line-height: 1.35;
  }
  .dlg-btn:disabled {
    opacity: 0.4;
    cursor: default;
  }
  .dlg-btn:disabled:hover {
    background: var(--surface-3);
    color: var(--text-primary);
  }
  .dlg-btns {
    display: flex;
    gap: 0.6rem;
    justify-content: flex-end;
  }
  .dlg-btn {
    padding: 0.35rem 1rem;
    border-radius: 0;
    border: 1px solid var(--surface-6);
    background: var(--surface-3);
    color: var(--text-primary);
    font-size: var(--fs-md);
    cursor: pointer;
    transition: background var(--trans-base), color var(--trans-base);
  }
  .dlg-btn:hover { background: var(--surface-6); color: var(--text-highlight); }
  .dlg-go {
    background: var(--tint-action-medium);
    border-color: var(--tint-action-border);
    color: var(--accent-action);
  }
  .dlg-go:hover {
    background: rgba(232,160,48,0.25);
    color: #f0b040;
  }

  /* ── Export/Import buttons ──────────────────────────────────────────────── */
  #exportSetupBtn.export-flash {
    color: var(--accent-sweep);
    background: rgba(118,255,3,0.12);
    transition: color var(--trans-slow), background var(--trans-slow);
  }

  /* ── Learning mode ───────────────────────────────────────────────────────── */
  #learnModeBtn.learn-active {
    color: var(--accent-lock);
    background: rgba(122,188,188,0.12);
  }
  /* THE TOOLTIP (redesigned 2026-09-24, Ek: "can the design of the tooltip be
     better?"). A menu item, not a note: one line, the word in the bright
     text, its inputs as keycaps in the tiles' own shape. Flat, per the brief —
     the app ground with the kit's hairline, no blur, no glow, no teal tint
     borrowed from the lock accent. It fades in over --m-touch, the hover
     timing, and never animates layout. Learn on widens it to the long text
     with the keycaps under it. */
  .learn-tooltip {
    position: fixed;
    z-index: 9999;
    pointer-events: none;
    display: flex; align-items: center; gap: var(--sp-3);
    max-width: 280px;
    padding: var(--sp-2) var(--sp-3);
    font-family: var(--font-sans);
    font-size: var(--fs-meta);
    line-height: 1.35;
    white-space: nowrap;
    color: var(--text-bright);
    background: var(--bg-app);
    border: 1px solid var(--border-medium);
    border-radius: var(--r-3);
    opacity: 0;
    transform: translateY(-2px);
    transition: opacity var(--m-touch) var(--ease-out), transform var(--m-touch) var(--ease-out);
  }
  .learn-tooltip.visible { opacity: 1; transform: none; }
  .learn-tooltip .lt-keys { display: inline-flex; gap: var(--sp-1); }
  .learn-tooltip kbd {
    font-family: var(--font-sans); font-size: var(--fs-nano); line-height: 1;
    padding: var(--sp-0) var(--sp-2); border-radius: var(--r-2, 3px);
    color: var(--text-secondary); background: var(--surface-2);
    border: 1px solid var(--border-faint);
  }
  .learn-tooltip--long { flex-direction: column; align-items: flex-start; gap: var(--sp-2);
    white-space: normal; padding: var(--sp-3); }
  .learn-tooltip--long .lt-text { color: var(--text-secondary); }

  /* ── Sample slots (in bottom panel sample instrument) ───────────────────── */
  .sample-slot {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.18rem 0.5rem;
    border-bottom: 1px solid rgba(0,0,0,0.1);
    font-size: var(--fs-md);
    color: var(--text-muted);
    user-select: none;
    transition: background var(--trans-fast);
  }
  .sample-slot:hover { background: var(--chrome-bg-well); }
  .sample-slot.active {
    border-color: var(--border-hover);
    background: var(--chrome-bg-well);
  }
  /* "active" here means the key is toggled ON for painting */
  .sample-slot.painting {
    border-color: var(--slot-color, var(--text-highlight));
    background: var(--chrome-bg-well);
  }
  .sample-slot.loaded { color: var(--text-primary); }
  .sample-slot.empty.active { border-color: var(--text-secondary); }
  .sample-slot.drag-over { border-color: var(--border-focus); background: var(--chrome-bg-well); }
  .sample-slot .slot-drag-handle {
    color: var(--text-secondary);
    cursor: grab;
    font-size: var(--fs-md);
    line-height: 1;
    padding: 0 var(--sp-0);
    visibility: hidden;
  }
  .sample-slot.loaded .slot-drag-handle { visibility: visible; }
  .sample-slot.loaded .slot-drag-handle:hover { color: var(--text-highlight); }
  .sample-slot .slot-key {
    color: var(--text-secondary);
    width: 1em;
    flex-shrink: 0;
    text-align: left;
  }
  /* Key shows the sample colour when that slot is actively painting */
  .sample-slot.painting .slot-key { color: var(--slot-color, var(--text-highlight)); }
  .sample-slot .slot-name {
    color: var(--text-secondary);
    flex-shrink: 0;
    max-width: 180px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .sample-slot.empty .slot-name { color: var(--text-secondary); }
  .sample-slot .slot-duration { color: var(--text-secondary); flex-shrink: 0; }
  .sample-slot .slot-waveform {
    flex: 1;
    height: 18px;
    background: var(--surface-4);
    border-radius: 0;
    overflow: hidden;
    position: relative;
    min-width: 60px;
  }
  .sample-slot.loaded .slot-waveform { height: 28px; cursor: default; }
  .sample-slot.loaded .slot-waveform.near-handle { cursor: col-resize; }
  .sample-slot .slot-waveform canvas {
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0; left: 0;
    pointer-events: none; /* let mouse events fall through to waveDiv */
  }
  .sample-slot.empty .slot-waveform { background: #282828; opacity: 0.4; }
  .sample-slot .slot-play {
    color: var(--text-secondary);
    font-size: var(--fs-md);
    line-height: 1;
    padding: 0 var(--sp-0);
    cursor: pointer;
    visibility: hidden;
    border: none;
    background: none;
    font-family: inherit;
    flex-shrink: 0;
  }
  .sample-slot.loaded .slot-play { visibility: visible; }
  .sample-slot.loaded .slot-play:hover { color: var(--text-highlight); }
  .sample-slot.loaded .slot-play.playing { color: #e0c860; }
  .sample-slot .slot-delete {
    color: var(--text-secondary);
    font-size: var(--fs-md);
    line-height: 1;
    padding: 0 var(--sp-2);
    cursor: pointer;
    visibility: hidden;
    border: none;
    background: none;
    font-family: inherit;
  }
  .sample-slot.loaded .slot-delete { visibility: visible; }
  .sample-slot.loaded .slot-delete:hover { color: var(--accent-danger); }

  /* Seed banks removed — spatial tracking via canvas edge indicators */

  /* ── Sample instrument modal ───────────────────────────────────────────────── */
  /* Sample modal — size override only */
  
  /* sample list scrollable area */
  
  
  
  
  
  /* ── Fullscreen button ───────────────────────────────────────────────────── */
  #fullscreenBtn {
    background: none;
    border: none;
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: var(--fs-xl);
    padding: 0;
    cursor: pointer;
    pointer-events: all;
    opacity: 0.7;
    transition: opacity 0.2s, color 0.2s;
    line-height: 1;
    flex-shrink: 0;
  }
  #fullscreenBtn:hover {
    opacity: 1;
    color: var(--text-primary);
  }

  /* ── Top-bar segmented picker (camera mode) ──────────────────────────────── */
  /* Reuses the panel .grain-seg / .grain-seg-btn vocabulary so a mode picker
     looks the same wherever it appears. The camera modal it replaced is gone
     (2026-08-01) — the per-mode copy is in the chip tooltips now.
     Only two things need overriding for the top-bar context. */
  .top-bar-seg-group {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    white-space: nowrap;
  }
  /* Label, not a control. It reads exactly like a .grain-label in a panel row
     — same size, same colour, same tracking — so nothing about it invites a
     click. (It was a button-styled chip with an icon at first, which looked
     pressable and wasn't.) */
  .top-bar-seg-label {
    font-family: var(--font-sans);
    font-size: var(--fs-sm);
    color: #5e5e5e;
    letter-spacing: var(--ls-normal);
    cursor: default;
    user-select: none;
  }
  /* .grain-seg carries `margin-left: auto` to right-align inside a panel row.
     In the top bar that would shove the chips to the far edge, away from
     their own label. Everything else — chip size, colours, hover, the active
     state — is inherited unchanged, so this picker and the panel pickers are
     the same control. */
  .top-bar-seg { margin-left: 0; }

  /* ── MIDI / Keyboard mapping modal ──────────────────────────────────────── */
  /* Mapping modal — size override only */
  /* Wider than the 1100 it was before the scale columns landed: the table's
     measured min-content at the designed tier is 1096, which 1100 clears by
     four pixels and a 95vw floor does not clear at all at the 1161px tier edge
     (95vw is 1103 there, and that margin vanished the moment anything was added
     to a cell). calc(100vw - 2rem) leaves ~30px. Re-measure if a column is ever added — see scripts/ui-shots.js. */
  .mapping-dialog { width: min(1140px, calc(100vw - 2rem)); }
  /* Held no text but still reserved a line inside the pinned header — a
     permanent empty band between the filter and the first row. */
  
  /* Title + filter + status pin together — .mu-dialog is the scroll container,
     so anything left unpinned scrolls away from a 100-row table. */
  
  /* The dialog scrolls inside its own padding, so rows would show in the strip
     above the pinned header.  Extend the background up and out over it. */
  
  /* A curved or windowed binding looks identical to a raw one once you've
     stopped reading the numbers — same reasoning as the accessory table's
     tinted pad number. */
  
  /* Unset cells all read as one dim hyphen — "unassigned", "—" and an inline
     grey were three ways of saying nothing across the same table. */
  
  /* osc + data are the reference columns — right-aligned so paths and type
     signatures line up on an edge instead of ragging. */
  
  @keyframes learnPulse { from { opacity: 1; } to { opacity: 0.4; } }
  .mapping-legend {
    margin: 0.8rem 0 0;
    padding: 0.6rem 0.7rem;
    border: 1px solid var(--chrome-border-dark);
    border-radius: var(--r-2);
    background: var(--chrome-bg-sunk);
    color: var(--text-hover);
    font-size: var(--fs-sm);
    line-height: 1.55;
    font-family: var(--font-sans);
  }
  /* ── Accessory modal ────────────────────────────────────────────────────── */
  /* Inherits .mapping-table for the grid; only the live-value chrome is new. */
  
  
  
  
  
  .acc-select {
    background: var(--bg-app);
    color: var(--text-primary);
    border: 1px solid var(--chrome-border-dark);
    border-radius: var(--r-1);
    font-family: var(--font-sans);
    font-size: var(--fs-md);
    padding: 0.1rem 0.2rem;
    max-width: 13rem;
  }
  .acc-select:disabled { color: var(--text-dimmer); }
  

  /* ── Scale stage: output window + response curve ─────────────────────────── */
  /* Numbers carry units ("−600 ¢"), so these are text inputs rather than
     type=number — the spinner arrows would also fight the unit suffix. Mono so
     the min/max columns stay visually aligned down the eight rows. */
  
  
  /* The mapping table carries ~130 rows in a fixed-width dialog, so its copies
     of these boxes are sized to the number alone — the unit lives in the hover
     text there rather than in the value. */
  
  
  /* A pot confined to part of its destination's travel looks identical to a
     full-range one once you've stopped reading the numbers. Tint the pad so the
     limited channels are findable at a glance. */
  

  /* ── LED mapping modal ──────────────────────────────────────────────────── */
  /* Inherits .mapping-table for the grid; the swatch strip is the only new
     chrome. Narrower than the mapping modal — seven short columns. */
  .led-dialog { width: min(820px, 95vw); }
  .led-rate {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    color: var(--text-dimmer);
    white-space: nowrap;
    width: 7rem;
  }

  /* Section headers inside the table — states vs events. */
  

  .led-row.is-off .led-fn { color: var(--text-dimmer); }

  /* Swatch strip. Fixed 11 cells, no wrap — the palette is deliberately small
     enough to stay one glanceable row. */
  
  .led-swatch {
    width: 16px;
    height: 16px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--r-1);
    cursor: pointer;
    /* Selection is shown with an inset ring rather than an outer border so the
       swatches don't shift position when the selection moves. */
    box-shadow: none;
  }
  .led-swatch.is-off { border-color: var(--chrome-border-dark); }
  .led-swatch:hover { border-color: var(--text-secondary); }
  .led-swatch.sel {
    border-color: var(--text-highlight);
    box-shadow: inset 0 0 0 1px var(--bg-app);
  }
  .led-swatch:disabled { opacity: 0.35; cursor: default; }
  .led-swatch:disabled:hover { border-color: transparent; }
  .led-swatch.is-off:disabled:hover { border-color: var(--chrome-border-dark); }

  .led-select { max-width: 6.5rem; }
  .led-test:disabled { opacity: 0.4; cursor: default; }

  /* Timbre rows take their colour from the audio, so the swatch strip is inert.
     Dimmed rather than hidden — the choice is still remembered for when the row
     goes back to solid, and hiding it would make the row jump. */
  
  
  /* Live colour going to the sensor right now. */
  .led-timbre-chip {
    cursor: default;
    border-color: var(--chrome-border-dark);
  }
  .led-timbre-chip:hover { border-color: var(--chrome-border-dark); }

  /* Live readout — which state is driving the LED, and what the last event did.
     Exists because the defaults match the old hardcoded palette, so the LED
     alone can't tell you whether this table is authoritative. */
  .led-activity {
    color: var(--accent-action);
    min-width: 12rem;
  }
  .led-activity.is-blocked { color: var(--text-dimmer); }

  /* Active state row. The marker sits in the gutter of the function cell so
     rows don't reflow as the active state moves between them. */
  

  /* Event fired — brief wash so a flash is visible in the table even when the
     performer's eyes were elsewhere. */
  @keyframes ledFired {
    from { background: var(--accent-action); }
    to   { background: transparent; }
  }

  .mapping-legend strong { color: var(--text-emphasis); font-weight: 500; }
  .mapping-legend em     { color: var(--text-bright); font-style: normal; font-weight: 500; }
  .mapping-legend code {
    color: #d0d0d0;
    background: var(--bg-panel);
    padding: 0.05rem 0.25rem;
    border-radius: var(--r-1);
    font-family: var(--font-mono);
    font-size: var(--fs-base);
  }

  /* ── Live MIDI/OSC monitor ───────────────────────────────────────────── */
  

/* ── Patch Table Editor modal ────────────────────────────────────────────── */
/* Patch table modal — size override only */
/* Parameter label column (frozen) */
/* Preset column headers */
/* ── Sensor registry modal ────────────────────────────────────────────────── */

/* ── Audio settings modal ─────────────────────────────────────────────────── */
/* Audio settings modal — size override only */
.audio-dialog { min-width: 640px; max-width: 860px; width: min(860px, 94vw); }
/* Each section is a proper box like the main-page devices */

/* Collapsible section labels inside audio settings — larger + obvious toggle caret */

.as-select {
  background: var(--chrome-bg-well);
  border: 1px solid var(--surface-6);
  border-radius: 0;
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  padding: 0.2rem 0.4rem;
  cursor: pointer;
  flex: 1;
  min-width: 0;
}
.as-slider {
  flex: 1;
  accent-color: var(--text-secondary);
  cursor: pointer;
  min-width: 0;
}
.as-val {
  min-width: 4.5rem;
  font-size: var(--fs-md);
  color: var(--text-primary);
  text-align: right;
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}
/* Noise gate visual meter */
.as-gate-meter {
  flex: 1;
  height: 28px;
  min-width: 0;
  border-radius: var(--r-2);
  cursor: ew-resize;
  border: 1px solid var(--surface-5);
}

/* Gate activity indicator light */
.as-gate-light {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--chrome-border-dim);
  flex-shrink: 0;
  transition: background 0.06s;
}
.as-gate-light.closed {
  background: #e05050;
  box-shadow: 0 0 4px #e05050;
}
.as-gate-light.recording {
  background: #e03030;
  box-shadow: 0 0 6px #e03030;
}
.as-dim {
  font-size: var(--fs-md);
  color: var(--text-muted);
  letter-spacing: var(--ls-normal);
}
/* A readout that is saying something. The ceiling's row is `idle` in the
   ordinary state and takes the warn ochre only when it has acted — the same
   step the recording budget and the CEIL meter use, so "getting close" looks
   the same wherever it appears. */
.as-note {
  font-size: var(--fs-md);
  color: var(--text-dim);
  letter-spacing: var(--ls-normal);
  font-style: italic;
  padding-left: 6rem;
  margin-top: -0.05rem;
}

.status-strip {
  font-size: var(--fs-md);
  padding: 0.28rem 0.6rem;
  letter-spacing: var(--ls-normal);
  margin-top: 0.05rem;
}
.status-strip.idle  { background: var(--chrome-bg-idle); color: var(--text-dim); border: 1px solid rgba(255,255,255,0.07); }
/* .ok / .warn / .error DELETED (round thirteen). The strip is a badge (round
   four) and settings-gui.css states its face at a depth these three could not
   reach, so they had not rendered since — round twelve tokenised them and the
   screen diff was empty because they were already dead. A state name does not
   need a colour when the meter two rows away is already saying sage, ochre or
   brick. js/ui-audio-settings.js still writes the class; it is now inert, and
   `idle` is the only one with a rule. */
.latency-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  display: inline-block;
  flex-shrink: 0;
  background: var(--chrome-border-dim);
}
.latency-dot.ok   { background: var(--status-ok); }
/* The ochre token is ~24% less luminous than the #e8c840 this used to be, and
   that is accepted (round twelve). If it stops reading at a glance mid-show the
   fix is this dot's SIZE or a ring around it — NOT a brighter colour. A
   near-duplicate --status-warn-bright would restart the palette drift on day
   one, which is the thing these rounds exist to undo. */
.latency-dot.warn { background: var(--status-warn); }
.latency-dot.bad  { background: var(--accent-danger); }
.as-btn {
  background: var(--chrome-bg-well);
  border: 1px solid var(--surface-6);
  border-radius: 0;
  color: var(--text-secondary);
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  padding: 0.3rem 0.9rem;
  cursor: pointer;
  letter-spacing: var(--ls-wide);
  transition: background var(--trans-fast), color var(--trans-fast);
}
.as-btn:hover { background: #2e2e2e; color: var(--text-primary); }
.as-btn.active { background: #2e2e2e; color: var(--text-primary); border-color: var(--border-focus); }

/* ── Multi-channel vertical meter strip ──────────────────────────────────── */
.as-multi-meter-wrap {
  display: flex;
  flex-direction: row;
  gap: var(--sp-1);
  background: var(--surface-3);
  border: 1px solid rgba(255,255,255,0.07);
  border-radius: 0;
  padding: 0.3rem 0.5rem 0.25rem;
  margin: 0.15rem 0 0.1rem;
  height: 64px;          /* fixed height — no stretching */
  align-items: stretch;
  overflow-x: auto;      /* scroll if many channels */
  overflow-y: hidden;
  overscroll-behavior: contain;
}
/* placeholder text shown when no stream is active */
.as-multi-meter-wrap:empty::before {
  content: 'no signal';
  color: var(--chrome-border-dim);
  font-size: var(--fs-md);
  letter-spacing: var(--ls-wide);
  align-self: center;
  margin: auto;
}
/* ── The ceiling's gain-reduction meter (2026-09-14) ──────────────────────
   Beside OUT, in the same wrap as every other meter so the row reads as one
   instrument. It hangs from the TOP — a GR meter measures what is being
   taken away, so it grows downward — and it is EMPTY in the normal state,
   which is the state it should be in all night. The bar is the warn ochre
   until it is deep, then the danger brick: the same two steps the recording
   budget uses, so "getting close" and "over" look the same everywhere. */
.ceil-chan { justify-content: flex-start; background: var(--surface-2); }
.ceil-bar {
  width: 100%; height: 0%;
  background: var(--accent-warn);
  transition: height 0.06s linear, background-color var(--trans-base);
}
.ceil-bar.deep { background: var(--accent-danger); }

.as-vchan {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--sp-0);
  flex: 0 0 18px;   /* fixed 18px per channel — scrolls for large counts */
  min-width: 0;
}
/* clip indicator dot above the bar */
.as-vchan-clip {
  width: 100%;
  height: 4px;
  border-radius: 1px;
  background: var(--surface-4);
  flex-shrink: 0;
  transition: background 0.05s;
}
.as-vchan-clip.clipping { background: var(--accent-danger); box-shadow: 0 0 4px rgba(224,96,96,0.4); }
/* selected input channel — outlined box in the settings modal, where the
   column is wide and labelled. The footer rail overrides this. */
.as-vchan.is-selected { outline: 1px solid var(--accent-lock); border-radius: 2px; }
/* the canvas bar itself */
.as-vchan canvas {
  width: 100%;
  flex: 1;
  display: block;
  border-radius: 1px;
  background: var(--bg-app);
  min-height: 0;
}
/* thin vertical divider between meter groups (e.g. house buses | L R mixdown) */
.as-vchan-sep {
  flex: 0 0 1px;
  background: rgba(255,255,255,0.14);
  align-self: stretch;
  margin: 0 var(--sp-2);
}
/* channel number label under bar */
.as-vchan-label {
  font-size: var(--fs-md);
  color: var(--text-secondary);
  text-align: center;
  letter-spacing: var(--ls-wide);
  flex-shrink: 0;
  line-height: 1.3;
}

/* ── Legacy routing table (kept for reference) ───────────────────────────── */

/* ── I/O mapping table (software-centric routing) ────────────────────────── */
/* Used for both input and output sections.
   Left column: software path label.  Right column: hardware channel dropdown. */
.as-io-table {
  margin: 0.4rem 0;
  border: 1px solid var(--surface-5);
  border-radius: 0;
  overflow: hidden;
}
/* Small secondary label (angle / "monitor") shown inside the sw cell */
.as-io-angle {
  font-size: var(--fs-body);
  color: var(--text-dim);
  margin-left: 0.35rem;
  letter-spacing: var(--ls-normal);
}
/* ── Speaker angle input (editable azimuth per speaker) ────────────────── */
.as-io-angle-input {
  width: 3.8rem;
  flex-shrink: 0;
  background: var(--chrome-bg-raised);
  border: 1px solid var(--surface-5);
  border-radius: var(--r-1);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  padding: 0.12rem 0.25rem;
  text-align: right;
  -moz-appearance: textfield;
}
.as-io-angle-input::-webkit-inner-spin-button,
.as-io-angle-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.as-io-angle-input:focus {
  outline: none;
  border-color: var(--border-focus);
  background: var(--chrome-bg-option);
  color: var(--text-light);
}
/* ── Pre-fader meter group label ─────────────────────────────────────────── */
.as-meter-group-label {
  font-size: var(--fs-body);
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: var(--ls-display);
  margin-top: 0.4rem;
  margin-bottom: 0.1rem;
}

/* ── Viz settings modal ──────────────────────────────────────────────────── */
/* Viz settings modal — size override only */
.viz-dialog { min-width: 400px; max-width: 520px; width: min(520px, 94vw); }
/* Reuse .as-section, .as-row, .as-label, .as-slider, .as-val inside viz-dialog */

/* ── x-IMU3 setup modal ───────────────────────────────────────────────── */
.imu-setup-dialog { width: min(1060px, calc(100vw - 2rem)); overflow-y: hidden; }
/* The two-column shell (.imu-setup-columns / -left / -right) and the per-device
   card frame were deleted with the sensors page's conversion (#293): the page is
   one column of rows now, and the selected sensor is a container rather than a
   card. `git log --diff-filter=D` has them if they are ever wanted back. */

.imu-setup-select {
  font: var(--fs-body)/1 var(--font-mono);
  background: var(--chrome-bg-flat);
  color: var(--text-emphasis);
  border: 1px solid rgba(122,188,188,0.2);
  border-radius: var(--r-2);
  padding: var(--sp-2) var(--sp-3);
  width: 100%;
}
.imu-setup-select:focus { border-color: var(--tint-lock-strong); outline: none; }

/* Device list */

.imu-setup-empty {
  font: var(--fs-micro)/1.4 var(--font-mono);
  color: var(--text-dim);
  padding: var(--sp-4) 0;
}

.imu-setup-cmd-log {
  max-height: 120px;
  overflow-y: auto;
  font: var(--fs-nano)/1.4 var(--font-mono);
  color: var(--text-subtle);
  padding: var(--sp-2) 0;
}
.imu-setup-cmd-log-entry {
  padding: 1px var(--sp-3);
}
.imu-setup-cmd-log-entry .cmd-dir { color: #6a8; }
.imu-setup-cmd-log-entry .cmd-resp { color: #a86; }
.imu-setup-cmd-log-entry .cmd-key { color: #8ab; }
/* Repeat counter — high-rate traffic (LED colour writes) folds into one row
   rather than evicting the connect handshake from the 80-row buffer. */
.imu-setup-cmd-log-entry .cmd-count { color: #666; }

/* Readout table */

/* Row-based readout (raw → polarity → calibrated) */

/* Polarity buttons */
  /* .as-io-capture-btn and .imu-setup-connect-btn wear the settings kit's
     .set-btn--sm now (round thirteen); their bespoke faces and the teal hover
     wash — rgba(122,188,188,.18-.25), which no other button in the app used —
     were deleted with them. .imu-setup-pol-btn keeps
     their base rules and their `.reversed` / `.active` ochre; only the teal
     hover went. */
.imu-setup-pol-btn {
  font: var(--fs-body)/1 var(--font-mono);
  width: 22px;
  height: 20px;
  background: rgba(122,188,188,0.1);
  color: rgba(122,188,188,0.8);
  border: 1px solid var(--tint-lock-medium);
  border-radius: var(--r-2);
  cursor: pointer;
  padding: 0;
  text-align: center;
  vertical-align: middle;
}
.imu-setup-pol-btn.reversed {
  color: #e8a65d;
  border-color: rgba(232, 166, 93, 0.3);
  background: rgba(232, 166, 93, 0.1);
}

/* Feed to sphere button */

/* Tare section */

/* Role dropdown in feed section */

.imu-setup-role-select {
  width: auto;
  min-width: 100px;
}


/* ══════════════════════════════════════════════════════════════════════════
   STAGING MODAL (experimental Change B — posture snapshot engine)
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Channel rows ──────────────────────────────────────────────────────── */

/* ── Snapshot table ────────────────────────────────────────────────────── */

/* Multiple values may overflow the channel-values area → let it scroll */

/* ── Readouts ─────────────────────────────────────────────────────────── */

/* ── Stream-out readouts ──────────────────────────────────────────────────── */

/* ── Posture map ──────────────────────────────────────────────────────────── */

/* Flash effect for snapshot rows when the map selects them — quick pulse so
   the performer can eyeball which table row matches the map sphere. */

@keyframes staging-snap-flash {
  0%   { background: rgba(255, 217, 138, 0.35); }
  100% { background: transparent; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   NARROW-WINDOW MODE
   (docs/archive/NARROW-LAYOUT-PLAN.md, docs/MULTI-INSTANCE-PLAN.md)

   The COLUMN TIERS are gone with the rig view (#291): they stepped the panel
   grid through 5/4/3/2 columns, and there is no panel grid — .right-panel is
   a hidden cabinet. What survives is the ≤700 rules that keep the bars, the
   modals and the tile screen's own rails usable in a small window.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 700px) {

  /* ── Shared ─────────────────────────────────────────────────────────────
     Bars wrap onto extra rows instead of clipping. flex-wrap alone is NOT
     enough: the button rows live inside align-items:flex-start columns
     (and the bottom groups have flex-shrink:0), so without a width cap
     they grow to content width and wrap never engages — the overflow just
     clips at the window edge. max-width + shrink give wrap a real edge to
     wrap against. Modals never exceed the window. */
  .top-bar,
  .top-bar-left,
  .top-bar-right,
  .top-bar-row {
    flex-wrap: wrap;
    max-width: 100%;
    min-width: 0;
    flex-shrink: 1;
  }
  /* Footer: fixed height at every width, mute + meters ALWAYS fully visible.
     Wrapping made it 129px tall (vs 84 wide), pushed the mute tile up and
     shoved the meters off the bottom edge. So: one row, never wrap; the left
     group is rigid; the sensor group takes the leftover and scrolls
     horizontally when it can't fit (~435px of buttons in ~175px of room). */
  .bottom-bar {
    flex-wrap: nowrap;    /* left group and sensor group stay side by side */
    align-items: center;
    overflow: visible;
    gap: 0.5rem;
  }
  .bottom-bar-left {
    flex: 0 0 auto;       /* mute tile + meter rail never shrink or wrap */
    flex-wrap: nowrap;
    overflow: visible;
    padding-right: 0;
  }
  /* Sensor group FOLDS DOWN like the top bar does — extra rows, nothing
     hidden. (An earlier pass scrolled it horizontally to keep the footer a
     fixed height; that trades a visible overflow for an invisible one, which
     is worse.) The footer grows upward; .main-layout gives up the space. */
  .bottom-bar-right {
    flex: 1 1 auto;
    min-width: 0;
    flex-wrap: wrap;
    justify-content: flex-end;
    align-content: center;
    row-gap: 0.3rem;
    overflow: visible;
  }
  .bottom-bar-right > * {
    flex: 0 0 auto;       /* buttons keep their size instead of being squeezed */
  }
  .mu-dialog {
    max-width: calc(100vw - 2rem);
  }
  /* min-width beats max-width in CSS, so these two would punch straight
     through the cap above (audio settings was 640px wide in a 480px window). */
  .audio-dialog,
  .viz-dialog {
    min-width: 0;
  }

  /* Sensor setup: the grid's hard 380px left track left the device-detail
     column ~78px wide — one word per line and the tare / connect buttons
     clipped off the edge. Stack it: device list first, full-width detail
     card under it, and let the dialog do the scrolling instead of each
     column scrolling inside a 70vh box. */
  
  
  
  .imu-setup-dialog {
    overflow-y: auto;
  }
  /* The staging table is 7 fixed tracks (~460px) — let it scroll sideways
     rather than crush every column. */
  

  /* ── Panel contents at ~2-column width (≈220px per column) ───────────────
     Everything below is about legibility at that width, not overflow —
     nothing clips, but several controls become unreadable or collide. */

  /* Patches: the 4-column grid truncates every name to "1 use…" / "26 shimm".
     Two columns keeps the names — which are the entire point of the panel. */
  /* Paired button rows (sweep|erase all, tare cursor|lock cursor) put icon,
     label and keycap badge in ~105px and they overlap. Stack them. */
  .seq-section--row {
    flex-direction: column;
  }

  /* Grain rows: recover slider length from the row gap and numbox padding
     rather than from the label column — narrowing the label wraps two-word
     labels ("dry gain", "fade out") onto two lines and grows the row instead. */
  .grain-row {
    gap: 0.35rem;
  }
  .grain-label {
    white-space: nowrap;
  }
  .grain-numbox {
    padding-left: 2px;
  }

  /* Segmented pickers (overflow off|oldest|nearest, dir ping-pong|fwd|rev)
     are three chips in one row — let them wrap rather than squeeze. */
  .grain-seg {
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--sp-0) var(--sp-1);
  }
}

/* ── 1-column tier (≤430px) ──────────────────────────────────────────────────
   Below this, two ~170px columns squeeze sliders to a few pixels and clip the
   third chip off every segmented picker. One full-width column keeps every
   control usable right down to the 380px Electron minimum. */
@media (max-width: 430px) {
  
  /* Below this the sensor group only gets ~75px beside the meter rail, so it
     folds to one button per row. Let the footer break instead: sensor group
     drops to its own full-width line under the mute + meters, where the same
     buttons fit 2–3 per row. Left group still never wraps or shrinks. */
  .bottom-bar {
    flex-wrap: wrap;
    justify-content: flex-start;
  }
  .bottom-bar-right {
    flex: 1 1 100%;
    justify-content: flex-start;
  }
}

/* ── Boot veil ───────────────────────────────────────────────────────────────
   Paired with the bootstrap script in index.html <head>. main.js is a module,
   so it applies the persisted UI scale only AFTER the document has painted —
   a visible reflow before the layout settles. Hide the moving part until it
   has.

   `booting-reveal` exists so the fade transition is only ever attached for the
   ~200ms it is used, then removed. .main-layout wraps the canvas, and the
   render loop is timing-sensitive — no permanent compositing layer over it. */
html.booting .main-layout {
  opacity: 0;
}
html.booting-reveal .main-layout {
  transition: opacity 0.18s ease;
}
@media (prefers-reduced-motion: reduce) {
  html.booting-reveal .main-layout { transition: none; }
}

/* ── mubone instrument panel ─────────────────────────────────────────────── */
/* The panel is settings-kit rows (docs/SETTINGS-GUI.md): its fields, buttons,
   badges and readouts are .set-* and defined there. What stays here is the
   handful of things the kit has no element for, because they READ the
   instrument rather than set it — a link light, a note the panel writes for
   itself, and the three hardware buttons. */

.syg-num { font-variant-numeric: tabular-nums; }

/* The link light. Four states rather than a colour on the text, because the
   status word beside it is the instrument's own vocabulary ("No IP", "Bad
   auth") and reads better plain. */
.syg-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--text-dimmer);
}

.syg-dot.good    { background: var(--accent-sweep); }
.syg-dot.bad     { background: var(--accent-danger); }
.syg-dot.off     { background: var(--text-dimmer); }
.syg-dot.working { background: var(--accent-lock); animation: syg-pulse 1.1s ease-in-out infinite; }

.syg-notes { display: flex; flex-direction: column; gap: var(--sp-2); margin-bottom: var(--sp-5); }
.syg-notes:empty { display: none; }

.syg-note {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-size: var(--fs-set-hint);
  line-height: 1.45;
  text-wrap: pretty;
}

@keyframes syg-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}

.syg-buttons { display: flex; align-items: center; gap: var(--sp-2); }

.syg-button-dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  border: 1px solid var(--border-soft);
  background: transparent;
  transition: background 60ms linear;
}

.syg-button-dot.on {
  background: var(--accent-action);
  border-color: var(--accent-action);
}

/* ════════════════════════════════════════════════════════════════════════
   THE SCREEN
   Chrome, full-bleed sphere, pinned rail, palette, toolbox, footer. Layout per
   docs/mockups/brush-model-ui.html (s-six), tokens the app's own.
   js/tile-layout.js.

   This used to be `body.tile-layout`, one of two layouts you could flip
   between. The other one is gone (#291), so the prefix went with it — every
   rule below is unconditional. `.top-bar` and `.right-panel` stay in the DOM
   as the RIG CABINET: hidden controls this screen writes through. See the
   comment at the top of index.html before deleting anything in them.
   ════════════════════════════════════════════════════════════════════════ */

  .top-bar      { display: none; }
  .right-panel  { display: none; }
  
  /* The canvas HUD (coordinates, patch name, commit dots, buffer count) is
     panel-era chrome — the tile chrome carries what survives of it, and the
     alt-lock indicator moved into tcStats. The sphere stays bare. */
  body .hud          { display: none; }

  /* Three sections, 1fr / auto / 1fr, so the name sits on the TRUE centre of
     the window and not wherever the side groups leave it. The footer uses the
     same grid and the two centres line up down the screen. */
  /* The title bar sits TALLER than the footer (Ek, 2026-08-29). The footer is
     a monitor strip — dense, read at a glance, packed to its edges. The header
     is where the instrument says its own name, and giving it more air than the
     thing at the other end is what makes it read as the top of a window rather
     than a second strip. 72px against the footer's 65. */
  body .tc-bar {
    display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
    gap: 0.55rem; padding: 1.25rem 0.55rem;
    background: var(--bg-app); border-bottom: 1px solid var(--border-faint);
    font-family: var(--font-sans);
  }
  .tc-side { display: flex; align-items: center; gap: 0.3rem; min-width: 0; }
  .tc-side-l { justify-self: start; }
  .tc-side-r { justify-self: end; }
  .tc-brand { display: flex; align-items: baseline; gap: 0.45rem; justify-self: center; }
  /* The row aligns on the BASELINE, which is right for the wordmark and the
     version but wrong for a square glyph: sitting a square on a baseline puts
     its optical centre half its height above the word's, and it reads as
     floating. `align-self: center` against the brand box — which is exactly the
     wordmark's own box — makes the two centres identical by construction rather
     than by a nudge that has to be re-tuned whenever the type scale moves. A
     first attempt DID use a nudge, `margin-bottom: -0.18em`, and measured 1.44px
     low; audit:align reads both centres now. */
  .tc-mark { width: 18px; height: 18px; align-self: center; flex: none; display: block; }
  .tc-brand b { font-size: var(--fs-control); font-weight: 700; letter-spacing: -0.02em; color: var(--text-light); }
  .tc-brand i { font-style: normal; font-size: var(--fs-nano); letter-spacing: 0.1em;
    text-transform: uppercase; color: var(--text-faint); }
  
  /* Chrome groups: related controls sit together with a hairline between
     groups, so ten buttons read as four ideas (#256). */
  .tc-grp { display: flex; align-items: center; gap: 0.3rem; }
  /* The mute state reads at a glance and must not be mistaken for an action. */
  
  /* ── ONE HAIRLINE PER DRAWER, AND NOWHERE ELSE (Ek, 2026-09-13) ────────
     "there should be no divider lines in the header other than beside the
     drawer icons." A rule between two clusters of glyphs was drawing a
     distinction the SPACING already draws; beside a drawer handle it means
     something — that side of the line is a panel, not a control. So the
     default is no border, and exactly two are put back: the group after the
     TOOLS handle, and the PINNED handle's own group. */
  .tc-grp + .tc-grp { margin-left: 0.55rem; padding-left: 0.55rem; border-left: 0; }
  .tc-grp:has(#tcTools) + .tc-grp,
  .tc-grp:has(#tcPinned) { border-left: 1px solid var(--border-faint); }

  /* ── The camera, one icon and a menu (Ek, 2026-09-13) ──────────────────
     It was a three-segment capsule: three slots of a bar that is otherwise
     glyphs end to end, spent on a choice made a few times a set. The button
     is the CURRENT mode in the chrome's own 32px glyph; the menu under it is
     where you change it, and it is the one place the three modes carry their
     WORDS — three glyphs alone were learnable only from a tooltip. Flat, one
     hairline, no gradient, no shadow beyond the one the strip already uses. */
  .tc-cam-wrap { position: relative; }
  /* `display: flex` here BEATS the browser's own `[hidden] { display: none }`
     — an author rule outranks the UA sheet — so the menu rendered open at all
     times while `menu.hidden` was perfectly true (Ek, 2026-09-13: "right now
     the box is always open"). The probe that passed this was reading the
     PROPERTY, not the screen, which is the trap DESIGN-SYSTEM § 3 is about. */
  .tc-cam-menu[hidden] { display: none; }
  .tc-cam-menu {
    position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
    display: flex; flex-direction: column; gap: var(--sp-0);
    min-width: 132px; padding: var(--sp-2);
    background: var(--bg-panel); border: 1px solid var(--border-subtle);
    border-radius: var(--r-2);
  }
  /* 24, the kit's height (Ek ruled 2026-09-15: snap them all). It was 30.9 —
     padding plus a line box around a 20px glyph, a number nobody chose. This is
     a MENU row and the comment in tile-layout.js calls it the rail's own row
     model, so it takes the row's height: the same 24 as `.trow`. The padding is
     horizontal only now, because `height` carries it. */
  .tc-cam-row {
    display: flex; align-items: center; gap: 0.5rem;
    height: 24px; box-sizing: border-box;
    padding: 0 0.45rem; border: none; border-radius: var(--r-1);
    background: none; cursor: pointer; text-align: left;
    font-family: var(--font-sans); font-size: var(--fs-control);
    letter-spacing: var(--ls-tight); color: var(--text-subtle);
    transition: color var(--trans-base), background var(--trans-base);
  }
  .tc-cam-row svg { width: 20px; height: 20px; flex: none; }
  .tc-cam-row:hover { background: var(--surface-1); color: var(--text-bright); }
  /* ON is the row that IS the mode — the rail's mark, not a dimming of the
     other two (DESIGN-SYSTEM: never dim to mean anything). */
  .tc-cam-row.on { color: var(--text-light); background: var(--surface-2); }
  .tc-cam-row:disabled { color: var(--text-faint); cursor: default; background: none; }

  
  
  /* Empty unless alt-lock is on, so it takes no room in the ordinary bar. */
  /* ── The recording budget (2026-09-13) ─────────────────────────────────
     A track and a fill, flat, no gradient, no text until there is something
     to DO about it. It reads at a glance and it is the last thing that was
     invisible: startLiveRecording refuses at the ceiling, and every warning
     it had went to `#vmBuffers` inside the `display:none` HUD, so the
     instrument stopped recording in silence (the 14-minute play test walked
     into it at 13.5 minutes). 44px so it reads as a gauge and not a slider —
     nothing here is draggable. */
  /* A BAR YOU CAN SEE, AND A NUMBER (Ek, 2026-09-14: "add a number beside the
     recording memory bar, and make that bar a bit clearer i can't see it").
     It was 44 × 3px of --surface-2 under a --text-dim fill: at rest that is a
     hairline the same value as the bar's own ground, which is why an EMPTY
     budget read as nothing at all. 5px tall, the track a step brighter, and
     the fill --text-secondary so the level is legible before it is urgent.
     The gauge still cannot be dragged — nothing in this bar can. */
  /* WHAT THE EYE SPACES IS THE INK, NOT THE BOX (Ek, 2026-09-14: "the spacing
     to the left of the recording bar is not equal like others"). Every icon
     beside it is a 13.3px glyph centred in a 32px button, so two neighbours
     leave ~24px of air between their drawings while the flex gap is 4.8px.
     The gauge has no button around it, so the same 4.8px gap put its edge
     14.1px from the garbage can — measurably tighter than every other pair in
     the run. 10px of margin brings its ink to 24.1px, which is the run's own
     figure (23.4 · 23.9 · 25.0 · 25.3 — they vary because glyph WIDTHS differ
     once their heights are matched). */
  .tc-rec-wrap {
    display: inline-flex; align-items: center; gap: 0.35rem; flex-shrink: 0;
    margin-left: 10px;
  }
  .tc-rec {
    display: inline-block; width: 44px; height: 5px; flex-shrink: 0;
    border-radius: var(--r-1);
    background: var(--surface-3); overflow: hidden; vertical-align: middle;
  }
  .tc-rec-fill {
    display: block; height: 100%; width: 0%;
    background: var(--text-secondary); border-radius: var(--r-1);
    transition: width var(--trans-base), background-color var(--trans-base);
  }
  /* m:ss RECORDED — the bar says what fraction of the budget that is, so the
     two never restate each other. Tabular, so the number does not jitter the
     icons beside it as it counts. It takes the fill's colour at the two
     thresholds, because by then it is the thing to read. */
  .tc-rec-num {
    font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow);
    color: var(--text-dim); font-variant-numeric: tabular-nums;
    min-width: 2.4em; text-align: left;
  }
  .tc-rec-num.warn { color: var(--accent-warn); }
  .tc-rec-num.crit { color: var(--accent-danger); }
  .tc-rec-fill.warn { background: var(--accent-warn); }
  .tc-rec-fill.crit { background: var(--accent-danger); }

  .tc-stats { font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow); text-transform: uppercase;
    color: var(--text-dim); font-variant-numeric: tabular-nums; }
  .tc-stats:not(:empty) { margin-left: 0.35rem; }
  .tc-stats b { color: var(--text-secondary); font-weight: 500; }

  /* ── ACTION vs TOGGLE vs STATE (#267) ──────────────────────────────────
     A rounded RECTANGLE is an action: press it, it happens, it returns to
     rest. The same rectangle with a state dot is a toggle: it stays on. A
     divided CAPSULE (.seg-pill) is a choice among states. One shape, one
     meaning — before this the capsule was all three and shape said nothing. */
  /* ── The bare glyph (2026-08-29) ────────────────────────────────────────
     A browser toolbar's four unlabelled icons carry its whole history model
     and nobody needs the words (Ek's reference). A box around each would be
     four boxes saying "this is a button" next to four shapes that already do.
     The hit target is still 30px — the box is what goes, not the target. */
  /* ONE icon size for the whole main page (2026-08-29) — the chrome's
     history glyphs, system mute, the three cursor mutes and dry are all the
     same object at the same weight, so none of them reads as more or less
     important than the others by accident. */
  .tc-icon {
    display: flex; align-items: center; justify-content: center;
    width: 32px; height: 32px; flex: none;
    background: none; border: none; border-radius: 4px; padding: 0;
    color: var(--text-dim); cursor: pointer;
    transition: color 0.14s, background 0.14s;
  }
  .tc-icon svg { width: 20px; height: 20px; display: block; }
/* ── ONE OPTICAL SIZE FOR EVERY MAIN-PAGE GLYPH (Ek, 2026-09-14) ───────────
   "check all the icons on the main page they should be the same size, use the
   garbage/sweep area as the reference size."

   The BOXES were already one size — 32px button, 20px svg, and the comment
   above says so — but the box is not what the eye reads: the DRAWING inside
   it is, and those were never brought to one size. Measured (drawn extent,
   the larger of width and height, in px):

     undo · redo        11.7      cam (steer)      11.5
     sweep              12.3      cam (surface)    14.0
     GARBAGE            13.3      cam (sensor)     16.5
     tools · pinned     15.0      mic              15.0
     sensor             16.5      COG              18.3

   So the cog was drawn half again the size of the broom beside it, and the
   camera button changed size when you changed camera mode. The footer's
   glyphs sit in a 24px svg rather than 20px and ran 15.9 → 18.0, bigger again
   than anything in the bar.

   `--gk` brings each glyph to the garbage can's 13.3px: a plain scale about
   the element's centre, which also pulls an off-centre drawing toward the
   middle of its box. The stroke is compensated in the same rule so weight
   does NOT scale with it — every main-page glyph renders at 1.5px, which is
   what the chrome's 1.8-at-20px already was. Nothing here changes a path;
   the factors are derived from the bboxes above, so a REDRAWN glyph needs a
   new factor and align-audit's "one optical size" check is what catches it. */
  /* Scaled INSIDE the viewBox, never on the svg element: a transform on the
     element shrinks its border box with it, and the footer's "every glyph on
     one top edge" invariant reads those boxes — it broke by 1.1px the first
     time this was written that way. `transform-box: view-box` puts the origin
     in user units, so every glyph scales about the centre of its own 24-unit
     square and the boxes stay identical. */
  .tc-icon > svg > *, .tc-readout > svg > * {
    transform: scale(var(--gk, 1));
    transform-box: view-box; transform-origin: 12px 12px;
  }
  .tc-icon > svg, .tc-readout > svg { stroke-width: calc(1.8 / var(--gk, 1)); }
  #tcTools, #tcPinned, #tcMic, #tcLearn { --gk: 0.887; }
  #tcUndo, #tcRedo            { --gk: 1.140; }
  #tcSweep                    { --gk: 1.078; }
  #tcClear                    { --gk: 1.000; }   /* the reference */
  .tc-sens                    { --gk: 0.887; }   /* every sensor chip (tile-layout.js adds them beside #tcSensor): the device body is 18 units tall → 13.3px */
  #tcSettings                 { --gk: 0.725; }
  /* The camera button wears whichever mode is on, and the three glyphs are
     13.8 / 16.8 / 19.8 units wide — so the factor is the GLYPH's, not the
     button's (tile-layout.js writes the class). */
  #tcCamBtn > svg.cam-g--steer   { --gk: 1.157; }
  #tcCamBtn > svg.cam-g--surface { --gk: 0.950; }
  #tcCamBtn > svg.cam-g--sensor  { --gk: 0.806; }
  /* The footer's cycle glyphs: 24px svg, so the stroke divisor is 1.5, and a
     button whose faces differ in extent gets a factor per face. */
  .bb-ax > svg > *, .bb-mute > svg > *, .bb-dry > svg > * {
    transform: scale(var(--gk, 1));
    transform-box: view-box; transform-origin: 12px 12px;
  }
  .bb-ax > svg, .bb-mute > svg, .bb-dry > svg { stroke-width: calc(1.5 / var(--gk, 1)); }
  #tcDryCycle                    { --gk: 0.739; }
  #tcReverb                      { --gk: 0.782; }   /* 17.0 → 13.3, measured by align-audit */
  #tcMute .bb-mute-live          { --gk: 0.796; }
  #tcMute .bb-mute-off           { --gk: 0.719; }
  #tcZeroHeading, #tcCursorLock  { --gk: 0.831; }
  #tcAzCycle, #tcElCycle         { --gk: 0.782; }
  .tc-icon:hover { color: var(--text-bright); background: var(--surface-1); }
  .tc-icon:active { color: var(--text-bright); background: var(--surface-3); }
  .tc-icon.tc-dis { color: var(--text-faint); pointer-events: none; }
  .tc-icon.tc-danger:hover { color: var(--accent-action, #f26415); }
  /* A drawer handle lights while its drawer is open — the one piece of state
     these carry, and the reason they are not plain actions. */
  .tc-drawer.on { color: var(--text-bright); background: var(--surface-2); }
  /* ── The glyph readout (2026-09-09) ────────────────────────────────────
     "Is it true?" as a glyph: the same 32px object as .tc-icon so the bar is
     glyphs end to end, with a 6px dot at the glyph's foot carrying the state
     in the four colours the pill used. Nothing here is a switch — the click
     is the pill's shortcut to the page where the value is set, and the
     hover lifts the ground one step and nothing else, exactly as before. */
  .tc-readout {
    position: relative; display: flex; align-items: center; justify-content: center;
    width: 32px; height: 32px; flex: none;
    background: none; border: none; border-radius: 4px; padding: 0;
    color: var(--text-subtle); cursor: pointer;
    transition: color 0.14s, background 0.14s;
  }
  .tc-readout svg { width: 20px; height: 20px; display: block; }
  .tc-readout:hover { background: var(--surface-1); }
  .tc-readout > i {
    position: absolute; right: 3px; bottom: 3px;
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--text-faint);
    box-shadow: 0 0 0 2px var(--bg-app);
    transition: background var(--m-state) var(--ease-out);
  }
  .tc-readout.on, .tc-readout.live { color: var(--text-secondary); }
  .tc-readout.on    > i { background: var(--accent-lock); }
  .tc-readout.live  > i { background: var(--mic-live-border); }
  .tc-readout.found > i { background: var(--accent-warn); }
  .tc-readout.lost  > i { background: var(--accent-danger); }
  /* The chrome's glyph segment: each segment is the bar's own 32px object
     with a 20px glyph — the same numbers as .tc-icon and .tc-readout (Ek,
     2026-09-10: "the 3 way pill … should be the same big size as the other
     icons on the header row … all the icons in the header row should be the
     same size"). It was a 24px capsule of 16px glyphs, the one smaller thing
     on the row. 28px segments inside the pill's 2px padding make 32. */
  .tc-bar .seg-pill.seg-glyph .grain-seg-btn { display: inline-flex; align-items: center; justify-content: center; height: 28px; padding: 0 var(--sp-3); }
  .tc-bar .seg-pill.seg-glyph .grain-seg-btn svg { width: 20px; height: 20px; display: block; }
  /* The unavailable face (§ .tc-dis): the sensor segment with no sensor up. */
  .tc-bar .seg-pill .grain-seg-btn:disabled { color: var(--text-faint); cursor: default; pointer-events: none; }

/* .mu-pill, the 18px word pill with a dot, was the rig readout from round
   twenty-five to 2026-09-09; the two readouts are .tc-readout now (above) and
   the pill has no users. git history has it. */
/* The footer zero button's outcome, as colour (#319): sage = heading zeroed,
   ember = there was no cursor sensor to zero. Reuses the app's two flash
   meanings; cleared by the JS after 900ms. */
.bb-ax.sweep-flash { color: var(--accent-sweep); }
.bb-ax.flashing    { color: var(--accent-action); }

  .tc-btn {
    display: flex; align-items: center; gap: 0.4rem; font-family: var(--font-sans);
    font-size: var(--fs-control); letter-spacing: 0.06em; text-transform: uppercase;
    background: var(--surface-0); border: 1px solid var(--border-faint);
    color: var(--text-dim); border-radius: 3px; padding: 0.26rem 0.62rem; cursor: pointer;
  }
  .tc-btn:hover { border-color: var(--border-soft); color: var(--text-secondary); }
  .tc-btn:active { background: var(--surface-3); color: var(--text-bright); }
  /* The way back to the old view is the quietest thing in the bar — it is
     on its way out (#255). */
  

  /* The rig footer's nav chips (sensors · feedback · gesture · mapping ·
     staging · accessory) are every one of them a nav item in Settings now
     (#255), so on this screen they were a second navigation language
     competing with the chrome. The bottom bar keeps only what is LIVE
     STATE — the meters and the sensor/OSC status readout. */
  body #sensorGroup .top-bar-btn { display: none; }
  /* Mute is a chrome pill on this screen, so the bottom bar's copy is a
     second control for one thing — and the loud red box was the most
     eye-catching object on a screen whose subject is the sphere (#256).
     What is left down there is only readouts: meters and sensor status. */
  body .bottom-bar-mute { display: none; }
  /* The footer is audio + sensor (#263): meters left, the cursor's two source
     pickers centre, sensor/OSC status right. */
  .bottom-bar-cursor { display: none; }
  /* ── THE FOOTER (#265) ─────────────────────────────────────────────────
     An at-a-glance monitor for the system and its state, built from the same
     vocabulary as the tool params: one eyebrow caption per group, hairline
     rules between groups, mono tabular values, one line, everything centred
     on it. It had grown four label styles and no grouping; this is one.
     The top border matches the header's bottom border exactly. */
  /* Same three-section grid as the chrome (2026-08-29), so the two centres
     line up: sensor and cursor left, the meters dead centre, what you set and
     the mute you reach for on the right. */
  body .bottom-bar {
    display: grid; grid-template-columns: 1fr auto 1fr; align-items: stretch;
    gap: 0; padding: 0;
    border-top: 1px solid var(--border-faint);
  }
  body .bb-side { display: flex; align-items: stretch; min-width: 0; }
  /* Explicit order: the right side comes FIRST in the markup, because the
     audio group and the sensor group each had to stay where their modules
     expect to find them. A grid places by order-modified document order, so
     the three columns are declared here rather than reshuffling the DOM. */
  body .bb-side-l { justify-self: start; order: 1; }
  body .bottom-bar > .bottom-bar-levels { order: 2; }
  body .bb-side-r { justify-self: end; order: 3; padding-right: 0.5rem; }
  /* Every group is the same shape: caption, then contents, then a rule. */
  /* :not(.bb-mute) — the group padding on a 32px icon button leaves 3px of
     content box and the glyph collapses to a sliver. The mute is a control in
     the side, not a group of them. */
  body .bottom-bar .bb-side > *:not(.bb-mute),
  body .bottom-bar > .bottom-bar-levels {
    display: flex; align-items: center; gap: 0.55rem;
    padding: 0.5rem 0.9rem; margin: 0;
  }
  /* ── Reading order (#287) ───────────────────────────────────────────────
     Left: what is driving the cursor and whether the sensor is there — the
     two things you check before you play. Middle: the audio you set and
     forget. Far right: the meters, which is where a mixer puts them and
     where the eye goes to answer "is it making sound".

     Done with `order`, not by moving the markup: the rig view shares this
     footer and keeps its own arrangement, and every module still finds these
     controls by id. Consequence worth naming — `+ *` follows DOM order, not
     visual order, so the separator cannot be a sibling rule any more. Every
     group carries its own left border and the first one visually drops it. */
  /* No rules between the groups (2026-08-29, Ek). Three sections on a grid
     already separate themselves by position and by the space around them;
     hairlines on top of that drew boxes nobody needed to see. */
  /* Sensor reads first on the left (Ek) — the cursor's two pickers follow. */
  body .bottom-bar-right   { order: 1; }
  body .bottom-bar-cursor  { order: 2; }
  /* The old bar's mute lives in the cabinet: #tcMute replaced it. */
  body .bottom-bar-left    { display: none; }
  /* The levels are their own group in the strip and part of the left group
     in the rig view — `display: contents` outside the tile layout so the rig
     view sees exactly the markup it always had. */
  .bottom-bar-levels { display: contents; }
  body .bottom-bar-levels {
    display: flex; align-items: center; gap: 0.55rem; padding: 0.5rem 0.9rem; }
  /* ── Mute: one icon, two states (2026-08-29) ────────────────────────────
     Bare like the chrome's glyphs, and the last thing on the right because it
     is the control you find without looking. Amber when muted — the only
     place on this screen where colour means "something is stopped". */
  .bb-mute { display: none; }
  body .bb-mute {
    display: flex; align-items: center; justify-content: center;
    width: 32px; height: 32px; flex: none; align-self: center;
    margin: 0 0.9rem 0 0.5rem;
    background: none; border: none; border-radius: 4px; padding: 0;
    /* THE SAME VALUE AS THE CHROME'S GLYPHS (Ek, 2026-09-14: "the brightness
       of the stuff in the footer bar is not the same as the top, match
       top"). Measured: the bar's icons are --text-subtle (#9d978e, luma 152)
       and the footer's were --text-dim (#776f66, luma 112) — 40 luma apart,
       which read as two families rather than one instrument. Their captions
       already matched at #938d83, which is why only the glyphs were off. */
    color: var(--text-subtle); cursor: pointer; transition: color 0.14s, background 0.14s;
  }
  body .bb-mute svg { width: 20px; height: 20px; display: block; }
  body .bb-mute:hover { color: var(--text-light); background: var(--surface-1); }
  body .bb-mute .bb-mute-off { display: none; }
  body .bb-mute.muted { color: var(--accent-action, #f26415); }
  body .bb-mute.muted .bb-mute-live { display: none; }
  body .bb-mute.muted .bb-mute-off  { display: block; }

  /* ── The cycle button (2026-08-29) ──────────────────────────────────────
     One icon, click to step through its states — the axis mutes and dry. The
     glyph says WHICH thing; how it is drawn says which state:
       free / on   solid, quiet
       mute / off  solid with a slash, in the mute amber
       map / auto  dashed or pipped, in the on-state teal
     Only one of the three faces is in the flow at a time. The word under it
     is the smallest label on the screen — it names the axis, and you stop
     reading it after the first session. */
  .bb-ax { display: none; }
  body .bb-ax {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: var(--seam); width: 40px; flex: none; align-self: center;
    background: none; border: none; border-radius: 4px; padding: var(--sp-0) 0;
    color: var(--text-subtle); cursor: pointer; transition: color 0.14s, background 0.14s;   /* the chrome's value — see .bb-mute */
  }
  body .bb-ax svg { width: 20px; height: 20px; display: block; }
  body .bb-ax i { font-style: normal; font-size: var(--fs-nano); letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--text-secondary); line-height: 1; }
  body .bb-ax:hover { color: var(--text-light); background: var(--surface-1); }
  body .bb-ax .ax-mute, body .bb-ax .ax-map { display: none; }
  body .bb-ax.is-mute { color: var(--accent-action, #f26415); }
  body .bb-ax.is-mute .ax-free { display: none; }
  body .bb-ax.is-mute .ax-mute { display: block; }
  body .bb-ax.is-map  { color: var(--grid-teal, #86b2ab); }
  body .bb-ax.is-map .ax-free { display: none; }
  body .bb-ax.is-map .ax-map  { display: block; }
  /* Dry's own row inside the borrowed audio device is what the icon drives,
     so the row itself would be the same control twice. */
  body #tcAudioSlot #apDryRow { display: none; }

  /* The group caption — the same eyebrow as a params section header. */
  .bb-cap { display: none; }
  body .bb-cap {
    display: block; flex: none;
    font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow);
    text-transform: uppercase; color: var(--text-secondary);
  }
  /* A control's own label, quieter than the group caption. */
  
  body #tcAudioSlot .grain-label,
  body .bottom-bar-label {
    font-size: var(--fs-meta); letter-spacing: 0.05em; text-transform: lowercase;
    color: var(--text-faint); font-weight: 400;
  }
  /* Values are numbers you compare, so they are mono and tabular — the same
     rule the properties rows follow. */
  body .bottom-bar .grain-numbox,
  body .bottom-bar .as-val,
  body .bottom-bar .sensor-group-status {
    font-family: var(--font-mono, monospace); font-size: var(--fs-meta);
    font-variant-numeric: tabular-nums; color: var(--text-secondary);
  }
  /* Audio is the group that gives when the window is narrow — it has the
     most in it and its controls are the least glanceable. Cursor and sensor
     hold their size, because a picker you cannot see is a picker you cannot
     read (#265). */
  body .bottom-bar-audio {
    font-family: var(--font-sans);
    min-width: 0; flex: 0 1 auto; overflow-x: auto; overflow-y: hidden;
  }
  body .bottom-bar-audio::-webkit-scrollbar { height: 0; }
  body .bottom-bar-cursor { flex: 0 0 auto; font-family: var(--font-sans); }
  /* The rail already says "sensor" as its caption. */
  body .bottom-bar .sensor-group-label { display: none; }
  body .bottom-bar .osc-indicator { font-size: var(--fs-meta); }
  body .bottom-bar-left { flex: 0 0 auto; }
  /* The meter rail: bars sit on one baseline with their names beneath, so the
     rail reads as one instrument rather than five labelled boxes. */
  body .bottom-bar-meters { gap: 0.7rem !important; align-items: flex-end; }
  body .bottom-bar-meters .levels-meter-col {
    flex-direction: column-reverse; align-items: center; gap: var(--sp-2); }
  /* The `!important` is here to beat the settings pages' own meter styles —
     the same .as-multi-meter-wrap markup is rendered in both places. It has to
     stay, so the NUMBER comes from the footer's row instead of being a second
     opinion about it (see section 26): one row height, and the meter is what
     is left after the caption and its gap. */
  body .bottom-bar-meters .as-multi-meter-wrap { height: var(--footer-meter, 34px) !important; }
  body .bottom-bar-gate .main-gate-meter { height: var(--footer-meter, 34px) !important; }
  /* ── The segmented PILL (#266) ─────────────────────────────────────────
     Same shape and the same reading as a chrome pill: a rounded capsule with
     a hairline, the chosen segment filled. A three-way choice is one control,
     so it gets one outline rather than three boxes side by side. */
  /* ── The segmented control, flattened (#276) ───────────────────────────
     Ek: "the pill might be too intense, it has a 3D vibe." The bevel came
     from stacking three cues — an outer BORDER, a recessed track, and a
     RAISED chip for the active segment. Any two of those read as depth.
     The shape stays a capsule, because that is what says "pick one of N"
     (#267) and params must not go square while the chrome stays round —
     shape would stop meaning anything. What goes is the weight: no border,
     and an active segment that is a FLAT tinted fill rather than a lighter
     surface sitting on top. One definition, so the chrome, the footer, the
     settings pages and the engine params are the same control. */
  .seg-pill,
  .tc-prail .opt .seg, .tc-lrail .opt .seg, .tc-rail .opt .seg,
  .settings-host .in-settings .grain-seg {
    display: inline-flex; align-items: center; gap: var(--seam);
    background: var(--surface-1); border: none;
    border-radius: 999px; padding: 2px; overflow: hidden;
  }
  /* The instrument's chrome segment keeps its 999. Round twenty-five set this
     shared rule to 6px meaning only the settings window's segmented, but the
     rule covers three selectors and two of them are chrome — so the footer, the
     rail and the engine sheet lost their pill for a round. The settings one is
     split out below. */
  .seg-pill .grain-seg-btn,
  .tc-prail .opt .seg span,
  .tc-lrail .opt .seg span, .tc-rail .opt .seg span {
    border: none; background: none; border-radius: 999px;
    font-family: var(--font-sans); font-size: var(--fs-meta, 0.6rem);
    letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-faint); padding: 0.16rem 0.55rem; cursor: pointer;
    transition: color 0.12s, background 0.12s;
  }
  /* A SEGMENT IS 24 TALL (Ek, 2026-09-22: "the pill toggles for the cursor
     behaviours … don't press easily, it was also a problem before it moved").
     Measured: each option was 28.6 x 19. The shared pill rule above sizes a
     button from its CONTENT — fine for the chrome's text segments, which come
     out at the kit's height on their own — but these hold a 15.2px glyph and
     0.16rem of padding, so they landed five pixels under it. The build sheet's
     pill variant is explicit: BUTTONS 24, group 28, "because the 2px that makes
     it a pill is not part of a segment". Stated here rather than left to fall
     out of a font size, which is what put it at 19.

     AND THE CONTAINER GIVES UP ITS PADDING, which is the other half. A PILL is
     buttons 24 inside a group of 28, and 28 does not fit here: `.tc-lrail .mrow`
     is 30 with `--sp-1` either side, a 24px content box, and the 30 pitch is a
     ruling of its own. So this is the kit's PLAIN segmented instead — "24 tall,
     container AND buttons, no padding" — which is exactly 24 and fits. It keeps
     the pill's radius; only the geometry changes. With padding the group clipped
     its own 24px buttons back to 20 (`overflow: hidden`), taking the hit area
     with it, so this is what makes the taller button real rather than drawn. */
  .tc-prail .opt .seg,
  .tc-lrail .opt .seg, .tc-rail .opt .seg { padding: 0; }
  .tc-prail .opt .seg span,
  .tc-lrail .opt .seg span, .tc-rail .opt .seg span {
    height: 24px; box-sizing: border-box;
    display: inline-flex; align-items: center; justify-content: center;
  }
  /* Settings scope only: r6 inner against the kit's r8 outer (SETTINGS-GUI § 3).
     Split out of the shared rule above so the chrome keeps its pill. */
  .settings-host .in-settings .grain-seg .grain-seg-btn {
    border: none; background: none; border-radius: 6px;
    font-family: var(--font-sans); font-size: var(--fs-meta, 0.6rem);
    letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-faint); padding: 0.16rem 0.55rem; cursor: pointer;
    transition: color 0.12s, background 0.12s;
  }
  /* ── .seg-pill is the CHROME-DENSITY segment ───────────────────────────
     Same control as the plain segment, at a second density — footer, tool
     rail, engine sheet, where a bordered box adds one line too many. The panel
     gets the bordered one. Two densities of one control, exactly like the two
     sliders and the two switches in DESIGN-SYSTEM § 5.

     Round twenty-one gave the three axis pickers r2 on the reasoning that a
     pill means "you cannot set this". That was ruled on nine elements without
     looking at #apDryEnableSeg, which is a pill in the FOOTER that you set.
     Reverted (round twenty-four).

     The vocabulary does not actually collide: a pill WITH SEGMENTS is a
     control; a pill with a dot and one label is a readout. Nobody confuses
     those, so the shape was never lying. */

  /* The chosen one: flat, and tinted by the surface's own accent where there
     is one (the engine pages set --c on the panel root), neutral elsewhere. */
  .seg-pill .grain-seg-btn.active,
  .tc-prail .opt .seg span.on,
  .tc-lrail .opt .seg span.on
  .settings-host .in-settings .grain-seg .grain-seg-btn.active {
    background: var(--surface-4); color: var(--text-emphasis);
  }
  .tc-prail .opt .seg span.on,
  .tc-lrail .opt .seg span.on, .tc-rail .opt .seg span.on {
    background: color-mix(in srgb, var(--c) 22%, transparent);
    color: color-mix(in srgb, var(--c) 70%, var(--text-bright));
  }
  .seg-pill .grain-seg-btn:hover,
  .tc-prail .opt .seg span:hover,
  .tc-lrail .opt .seg span:hover, .tc-rail .opt .seg span:hover { color: var(--text-secondary); }
  /* An ICON segment (2026-09-03): a glyph in place of the caps word where the
     glyph is the thing itself — link / unlink, the wide and spot lens marks
     for area / nearest, and so on (SEG_ICONS in tiles.js). Tighter than a
     word, and its name moves to the title. */
  .tc-prail .opt .seg span.seg-ico,
  .tc-lrail .opt .seg span.seg-ico, .tc-rail .opt .seg span.seg-ico { display: inline-flex; align-items: center; padding: 0.12rem 0.42rem; }
  .tc-prail .opt .seg span.seg-ico svg,
  .tc-lrail .opt .seg span.seg-ico svg, .tc-rail .opt .seg span.seg-ico svg { width: 0.95rem; height: 0.95rem; display: block; }
  /* ── Air around the chips (#282) ──────────────────────────────────────
     The shared definition is sized for the chrome and the footer, where the
     constraint is horizontal space. In the properties rail there is room, and
     a capsule packed to its text reads as cramped next to rows of open track.
     Scoped so the footer, which was compacted on purpose, keeps its size. */
  .tc-prail .opt .seg,
  .tc-lrail .opt .seg, .tc-rail .opt .seg,
  .tc-prail .seg-pill { padding: var(--sp-1); }
  .tc-prail .opt .seg span,
  .tc-lrail .opt .seg span
  .tc-prail .seg-pill .grain-seg-btn { padding: 0.24rem 0.72rem; }
  /* Ducked auto-monitor: the setting is still auto, the sound is off. */
  .seg-pill.ducked .grain-seg-btn.active { opacity: 0.5; }

  /* Footer numbers behave like the properties rows: click and type. */
  body .bottom-bar .grain-numbox {
    border: none; border-bottom: 1px solid transparent; background: none;
    text-align: right; padding: 0; cursor: text;
  }
  body .bottom-bar .grain-numbox:hover { border-bottom-color: var(--border-faint); }
  body .bottom-bar .grain-numbox:focus {
    outline: none; border-bottom-color: var(--grid-teal, #7abcbc); color: var(--text-bright); }

  /* In the footer the pills are compact: three of them plus the meters and
     the values do not fit a 1400px strip at chrome proportions, and the one
     that was being clipped was MASTER — the value you most want at a glance.
     Same shape, less air. */
  body .bottom-bar .seg-pill .grain-seg-btn {
    padding: 0.14rem 0.38rem; letter-spacing: 0.02em; font-size: var(--fs-nano);
  }
  body .bottom-bar .grain-row { gap: 0.32rem; }
  /* The borrowed audio device lays out as a ROW down here — it is built as a
     stack for a 246px panel column, and the footer is a strip. */
  /* `.device` and `.device-body` both set flex-direction: column for the
     panel column; declaring display alone left that in place and the footer
     grew to 208px. Direction has to be said. */
  body #tcAudioSlot .device--audio {
    display: flex; flex-direction: row; align-items: center; gap: 0.5rem;
    border: none; background: none; padding: 0; margin: 0; width: auto;
    overflow: visible; }
  body #tcAudioSlot .device-label { display: none; }
  body #tcAudioSlot .device-body {
    display: flex; flex-direction: row; align-items: center; gap: 0.5rem;
    overflow: visible; flex: none; }
  body #tcAudioSlot .grain-row { display: flex; flex-direction: row;
    align-items: center; gap: 0.4rem; margin: 0; flex: none; white-space: nowrap; }
  body #tcAudioSlot .grain-label { font-size: var(--fs-eyebrow);
    letter-spacing: var(--ls-eyebrow); text-transform: uppercase; color: var(--text-secondary); }
  body #tcAudioSlot .grain-slider { width: 42px; flex: none; }
  body #tcAudioSlot .grain-numbox { width: 2.5rem; }
  /* Two rows leave the footer's audio group. Each row is ~160px and four of
     them do not fit a 1400px strip beside the meters and the two cursor
     pickers — and the row being clipped was MASTER, the value you most want
     at a glance. Both survivors are STATE you monitor; both exiles are levels
     you set once, and both keep their home in Settings → audio:
       · input CHANNEL — chosen when you plug in.
       · the GATE row (Ek: "I just need to be able to monitor it in the
         levels"). The LEVELS rail already draws the gate meter with its
         threshold marked, which IS the monitoring; a second copy in AUDIO was
         the same fact twice. Setting the threshold lives in Settings → audio.
     Dry GAIN went out with them for a moment and came straight back (Ek) —
     with the gate row gone there is room, and the dry level is something you
     ride mid-piece rather than set once. The audio group is dry mode, dry
     level, master. */
  body #tcAudioSlot .grain-row:has(#apInputChannelSelect),
  body #tcAudioSlot .grain-row:has(#apGateMeter) { display: none; }
  /* The gate meter in the audio group is the draggable one; keep it small but
     wide enough to aim at. */
  /* The gate meter STAYS: it is the gate's control, not a second readout —
     you drag it to set the threshold (#266). */
  /* ── The pins page is PARAMS only (#270, Ek) ───────────────────────────
     What the commits device shows in the rig view is mostly performance, and
     the tile screen already owns all of it:
       · the 8-slot bank + count → the PINNED rail lists them by name;
       · the D / hold-D / ⌘D buttons and `clear all` → the pin buttons in that
         rail and the chrome;
       · commit MODE (cloud | loop) → the type is baked into the stroke now:
         a line/slice/looper tile pins a loop, a grain tile pins a cloud, so
         asking again here is asking twice;
       · cloud MORPH → sunset.
     Hidden here rather than deleted: `S.commitMode` and the morph engine are
     still wired, and unpicking them from the engine is a separate job with
     real reach (see TODO #270). This is the page, not the feature. */
  .settings-host .commit-section--bank,
  .settings-host .seq-section:has(#commitModeSeg),
  .settings-host .seq-section:has(.mu-btn),
  .settings-host .seq-section:has(#commitClearBtn),
  .settings-host [data-collapse-key="commitCloudParams"],
  .settings-host [data-collapse-key="commitLoopParams"] { display: none !important; }

  /* ── One type scale for this screen (#256) ─────────────────────────────
     Before this there were six micro-label sizes (0.52 → 0.63rem) at four
     letter-spacings across the chrome, the strip, the rail and the panel —
     every surface had invented its own. Three roles, three tokens, and
     nothing on this screen sets a size outside them:
       EYEBROW  uppercase section captions and state readouts
       CONTROL  anything you press or read as a name
       META     values, key legends, footnotes  */
  /* ── …and it lives in tokens.css (2026-08-29) ──────────────────────────
     The three roles above are right and they stay. What was wrong is that
     they were DEFINED here, on `body`, with literal values — which shadows
     the `:root` block in tokens.css for this entire screen. tokens.css was
     therefore dead for everything a performer actually looks at: editing the
     type scale there changed the modals and nothing else, silently.

     Found by measuring: --fs-eyebrow resolved to 0.56rem on the footer
     captions while tokens.css said 0.69rem, and the difference was this
     block. The global scale now carries these same three roles, so the
     override has nothing left to say and the screen inherits. */

  /* ── The grid: stage | rail / tilebar | lens dock / properties ──────────
     Footer rows are FIXED (Ek, 2026-08-27, restated 2026-08-28): auto rows
     resized with each tile's param count, which resized the canvas without a
     resize event — the sphere stretched and the radius circle went
     elliptical. The properties row is the same rule taken further: 17rem
     whenever it is shown, whatever engine is in it, so nothing moves under
     the player mid-edit. Content scrolls inside the row. */
  body .main-layout {
    display: grid;
    /* One column (#259). Every rail overlays the stage now — left pair and
       pinned alike — so none of them can resize the sphere, and each can be
       hidden without the others moving. */
    grid-template-columns: minmax(0, 1fr);
    /* stage · tile row · properties. BOTH footer rows are 0 in performance
       (#254): the toolbox and the properties panel are one prep surface and
       they open together, so the playing screen is sphere + floating palette +
       chrome and nothing else. Fixed heights when shown — never
       content-sized, so the sphere never resizes under the player (#251). */
    grid-template-rows: minmax(0, 1fr);
    gap: 0; padding: 0;
    height: calc(100vh - var(--tc-bar-h, 2.6rem) - var(--bottom-bar-h, 3.4rem));
  }

  body .canvas-wrapper {
    grid-column: 1; grid-row: 1;
    width: auto; height: auto; min-height: 0; max-width: none;
    border-radius: 0; margin: 0;
  }

  /* ── The left rails (#258) ─────────────────────────────────────────────
     Ableton's browser and device view: a LIST of every tool, and beside it
     the properties of whichever one is open. Both float over the stage and
     mirror the pinned rail on the right — same frame, same header bar, same
     type — so the screen reads as one idea from either edge. They overlay
     rather than taking grid columns, so opening either never resizes the
     sphere (the #251 lesson: a stage that resizes under the player is worse
     than one that is partly covered). */
  .tc-lrail, .tc-prail { display: none; }
  body.props-open .tc-lrail,
  body.prail-open .tc-prail {
    display: flex; flex-direction: column;
    grid-column: 1; grid-row: 1; justify-self: start; align-self: stretch;
    width: var(--lrail-w); min-height: 0; z-index: 4;
    background: var(--bg-app); border-right: 1px solid var(--border-faint);
    font-family: var(--font-sans);
  }
  /* The editor is as wide as the thing it holds and no wider (Ek, 2026-09-22:
     "make the bench one the same width as the spacebar and reduce the width of
     the rail to match") — so its width is DERIVED from the hand tile rather
     than stated beside it, and it follows the strip onto a narrow screen. It
     went to 20rem for a day when the bench was a card with buttons in it, and
     shrank when the buttons left. The properties rail starts where it ends. */
  /* The DRAWER keeps its own 15.5rem: it is a sheet that opens over the stage,
     not one of the two rails you work in. */
  body.prail-open .tc-prail { width: 15.5rem; margin-left: var(--lrail-w); overflow-y: auto; }
  /* An ENGINE page was two columns of rows at 37rem, and at that width the
     drawer covered too much of the sphere (Ek, 2026-09-03: "maybe 3/5ths").
     It is ONE column at 22rem now — the #272 lesson still holds (30rem across
     two columns left each TRACK about 78px, which reads as "stuck" however
     correctly it writes), so the column count went, not the track: one
     column at 22rem gives a track ~185px, wider than the two-column 170. The
     sampler's sheet is a list and keeps the narrow rail (#261). */
  body.prail-open.engine-page .tc-prail { width: 22rem; }
  /* THE ENGINE SHEET ENDS WHERE ITS CONTENT ENDS (Ek, 2026-09-24: "can the
     height of the sheet not be full height, it should go from top to the
     bottom of the content so i can see what's beneath it"). The frame starts
     at the top and stops under the last row, closed by a bottom rule; only
     when the rows are taller than the stage does it take the full height and
     scroll, as before. The tool rail beside it is a rail and stays full. */
  body.prail-open.engine-page .tc-prail { align-self: start; max-height: 100%;
    border-bottom: 1px solid var(--border-faint); }
  /* `--sp-3`, not 0.4rem: 6.4px was the one inset in this rail that is not a
     step, and the sheet says snap rather than name a component token.
     `--sp-5`, not `--sp-3` (2026-09-22, evening): 6px is less than the 12px the
     last card carries as its own bottom padding, so on every tab the frame cut
     the card before its corner closed — measured, the list overflowed by 13px
     on tape with nothing in it, and 13 is that padding. The rail read as
     running off the bottom of the window rather than ending. 12 matches the gap
     between the cards; the top is `--sp-6` because a DIVISION sits above it. */
  .tc-lrail .lyr-list { padding-bottom: var(--sp-5); }
  /* AFTER THE `TOOLS` DIVIDER (Ek, 2026-09-22: "spacing is not enough above the
     cursor presets bed after the tool line divider"). It was `--sp-3`, half of
     what every other gap in this rail gets, so the first card sat closer to the
     rule above it than the cards sit to each other. A DIVISION breaks harder
     than a band gap, so both dividers take `--sp-6` and the bands keep
     `--sp-5`: 16 around a rule, 12 between cards. */
  .tc-lrail .lyr-list { padding-top: var(--sp-6); }
  /* A tool ROW — the rail's unit, where the palette's unit is a tile. */
  /* 24, the kit's button height (Ek ruled 2026-09-15: snap them all). It was
     27.9 — padding plus a line box, a number nobody chose — and the rail is the
     one surface where a row's height multiplies by twelve. Stated, not fallen
     out of: the padding is now only horizontal, and `height` carries it. */
  .trow { display: flex; align-items: center; gap: 0.55rem; width: 100%;
    height: 24px; box-sizing: border-box;
    background: none; border: none; border-left: 2px solid transparent;
    padding: 0 var(--sp-5); cursor: pointer; text-align: left;
    color: var(--text-dim); font-family: var(--font-sans);
    font-size: var(--fs-control); }
  .trow svg { width: 1.05rem; height: 1.05rem; flex: none; color: var(--c, var(--eng)); }
  .trow:hover { background: var(--surface-1); color: var(--text-bright); }
  .trow .tile-nm { flex: 1 1 auto; min-width: 0; }
  /* Two independent facts, two marks: ON is the choice this row carries — the
     lens that is reading, the input that is inked from — as a left edge in
     the engine's hue; OPEN is what the properties rail is showing (a raised
     ground). They are usually the same row and must be able to disagree —
     clicking a lens opens it without turning it on. (`.armed` was the class
     until 2026-09-11: it named the tool that space would fire, and the tool,
     the space bar and the word all went with arming.) */
  .trow.on { border-left-color: var(--c); color: var(--text-bright); }
  .trow.on svg { color: var(--c); }
  /* The row lights with its palette tile while the tool sounds (#260's
     "the same tile, twice" — a class write hits both copies). */
  .trow.playing { background: color-mix(in srgb, var(--c) 22%, var(--bg-app)); }
  .trow.open { background: var(--surface-2); color: var(--text-bright); }
  .trow.ghost .tile-nm { font-style: italic; }

  /* A PRESET row is a word alone (2026-09-24 evening; the dot and the half
     moon are gone). Its 2px left border is the ON mark and sits in the
     gutter, so the word steps back by it to share the left edge of the labels
     above it — measured 27 against 25 before this line. */
  .trow--voice { padding-left: calc(var(--sp-5) - 2px); }
  /* ON is a plain highlighted item, as every list does it (Ek, 2026-09-24:
     "no need for the left edge line … just a normal highlighted item like all
     gui"): the row's ground raised, the word bright, no mark at the edge. */
  .trow--voice.on { border-left-color: transparent; background: var(--surface-2); color: var(--text-bright); }
  /* PRESETS CARRY WEIGHT (Ek, 2026-09-25 — the rails canvas, option B). 42, not
     the rail's 30: a preset is a thing you pick, not a setting you read, and
     the row carries three facts — the instrument's glyph, the name at 600, and
     the preset's own few numbers flush right. Hairlines between, the list's own
     rule. The glyph is ash until the row is on, then the engine's hue. */
  /* The row's height is a named box (`--row-preset`), stated once — the way the
     footer's buttons fill --footer-row — so the kit audit reads the token. */
  .tc-lrail { --row-preset: 42px; --card-head: 40px; }   /* --card-head: a card's top edge — its head, or the tab strip — stated once */
  .trow--voice { height: var(--row-preset); gap: var(--sp-5); font-size: var(--fs-md); font-weight: 600;
    color: var(--text-hover); border-bottom: 1px solid var(--border-faint); }
  .trow--voice .trow-g { width: 16px; height: 16px; color: var(--text-dimmer); }
  .trow--voice.on .trow-g { color: var(--c); }
  .trow--voice.on .tile-nm { color: var(--text-highlight); }
  .trow--voice .trow-sum { flex: none; margin-left: auto; font-size: var(--fs-body); font-weight: 400;
    color: var(--text-dimmer); white-space: nowrap; font-variant-numeric: tabular-nums; }
  .trow--voice.on .trow-sum { color: var(--text-secondary); }
  .trow--voice .trow-del { margin-left: 0; }
  .trow--voice .trow-sum + .trow-del { margin-left: calc(-1 * var(--sp-2)); }
  .trow.dragging { opacity: 0.35; }
  .trow.over { box-shadow: inset 0 2px 0 var(--text-tertiary); }
  /* The rail is a LIST. #srcBar / #tileBar still carry the strip's
     flex rules from when they were one horizontal shelf, so they are undone
     here rather than there — the rig view still renders the strip shape. */
  .tc-lrail #srcBar, .tc-lrail #tileBar,
  .tc-lrail .tbx-grp, .tc-lrail .tbx-tiles,
  .tc-rail .tbx-grp, .tc-rail .tbx-tiles {   /* the pin group in the pinned rail (2026-09-12) */
    display: block; flex: none; overflow: visible; border: 0; margin: 0; padding: 0;
  }
  /* The title is a flex line so its `+` sits flush right, over the ⋯ column
     of the rows beneath (the row's 0.7rem side padding is the title's). */
  .tc-lrail .tbx-lbl, .tc-rail .tbx-lbl { display: flex; align-items: center; padding: 0.75rem 0.7rem 0.25rem; }
  /* Air under the pin group (Ek, night: "spacing under the pin tools section
     is tight"): the last row was on the section's hairline. */
  /* THE PIN ROWS in the pinned rail (Ek, 2026-09-12, night): the tool rail's
     row, an act's grey glyph (a pin holds any engine's material), the key as
     the legend's own cap at the right edge, brick on approach for unpin all. */
  .tc-rail .trow--act svg { color: var(--text-dim); }
  .tc-rail .trow--act:hover svg { color: var(--text-bright); }
  .tc-rail .trow--act.fired svg { color: var(--accent-action); }
  .tc-rail .trow--danger:hover { color: var(--accent-danger); background: var(--tint-danger-soft); }
  .tc-rail .trow--danger:hover svg { color: var(--accent-danger); }
  /* ── THE RAIL ON THE SCALE (Ek, 2026-09-22: "it's really tight around the
     tile, use the design kit to fix all the spacing in the left rail") ──────
     One horizontal inset everywhere — `--sp-5`, the same 12 as the gutter
     outside the cards and the gap between them, so a caption, a row's glyph and
     the bench tile all start on one edge and the air around a card is the same
     on all four sides. It was `0.7rem` = 11.2px, which is not a step: the scale
     is 2 · 3 · 4 · 6 · 8 · 12 · 16 · 24 · 32 and the build sheet says snap.

     Vertically, inside a card: 12 above a caption's text, 4 below it, 12 under
     the last thing before the card's edge. `:first-child` gave a group's first
     caption 8 and every other 12, so `voice presets` sat lower than `shape
     presets` in the same card; one value now.

     THE CAPTION TAKES THE ROW'S 2px BORDER, transparent. A row carries one and
     a caption did not, so matching their padding still left them 2px apart —
     this aligns them by construction rather than by a number that has to be
     kept in step by hand. */
  .tc-lrail .tbx-lbl {
    padding: var(--sp-5) var(--sp-5) var(--sp-2);
    border-left: 2px solid transparent;
  }
  .tc-prail .ds-body { column-width: auto; columns: 1; padding: 0 0 0.6rem; }
  /* UNAVAILABLE, NOT ABSENT (2026-09-22). A row the current mode cannot use
     STAYS where it is and goes ash: the page keeps its shape, every other row
     keeps its place, and the value it still holds is visible rather than
     implied. This is the one legitimate dim — the sheet's always-wrong is about
     using dimness to encode a STATE you set (on against off), and `--text-faint`
     is the token for "disabled marks only". The wrapper keeps its pointer so the
     tooltip can say why; everything inside it is inert. */
  /* Both rails: the cursor-interaction rows moved to the TOOL rail on
     2026-09-22 and grey there for the same reason they greyed on the sheet. */
  /* A DISABLED WORD IS STILL A WORD (2026-09-22, evening). `--text-faint` is
     2.7:1 and its own token comment says "disabled marks only, never a word" —
     but grain's five arrival rows are five words (dwell · start · release ·
     retrig · rearm) and they were all wearing it, which is the build sheet's
     always-wrong § 4 shipped five times in a column. The MARKS inside them keep
     faint (`--c` drives the segment's chosen fill, and a mark is what the token
     is for); the words step down to `--text-dim`, 4.0:1, the quietest text the
     ramp allows to be READ. Against a live row's `--text-secondary` at 8.1:1
     that is still half the contrast, so "unavailable, not absent" survives
     intact — it just stops being unreadable. */
  .tc-prail .ds-na, .tc-prail .ds-na *,
  .tc-lrail .ds-na, .tc-lrail .ds-na * { color: var(--text-dim); --c: var(--text-faint); }
  .tc-prail .ds-na > *, .tc-lrail .ds-na > * { pointer-events: none; }
  /* A SWITCH in ash too (Ek, 2026-09-23: "the toggle should also be greyed
     out if walk is off"). `.ds-na` reached the words and `--c`, and the kit's
     switch paints from its own tokens — lit `--eng-pins`, resting
     `--border-soft` — so a greyed row kept a live-looking switch beside an
     ashed label. The two quietest steps the ramp allows, never opacity. */
  .tc-lrail .ds-na .mrow-sw, .tc-lrail .ds-na .mrow-sw.on { background: var(--border-faint); }
  .tc-lrail .ds-na .mrow-sw .mrow-knob { background: var(--text-faint); }

  /* ── The parameter row (#261) ──────────────────────────────────────────
     name · track · number, on one line. Rows line up, so a section reads as
     a shape and you can see that fade is high while prob is low without
     reading either. Two columns on an engine page; one in the narrow rail. */
  /* ── One grid, every variant (#282) ───────────────────────────────────
     The label and value columns are TOKENS, not numbers repeated per row
     variant. Repeating them is why duration and period began further right
     than the rows above: `.prow--wide` carried its own 4.6rem label column,
     so the two most important tracks in the engine were the two that broke
     the left edge. A table is only a table if every row agrees where the
     columns are — including the full-width ones. */
  .tc-prail {
    --prow-lab: 4.1rem;   /* name */
    --prow-val: 4.4rem;   /* value — sized for `last 9 strokes` (70px), the readout the
                             vocabulary rules on; 3.1rem cut it to `last 3 stro` and 65.00ms
                             to 65.00m, and had since #272 (measured 2026-09-03) */
    --prow-var: 3rem;     /* the ± beside it */
    --prow-live: 3.4rem;  /* a number the app writes, not one you set (lens k) */
    /* 8px. Was 0.6rem = 9.6px, and the only member of this block with no
       comment — its neighbours all state what measured them. A fractional
       token is legal where the fraction is DERIVED and the derivation is
       written beside it; this one could not say, so it predates the scale and
       snaps to the nearest step. */
    --prow-gap: var(--sp-4);
  }
  .tc-prail .ds-sec-cells { display: grid; grid-template-columns: 1fr;
    gap: 0.3rem 1.6rem; }
  /* One column on the engine page too (2026-09-03) — the rule that made it
     two is kept as a line so the width note above has something to point at. */
  body.engine-page .tc-prail .ds-sec-cells { grid-template-columns: 1fr; }
  .prow { display: grid; grid-template-columns: var(--prow-lab) 1fr var(--prow-val);
    align-items: center; column-gap: var(--prow-gap); height: 1.6rem; }
  .prow-n { font-size: var(--fs-meta); color: var(--text-dim);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* The visible track is 2px; the HIT TARGET is the whole row height — a 2px
     strip cannot be landed on, which read as "the settings don't work" the
     first time the options bar was played. Same lesson, same fix. */
  .prow-t { position: relative; height: 1.45rem; cursor: ew-resize;
    touch-action: none; min-width: 0; }
  .prow-t::before { content: ''; position: absolute; left: 0; right: 0; top: 50%;
    height: 2px; margin-top: -1px; background: var(--surface-4); border-radius: 1px; }
  .prow-f { position: absolute; left: 0; top: 50%; height: 2px; margin-top: -1px;
    background: color-mix(in srgb, var(--c) 75%, transparent); border-radius: 1px; }
  .prow-h { position: absolute; top: 50%; width: 2px; height: 11px; margin: -5.5px 0 0 -1px; /* geometry */
    background: var(--c); border-radius: 1px; }
  /* The default, so you can see where a double-click will land before you
     commit to it. */
  .prow-d { position: absolute; top: 50%; width: 1px; height: 5px; margin-top: -2.5px;
    background: var(--text-faint); }
  /* ── The spread band (#277) ────────────────────────────────────────────
     A parameter's variation drawn on the parameter's own track: the handle is
     the value, the band is the range it moves through. Behind the handle and
     the default tick, so it never hides them. */
  .prow-band { position: absolute; top: 50%; height: 6px; margin-top: -3px;
    background: color-mix(in srgb, var(--c) 26%, transparent);
    border-radius: 3px; pointer-events: none; }
  /* The ± value: quieter than the value it belongs to, and quieter still when
     it is zero — a spread of nothing should not draw the eye. */
  .prow-s { font-family: var(--font-mono, monospace); font-size: calc(var(--fs-meta) * 0.92);
    color: var(--text-tertiary); background: none; border: none;
    border-bottom: 1px solid transparent; width: 100%; text-align: right;
    padding: 0; font-variant-numeric: tabular-nums; }
  .prow-s.zero { color: var(--text-faint); opacity: 0.55; }
  /* A number cell is a slider you can type into (tiles.js _wireKnobs): the
     cursor says so, and it stays a text caret once the cell has focus. */
  .prow-s, .prow-v { cursor: ew-resize; }
  .prow-s:focus, .prow-v:focus { cursor: text; }
  .prow-s:hover { border-bottom-color: var(--border-faint); }
  .prow-s:focus { outline: none; border-bottom-color: var(--c); color: var(--text-bright); opacity: 1; }
  /* A paired row gives the ± its own narrow column. */
  .prow--var { grid-template-columns: var(--prow-lab) 1fr var(--prow-val) var(--prow-var); }
  /* ── Full-width rows (#278) ────────────────────────────────────────────
     duration and period decide what granular sounds like, and their ± band
     is unreadable in a half-width track. They take the whole row, which also
     gives the band room to be seen rather than inferred. */
  body.engine-page .tc-prail .ds-sec-cells > .prow--wide { grid-column: 1 / -1; }
  /* Same label and value columns as every other row — only the TRACK grows,
     which is the whole point of a full-width row. */
  .prow--wide { grid-template-columns: var(--prow-lab) 1fr var(--prow-val) var(--prow-var); }
  /* A full-width row with no ± must not leave that column standing empty —
     the number was hanging one column short of the edge (#284). */
  .prow--wide:not(:has(.prow-s)) {
     grid-template-columns: var(--prow-lab) 1fr var(--prow-val); }
  /* The wide row is the SAME SIZE as every other row (Ek, 2026-09-10: "make
     all the params of the engine sheet the same size, dur per pitch and vol
     are bigger, make them like the others"). Until then it was a lead row —
     1.95rem tall, --fs-control label and value, a 3px track, a 15px handle,
     a 9px band — and in a one-column sheet the four of them read as four
     different controls rather than the sheet's most important. What "wide"
     still means is the layout: the whole row, and the ± column. */
  /* Two ideas on one line: taper (a track) and curve (a choice). The track
     needs a floor or the choice's chips starve it, and `_rowFor` emits its own
     label inside `.opt > i` — which showed up as a truncated second caption
     next to the one this row already draws. */
  /* ── The lens page's live columns (2026-09-07) ─────────────────────────
     k is a CEILING, and a ceiling is only useful beside the count pressing on
     it, so those two rows carry a number the app writes rather than one you
     set. It gets its own narrow column at the end — never the value column,
     which is the typeable one, and the dim mono of `.prow-note` is what says
     "this is a readout" without a caption. */
  /* Saturated — the ceiling is what is limiting the cursor, which is the one
     thing k has to be able to say. The engine's own accent, never a new hue. */
  .prow-note.hot, .mrow-live.hot { color: var(--eng-lens, var(--text-bright)); }
  /* ── A boolean is a SWITCH on the engine sheet too (2026-09-07) ────────
     INSTRUMENT-GUI § 3 assigns the shape — "do you want it? set and forget"
     — and it had one instance, a grain brush's `wet`. `fade`, `chop`,
     `loop on end` and `link` ask exactly that question and were wearing the
     SEGMENTED shape, which answers "which one?" and made a yes/no read as a
     mode pick. Same 32×18 switch as the settings kit, in the ENGINE's hue so
     the sheet stays one colour. */
  .prow--sw { grid-template-columns: var(--prow-lab) auto minmax(0, 1fr); }
  .ds-sw { display: inline-flex; align-items: center; background: none; border: none;
    padding: 0; cursor: pointer; color: var(--text-dim); }
  .ds-sw.on { color: var(--c, var(--text-bright)); }
  .ds-sw.on .mu-switch { background: var(--c, var(--text-bright)); }
  .ds-sw.on .mu-switch b { left: 16px; background: var(--bg-app); }
  .prow--duo { grid-template-columns: var(--prow-lab) minmax(3.5rem, 1fr) var(--prow-val) auto auto; }
  .prow--duo .opt > i { display: none; }
  .prow--duo .opt { gap: 0; }
  .prow-n--mate { padding-left: 0.7rem; text-align: right; }
  /* Link and octave are their own rows now (#280), each a state pill with a
     quiet note saying what the state means in numbers. */
  /* The extra air goes between the pill and its note, NOT in the label gap —
     widening the gap here pushed the pill 5px right of every track above it,
     which is the same broken left edge in miniature. */
  .prow--seg:has(.seg-pill) { grid-template-columns: var(--prow-lab) auto minmax(0, 1fr);
    column-gap: var(--prow-gap); }
  /* A chip row wide enough to need the whole row gets it, same as duration
     and period do — a clipped capsule cannot be read, and `overflow: hidden`
     on the pill means it clips SILENTLY. */
  body.engine-page .tc-prail .ds-sec-cells > .prow--segwide { grid-column: 1 / -1; }
  .prow-note { font-family: var(--font-mono, monospace); font-size: calc(var(--fs-meta) * 0.92);
    color: var(--text-faint); white-space: nowrap; padding-left: 0.85rem; }

  /* A readout longer than its column (`first 10 strokes`) spills LEFT over
     the track's tail — end-aligned overflow goes to the start side — rather
     than being cut at the rail's edge, where the loss was silent. */
  .prow-v { justify-self: end; width: max-content; max-width: none; }
  .prow-v { font-family: var(--font-mono, monospace); font-size: var(--fs-meta);
    color: var(--text-secondary); background: none; border: none;
    border-bottom: 1px solid transparent; width: 100%; text-align: right;
    padding: 0; font-variant-numeric: tabular-nums; }
  .prow-v:hover { border-bottom-color: var(--border-faint); }
  .prow-v:focus { outline: none; border-bottom-color: var(--c); color: var(--text-bright); }
  /* A row of chips is taller than a row with a 2px track in it, so it gets
     its own breathing room rather than being squeezed to a track's height. */
  .prow--seg { grid-template-columns: var(--prow-lab) 1fr; height: auto;
    min-height: 1.85rem; padding: 0.1rem 0; }
  .prow--seg .ds-chips { min-width: 0; }
  /* A chip row that ends in a number (curve's slope, 2026-09-24) takes the
     value column every track row has, so the number sits on the same right
     edge as the numbers above and below it. */
  .prow--seg:has(> .prow-v) { grid-template-columns: var(--prow-lab) 1fr var(--prow-val); }
  .prow--seg .opt > i { display: none; }
  /* Same rule in the TOOL rail: `.mrow-l` names a cursor-interaction row, in
     MODE's type, so the pill's own caps word would be the name written twice —
     and at a lighter weight than the rows above it (Ek, 2026-09-22: "match the
     text with the ones under mode. it's too light"). */
  .tc-lrail .mrow .opt > i, .tc-rail .mrow .opt > i { display: none; }
  .tc-lrail .mrow .opt, .tc-rail .mrow .opt { flex: none; }
  /* SENTENCE CASE IN THE CAPSULES (Ek, 2026-09-22 night: "all settings that use a
     multichoice pill look like they're not all caps in the settings, they
     shouldn't be in the pill section also"). The settings pages already set
     their segments in sentence case (settings-gui.css); the rails and the
     drawer inherited the chrome's uppercase from the shared `.seg-pill` rule.
     One rule, the settings kit's own tracking, and a word in a capsule reads
     as the same word it is on the row beside it. */
  .tc-prail .opt .seg span, .tc-lrail .opt .seg span, .tc-rail .opt .seg span {
    text-transform: none; letter-spacing: var(--ls-tight); }
  /* AIR BETWEEN THE PILLS (Ek, 2026-09-22: "the pills are too tight betwen each
     other"). MODE's rows are 24 with no gap and read as open because the SWITCH
     is 18 inside them — 3 above, 3 below, so 6 between two switches. The pill
     is 24 in a 24 row and had none at all. `--sp-1` either side gives it the
     same 6, so the two sections share one rhythm rather than each having its
     own. The row grows; the pill stays the kit's 24. */
  /* ONE LINE PITCH DOWN THE RAIL — 30 (Ek, 2026-09-22: "let's space everything
     out to match the cursor interaction line space … only if it's approved by
     the design kit"). It is, and the kit is also what splits the fix in two.
     A `.mrow` is a DIV: it can simply grow, so `--sp-1` either side takes it
     from 24 to 30 and the switch or pill inside keeps its own kit height.
     A `.trow` is a BUTTON, and R6 holds every button in the instrument to
     18 / 24 / 32 / 38 — so the preset rows must NOT grow. They keep their 24
     and the LIST spaces them instead: `--sp-3` between rows is the same 30
     pitch, with nothing off the ladder.
     The `.mrow` height is STATED rather than grown, because a row grown by its
     padding is only as tall as what sits in it: a MODE row holds an 18 switch,
     so 18 + 3 + 3 came back to the 24 it started at while a cursor row holding
     a 24 pill went to 30. 30 is the kit's 24 plus the two steps, which is what
     every row in this column now is. */
  .tc-lrail .mrow { height: 30px; padding-top: var(--sp-1); padding-bottom: var(--sp-1); }
  /* A SUB-ROW — dwell and retrig belong to WALK (Ek, 2026-09-22). The kit's
     own subordination, ported from the settings rail's `.set-nav-item--sub`:
     an indent plus a step quieter, and no mark. There the indent is 37px
     because it clears the parent's icon; here the parent's label starts at the
     row's left edge, so it is one step of the scale — `--sp-5`, 12px, the same
     step the rail's cards are padded by. The label alone moves: the control
     stays flush right, so the column of pills reads straight down and only the
     WORDS step in, which is the whole point — the two rows are a sentence
     under the switch above, not a second group.
     A step quieter is `--text-tertiary` against the row's `--text-secondary`;
     `.ds-na` is a different statement (nothing to act on) and overrides this
     one while walk is off, which is correct — it is the stronger fact. */
  .tc-lrail .mrow--sub .mrow-l { padding-left: var(--sp-5); color: var(--text-tertiary); }
  /* …and `.ds-na` has to be SAID here, because specificity is not reading
     order: the disabled rule is `.tc-lrail .ds-na *` at three, and the line
     above is at three with a class more, so without this the sub label would
     stay tertiary inside a greyed row while everything beside it went dim. */
  .tc-lrail .ds-na .mrow--sub .mrow-l { color: var(--text-dim); }
  /* The SHEET's sub-row (Ek, 2026-09-23: "step is part of pitch … taper is
     part of curve … indented"): the same statement, on `.prow`. One step of
     the scale and a step quieter than the `--text-secondary` the sheet's
     names wear (`html body .prow-n`, the ramp note further down); the track,
     chips and number stay in their columns, so only the WORD steps in. */
  .tc-prail .prow--sub .prow-n { padding-left: var(--sp-5); color: var(--text-tertiary); }
  .tc-lrail .tbx-tiles { display: flex; flex-direction: column; gap: var(--sp-3); }
  /* The number is the row's own control, flush right like every other.
     `.prow-v` is sized for the sheet's GRID — `width: max-content` with
     `justify-self: end`, neither of which means anything in this flex row, so
     it stretched to the rail's width and hung off the end. Sized here instead,
     and 24 tall so its row is the same 30 as a pill's and the column stays
     even. `.mrow-l` already grows, which is what puts this against the edge. */
  .tc-lrail .mrow-num { flex: 0 0 auto; width: 3.5rem; height: 24px;
    line-height: 24px; text-align: right; }
  /* A readout in words — depth's `last 3 strokes` — at the sheet's own 70px
     (`--prow-val` on the lens rows, 2026-09-07) rounded to the rem scale. */
  .tc-lrail .mrow-num--words { width: 6rem; }
  /* THE LENS TAB'S LIVE PAIR (2026-09-22 night): marks in reach beside radius,
     taken / k beside k — the numbers the lens sheet carried since 2026-09-07,
     on the rows that cause them. A number you WATCH, not one you set: the
     tertiary step, visibly not the editable value beside it, and the same
     mono the numbox uses so the two read as one column. Its width is stated
     so a `—` and a `12/40` hold the numbox still. */
  .tc-lrail .mrow-live { flex: 0 0 auto; min-width: 3rem; margin-right: var(--sp-3);
    font-family: var(--font-mono, monospace); font-size: var(--fs-control);
    color: var(--text-tertiary); text-align: right; white-space: nowrap; }

  /* ── The sound window ─────────────────────────────────────────────────── */
  .eng-scope { padding: 0.6rem 0.9rem 0.45rem; border-bottom: 1px solid var(--border-faint); }
  .eng-scope canvas { display: block; width: 100%; height: 68px; }

  /* ── The filter, drawn ────────────────────────────────────────────────── */
  #engFilter { display: block; width: 100%; height: 76px; cursor: crosshair; touch-action: none; }
  #engFilter { margin-bottom: 0.5rem; }
  /* (A folded `experimental` section lived here from #283 until 2026-09-22.
     Every section on the sheet is open now — see renderProps.) */
  /* ── Your tool's own gesture, on the row (#284) ───────────────────────
     Double-click the name to rename (rows with `.trow--own` only). The
     row's hover-only `del` went 2026-09-10: Delete is a button in every
     tool's drawer head (.ds-del), factory or yours. */
  .trow-rn { flex: 1; min-width: 0; background: var(--surface-1); border: none;
    border-radius: 3px; color: var(--text-bright); font: inherit;
    padding: 0.05rem 0.25rem; margin: 0; }
  .trow-rn:focus { outline: 1px solid var(--c); }
  .tc-prail .ds-head { position: sticky; top: 0; z-index: 1; background: var(--bg-app); }

  body .tc-rail { display: none; }
  body.pinned-open .tc-rail {
    display: flex; grid-column: 1; grid-row: 1;
    justify-self: end; align-self: stretch;
    /* 20rem since the mixer (2026-09-16, Ek: "wider — ~320px"): a track carries
       the material, the fader and M / S on one line at a readable size. It is
       an overlay on the stage, so the sphere is untouched. */
    width: var(--rail-w); min-height: 0; z-index: 4;
    flex-direction: column;
    background: var(--bg-app); border-left: 1px solid var(--border-faint);
    font-family: var(--font-sans);
  }
  /* ONE HEADER HEIGHT FOR BOTH RAILS (Ek, 2026-09-22 night: "the tool title
     bar in the tool rail and the pinned title rail dont seem like the same
     height"). They were grown: 9.92 of padding round whatever the contents
     made — a 14px word on the left (34), the kit's 24 button on the right (45)
     — so the two titles sat on different lines across the stage. STATED now:
     40 = the kit's 24 button + `--sp-4` either side, the same box whether the
     bar holds a word or a button, and the title centres in it on both sides.
     The 0.62rem was a number nobody chose. */
  .tc-rail .lyr-bar, .tc-lrail .lyr-bar { display: flex; align-items: center; justify-content: space-between;
    gap: 0.5rem; height: 40px; box-sizing: border-box; padding: 0 var(--sp-5); border-bottom: 1px solid var(--border-faint); }
  .tc-rail .lyr-bar, .tc-lrail .lyr-bar { height: 52px; align-items: flex-end; padding-bottom: var(--sp-4); border-bottom: none; }
  /* ── The eyebrow-caption layer sits where the tokens say (2026-09-13) ──
     Ek, after the six engine hues landed on the rails and tiles: "the new
     brighter colours on the rails and tiles are great but now everything
     seems dull." Measured rather than guessed — the four regions sampled off
     the running app come out at nearly the same mean lightness (chrome 66.9,
     footer 67.5, rail 70.9, palette 74.2), so it was never brightness. It is
     that the coloured half got brighter and the LETTERING did not: every
     eyebrow caption in the instrument was set in `--text-faint`, 2.7:1, whose
     own comment in tokens.css reads "disabled marks only, never a word" —
     while the same file assigns `--text-secondary` (8.1:1) to "eyebrow
     captions, group labels". The CSS sat five steps below its own spec, and
     the folded section's own header proved it: resting faint, hovering
     secondary (that header is gone; the contrast finding is not). Faint
     keeps every genuinely-off use — disabled buttons, ghosts, `.off` rows,
     an unbound sticker. */
  /* THE RAIL TITLE IS A LARGE WORD (Ek, 2026-09-25 — the rails canvas, F:
     "more balanced, i like the spacing"): Tools · Cursor · Pinned at 20px bold
     in the one true white, sentence case, no glyph, sitting on the bottom of a
     52px bar with no hairline — the size divides the rail, so the rule went.
     It was the 11px eyebrow with a glyph and a rule above and below. */
  .tc-rail .lyr-title, .tc-lrail .lyr-title { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.01em;
    text-transform: none; color: var(--text-highlight); display: inline-flex; align-items: center; }
  .tc-rail .lyr-bar button, .tc-lrail .lyr-bar button { font-family: var(--font-sans); font-size: var(--fs-meta);
    letter-spacing: 0.09em; text-transform: uppercase; background: var(--surface-1);
    border: 1px solid var(--border-faint); color: var(--text-tertiary);
    border-radius: 3px; padding: 0.16rem 0.5rem; cursor: pointer; }
  .tc-rail .lyr-bar button:hover, .tc-lrail .lyr-bar button:hover { color: var(--text-light); border-color: var(--border-soft); }
  .tc-rail .lyr-list, .tc-lrail .lyr-list { flex: 1; overflow-y: auto; min-height: 0; }
  /* THE TOOL RAIL SCROLLS AS ONE (2026-09-25, tiles.js railScrollMark): the
     list grows to fill and keeps its content height, so with room the cursor
     sits on the foot and without it the rail scrolls, the Tools bar held. */
  body.props-open .tc-lrail { overflow-y: auto; }
  .tc-lrail .lyr-list { flex: 1 0 auto; overflow: visible; }
  .tc-lrail > .lyr-bar { position: sticky; top: 0; z-index: 3; background: var(--bg-app); flex: none; }
  /* The pin actions sit under the rail's header, above what they act on —
     as TOOL ROWS (.trow, renderPinChrome) since 2026-09-12: the old .lyr-act
     row model (dot, eyebrow label, keycap) is gone with the keycaps. The
     rail's flash is `.tc-rail .trow--act.fired` above. */
  /* ── THE MIXER (2026-09-16) ──────────────────────────────────────────────
     One TRACK per pin, and the track IS its fader (Ek: "the whole track is a
     slider bar that's the volume of that track"). Its fill is the level, a 2px
     edge is the exact value, the material sits inside it laid flat, in the
     pin's own slot colour — the colour it has on the sphere — and the number is
     in the ENGINE hue, the kind. Nothing on a track is a label: the shape of
     the material says loop or cloud, and the coordinates are gone (Ek: "i dont
     ever look at it"). Instrument scope: 32px bar, 18px M / S, eyebrow type.

     --c is the pin's engine hue and the only colour a track wears: the slot
     colour (--m) left the rail on 2026-09-17 — Ek does not read it live. */

  /* The header's right half: the count, and the settings door — at the
     TITLE's size (Ek, 2026-09-25: "make it more obvious that you can edit the
     8 and that the first number is a readout. make it bigger, as big as the
     title"). The count is plain text, a readout; the max is a FIELD — the
     settings kit's field in miniature: a ground, a hairline, the ember border
     on focus — so the one number you can change looks like it. */
  .lyr-bar-r { display: flex; align-items: center; gap: var(--sp-5); }
  .lyr-slots-n { display: flex; align-items: baseline; flex: none;
    font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums;
    line-height: 1; color: var(--text-secondary); }
  #lyrSlotsFilled { font-weight: 700; color: var(--text-secondary); }
  .lyr-slots-sep { color: var(--text-muted); margin: 0 var(--sp-2); font-weight: 400; }
  .lyr-slots-max { width: 2.25rem; height: var(--lyr-bar-btn-h); padding: 0; margin: 0; box-sizing: border-box;
    background: var(--surface-1); border: 1px solid var(--border-soft); border-radius: var(--r-2);
    color: var(--text-highlight); font-family: var(--font-sans);
    font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums;
    line-height: 1; text-align: center; cursor: text; }
  .lyr-slots-max:hover { border-color: var(--border-strong); }
  .lyr-slots-max:focus { outline: none; border-color: var(--accent-action); background: var(--surface-0); }

  /* ── The mode bar: blend · tether · sort · width ──────────────────────
     The four pin settings you switch mid-set, each a second door onto the S
     field Settings → Pins holds. Blend and sort are "which one?" — the
     chrome-density segment; tether is "do you want it?" — the 32×18 switch,
     in the SENSOR hue because what it turns on is the body driving the faders
     (the same violet the fader edges wear while it does). */
  /* ON A BED, LIKE THE TOOL RAIL'S CARD (Ek, 2026-09-22 night: "look at the
     pin rail, can you add the bed under the settings and make the design match
     the left side rail"). The same object `#instrPanel` is across the stage:
     one ground a step up from the rail (`--surface-1`), the hand's radius,
     12 in from either rail edge and 12 of air on all four sides inside —
     measured on the left: card 13→307 in a 1→319 rail, first row 12 under
     the card's top, last row 12 over its bottom. The hairline that closed the
     bar is gone: the card's edge is the division now, as it is on the left,
     and `--sp-6` above it is the same 16 the left list puts under TOOLS. */
  /* AND ITS ROWS ARE THE TOOL RAIL'S (Ek, the same night: "the formatting of
     how the text is aligned and else is not the same as the left rail yet").
     The card carries 12 top and bottom and NO side padding, and each `.mrow`
     carries its own 12 — exactly how `#instrPanel` and the lens tab's rows are
     built — so the label lands at card + 12 by the same rule on both rails.
     The row is the lens tab's row: 30 tall, `--sp-1` either side of a 24 line,
     the name in `.mrow-l` (sentence case, `--fs-control`, secondary), the
     control flush right by the label's own flex-grow, no gap between rows. */
  .lyr-modes { display: flex; flex-direction: column;
    margin: var(--sp-6) var(--sp-5) var(--sp-5); padding: var(--sp-5) 0;
    background: var(--surface-3); border-radius: var(--r-hand);
    --c: var(--eng-pins); }   /* the capsules' lit tint — the pins' own hue, bone */
  .tc-rail .mrow { height: 30px; padding-top: var(--sp-1); padding-bottom: var(--sp-1); }
  .lyr-sw { color: var(--text-dim); }

  /* ── The tracks ───────────────────────────────────────────────────────
     Absolutely placed and moved by transform: the SORT reorders them (near /
     far / old, which is the selected pin), and a row that moves by transform
     costs the render loop nothing. The list sets its own min-height. */
  .lyr-list { position: relative; padding: var(--sp-3) var(--sp-5); }
  /* THE SELECTED FRAME (Ek, 2026-09-16: "i dont think it's obvious enough that
     the first item on the pinned rail is the selected one … it needs to be a
     full border around the first item that's always there, even when there
     are no pins"). Row one is what unpin takes — the sort IS the selection —
     and the half moon said so in the rail's radio language, which a player
     mid-show does not read. So the slot itself is drawn: a hairline frame in
     the pins hue around the first track's place, a FIXTURE of the rail that
     stays when the list is empty and holds "nothing pinned" until there is a
     track to hold. Its label, SELECTED, is the rail's eyebrow sitting ON the
     line at the top-right corner, the rail ground behind it so the line reads
     as passing under — a tile's sticker with a word. Nothing new in the kit:
     the hairline, the eyebrow, the sticker's rule. Neutral, never ember —
     chosen is not exceptional (INSTRUMENT-GUI, the faces). The moon is gone
     from this rail: one mark per thing. Geometry: the bar is r-1 (2px), the
     frame sits 2px out, so its radius is r-3 (4) — the concentric rule; the
     8px of room above row one is the label's half above the line. ui-pins.js
     mirrors SEL_INSET and SEL_ROOM. */
  .lyr-sel { position: absolute; left: calc(var(--sp-5) - 2px); right: calc(var(--sp-5) - 2px);
    top: calc(var(--sp-3) + 8px - 2px); height: 36px; /* geometry: 32 + 2 × 2 */
    border: 1px solid var(--eng-pins); border-radius: var(--r-3); pointer-events: none; }
  /* The label speaks the rail's new type (2026-09-25, the rails canvas F): the
     card heads' sentence case and weight at the small size, not the spaced
     capitals the rails have left behind. */
  .lyr-sel-l { position: absolute; pointer-events: auto; cursor: default; right: var(--sp-3); top: -8px; /* geometry: half the label's line above the border */
    padding: 0 var(--sp-2); line-height: 14px; background: var(--bg-app);
    font-size: var(--fs-body); font-weight: 600; letter-spacing: normal; text-transform: none; color: var(--eng-pins); }
  .lyr-trk { position: absolute; left: var(--sp-5); right: var(--sp-5); top: calc(var(--sp-3) + 8px);
    transition: transform var(--m-state) var(--ease-snap); will-change: transform; }
  /* THE ROW IS THREE COLUMNS (Ek, 2026-09-23): number · the box · M S. The
     box is the fader and only the fader — level, material, playhead — so
     nothing that comes and goes can squeeze the material; the dub target is
     a ring round its NUMBER, not a mark in the row. The bus lines use the
     same columns, so every box in the rail shares one left and one right edge. */
  .lyr-trk-row, .lyr-bus-line { display: grid; grid-template-columns: 1.5rem minmax(0, 1fr) auto;
    align-items: center; column-gap: var(--sp-2); }
  .lyr-trk-row { height: 32px; }
  .lyr-trk-bar { position: relative; height: 32px; border-radius: var(--r-1); overflow: hidden;
    background: var(--surface-0); cursor: ew-resize; }
  /* The fill is the LEVEL: the pin's hue at low alpha, scaled from the left.
     Read from the audio nodes every frame (ui-pins.js), so the bar is the
     truth rather than a copy of it — a fader the cursor is driving moves. */
  .lyr-fill { position: absolute; inset: 0; transform-origin: left center;
    background: var(--c); opacity: 0.18; will-change: transform; }
  .lyr-edge { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -2px; /* geometry */
    background: var(--c); pointer-events: none; }
  /* UNITY is a tick two thirds along (ui-pins.js, the fader law): the kit's
     2px slider tick, under the material and over the fill, so the eye has
     0 dB to read the fill against — the right third is +12 dB of gain. */
  .lyr-unity { position: absolute; left: 66.667%; top: 0; bottom: 0; width: 2px; margin-left: -1px; /* geometry */
    background: var(--border-medium); pointer-events: none; }
  /* The body is driving it: the edge turns the sensor hue while the blend is
     focus. This is the one mark that says "focus is on", and it is on every
     track at once. */
  .lyr-trk.auto .lyr-edge { background: var(--accent-sensor); }
  /* Under follow the cursor holds the fader, so the hand cannot (2026-09-22
     night); the bar says so by not offering a grab. */
  .lyr-trk.auto .lyr-trk-bar { cursor: default; }
  /* The NUMBER is the identity — the same digit the sphere's anchor mark
     wears — in the engine hue: gold is a cloud, pink is a loop. A label, not a
     button: a pin has no settings of its own (In / Out are Settings › Pins). */
  .lyr-num { position: relative; width: 1.5rem; height: 32px;
    display: grid; place-items: center;
    font-family: var(--font-sans); font-size: var(--fs-meta); font-weight: 600;
    font-variant-numeric: tabular-nums; line-height: 1; color: var(--c); }
  /* THE RING (Ek, 2026-09-23; by touch since 2026-09-26): the loop a take
     would join — the one the cursor is on — wears a white ring round its
     number, so you can see what the spacebar is about to layer onto. */
  .lyr-trk.dub .lyr-num::before { content: ''; position: absolute; left: 50%; top: 50%;
    width: 20px; height: 20px; transform: translate(-50%, -50%); /* geometry: centred on the digit */
    border: 1.5px solid var(--text-light); border-radius: 50%; box-sizing: border-box; pointer-events: none; }
  /* Over the line: the max was pulled below this slot, so the engine no longer
     services it. Its number outlined, not filled — the instrument's own way of
     saying a thing is not live (the old tracker cell did the same). */
  .lyr-trk.over .lyr-num { box-shadow: inset 0 0 0 1px var(--c); border-radius: var(--r-1); }
  /* The material fills the box, edge to edge: the box is its timeline. */
  .lyr-mat { position: absolute; inset: 0; pointer-events: none; }
  .lyr-mat canvas { display: block; width: 100%; height: 32px; }
  /* The playhead: where the loop (or the moving cloud) is. It keeps running
     through a mute — that is what a DJ mute is, and the bar shows it. */
  .lyr-ph { position: absolute; top: 0; bottom: 0; width: 1px; left: 0;
    background: var(--c); will-change: transform; pointer-events: none; }
  .lyr-ph::before { content: ''; position: absolute; top: 0; left: -1px; width: 3px; height: 3px; background: inherit; }
  /* The readout prints only when the HAND set a level: eight tracks at 0 dB
     are eight bars of one length, not eight numbers to read. */
  /* Over the material now the material fills the box (2026-09-23), so it sits
     on a chip of the rail's ground — not text laid across the waveform. */
  .lyr-db { position: absolute; right: var(--sp-2); top: 50%; transform: translateY(-50%);
    padding: 0 var(--sp-1); border-radius: var(--r-1); background: var(--bg-app);   /* opaque: --surface-0 is 3% and let the wave through */
    font-size: var(--fs-nano); font-variant-numeric: tabular-nums; color: var(--text-tertiary); pointer-events: none; }
  .lyr-db:empty { display: none; }
  .lyr-trk.touched .lyr-db { color: var(--text-light); }
  .lyr-ms { display: flex; gap: var(--sp-1); }
  /* A dot per overdub (docs/archive/OVERDUB-PLAN.md § 2.5), and the tool's
     own glyph on the loop a dub would JOIN (Ek, 2026-09-15) — two different
     questions about two possibly-different pins, beside the readout. */
  .lyr-ovd { position: absolute; left: var(--sp-3); top: var(--sp-2); display: flex; gap: var(--sp-1); pointer-events: none; }
  .lyr-ovd i { width: 5px; height: 5px; border-radius: 50%; background: var(--c); display: block; }
  /* still recording: the same dot, hollow — filled once the layer is kept */
  .lyr-ovd i.rec { background: transparent; box-shadow: inset 0 0 0 1px var(--c); }
  /* The selected pin wore the rail's half moon here until 2026-09-16; the
     frame above (.lyr-sel) is its one mark now. `.sel` stays on the row for
     anything that reads it. */


  /* M and S — the same two on a track, on a bus, and on the mute tile. Pressed
     faces from the ramps (danger for a mute, ember for a solo): the raw reds
     that sat here were the last hand-written colours in the rail. */
  .lyrmute { font-family: var(--font-sans); font-size: var(--fs-nano); font-weight: 600;
    width: 1.15rem; height: 1.15rem; display: grid; place-items: center; padding: 0;
    background: var(--surface-1); border: 1px solid var(--border-faint);
    color: var(--text-faint); border-radius: 2px; cursor: pointer; }
  .lyrmute:hover { color: var(--text-bright); border-color: var(--border-soft); }
  .lyrmute[aria-pressed="true"] { background: color-mix(in srgb, var(--accent-danger) 30%, transparent);
    border-color: var(--accent-danger); color: var(--text-light); }
  .lyrsolo { font-family: var(--font-sans); font-size: var(--fs-nano); font-weight: 600;
    width: 1.15rem; height: 1.15rem; display: grid; place-items: center; padding: 0;
    background: var(--surface-1); border: 1px solid var(--border-faint);
    color: var(--text-faint); border-radius: 2px; cursor: pointer; }
  .lyrsolo:hover { color: var(--text-bright); border-color: var(--border-soft); }
  .lyrsolo[aria-pressed="true"] { background: color-mix(in srgb, var(--accent-action) 32%, transparent);
    border-color: var(--accent-action); color: var(--text-light); }

  /* ── The busses: the two groups' M and S, with their members' mean level ── */
  .lyr-bus { display: flex; flex-direction: column; gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--border-faint); }
  .lyr-bus:empty { display: none; }
  .lyr-bus-row { position: relative; height: 24px; border-radius: var(--r-1); overflow: hidden; background: var(--surface-0); }
  .lyr-bus-row .lyr-fill { opacity: 0.14; }
  .lyr-bus-row.soloed { box-shadow: inset 2px 0 0 var(--accent-action); }
  /* ALL has no solo: the S column keeps its width, empty, so ALL's M stands
     under the busses' M. */
  .lyr-ms-none { visibility: hidden; }
  /* UNPIN ALL in the block: the kit's 24 button, content-sized, the danger face,
     its glyph at the icon button's size. */
  .lyr-modes .lyr-unpin { flex: none; width: auto; padding: 0 var(--sp-4); }
  .lyr-modes .lyr-unpin svg { width: 13.33px; height: 13.33px; }
  .lyr-modes .lyr-unpin.fired { color: var(--accent-action); border-color: var(--accent-action); }
  .lyr-bus-nm { position: absolute; left: var(--sp-4); top: 0; height: 24px; display: flex; align-items: center; gap: var(--sp-3);
    font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow); text-transform: uppercase; color: var(--c); }
  .lyr-bus-nm b { font-weight: 500; color: var(--text-muted); font-variant-numeric: tabular-nums; letter-spacing: 0; }

  /* The pin acts, at the foot since the mixer. The rails' FOOT (helper text)
     is gone (Ek, 2026-09-12, night). */
  /* Inside the selected frame when there is nothing to hold: the track's own
     box, the words centred in it. */
  .lyr-empty { position: absolute; left: var(--sp-5); right: var(--sp-5); top: calc(var(--sp-3) + 8px);
    height: 32px; line-height: 32px; text-align: center; font-size: var(--fs-meta); color: var(--text-faint); }

  /* ── Tile row ─────────────────────────────────────────────────────────── */
  /* The row is a flex wrap holding the source tiles then the hand tiles —
     the grid cell belongs to the wrap so the source group never scrolls
     away with an overflowing tile row. */  body .tilebar {
    display: flex; align-items: center; flex: 1 1 auto; min-width: 0; gap: var(--seam);
    overflow-x: auto; overflow-y: hidden; height: 100%;
  }
  /* ── The toolbox strip (#253) ───────────────────────────────────────────
     ONE edge-to-edge footer: source · lens · loop · grain · erase, every tile
     drawn by the same rules, groups told apart by a caption and a vertical
     rule rather than by each having its own shelf. Before this the source
     tiles carried live meters and their own sizes, and the lens was penned
     into the right column beside the pinned rail — three species of control
     for one job. The toolbox is the CATALOGUE: a tool on the palette is still
     drawn here, so this strip always shows everything the app has. */
  body .srcbar,
  .src-tile.on .tile-nm { color: var(--text-bright); }
  .src-sampler .tile-nm { max-width: 100%; }
  .srcbar.flash,
  .tile.flash { animation: src-refuse 0.3s; }
  @keyframes src-refuse { 50% { background: rgba(224, 96, 96, 0.25); } }

  /* ── The sampler's sheet (#288) ────────────────────────────────────────
     It is an engine page like any other: the same head, the same type scale,
     the same action buttons. Its own head markup is what gave its two
     buttons the browser's default font — they set no family at all. */
  .src-sheet { display: flex; flex-direction: column; gap: 0.2rem;
    padding: 0.55rem 0.9rem; font-size: var(--fs-control);
    color: var(--text-dim); overflow-y: auto; }
  .ds-editbtn.on { color: #e83030; border-color: rgba(232, 48, 48, 0.5); }
  /* The pad's key sticker is its own column after the name (2026-09-30). The
     NAME takes the slack now and ellipsizes: with a fixed 9.5rem name the row
     was ~406px in a 351px rail, so `del` (and with the sticker, ▶) sat past
     the rail's edge — measured, not seen, since del only shows on hover.
     Every column but the name is a fixed width, so the rows' columns agree
     (`auto` columns drifted with each digit's glyph and with ▶ against ■).
     The row is not clickable — there is no current sample to pick. */
  .src-row { display: grid; align-items: center; gap: 0.55rem;
    grid-template-columns: 8px minmax(0, 1fr) 1.6rem 64px 2.6rem 1.9rem 1.8rem;
    padding: 0.3rem 0.45rem; border-radius: 3px;
    font-family: var(--font-sans); }
  .src-row:hover { background: var(--bg-panel); }
  .src-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
  .src-row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap; }
  .src-rn { width: 100%; background: var(--surface-1); border: none; border-radius: 3px;
    color: var(--text-bright); font: inherit; padding: 0.05rem 0.25rem; }
  /* The sampler's sheet answers to `--eng-source`, not to the settings pages'
     teal: a surface wears its subject's hue (2026-09-22). */
  .src-rn:focus { outline: 1px solid var(--eng-source); }
  .src-row-wavewrap { height: 30px; min-width: 60px;
    background: var(--bg-app); border-radius: 2px; overflow: hidden;
    position: relative; }
  .src-row-wave { display: block; width: 100%; height: 100%; }
  .src-row-ph { position: absolute; top: 0; bottom: 0; left: 0; width: 0;
    background: rgba(232, 200, 64, 0.14);
    border-right: 2px solid #e8c840;
    opacity: 0; transition: opacity 0.15s; pointer-events: none; }
  .src-row-dur { color: var(--text-faint); text-align: right;
    font-family: var(--font-mono, monospace); font-variant-numeric: tabular-nums; }
  .src-row-play { background: none; border: 1px solid var(--border-faint);
    border-radius: 3px; color: var(--text-dim); cursor: pointer;
    font-family: var(--font-sans); font-size: var(--fs-meta);
    padding: 0.16rem 0.5rem; }
  .src-row-play:hover { color: var(--text-bright); }
  .src-row-play.playing { color: var(--eng-source); border-color: var(--eng-source); }
  /* Same affordance as a custom tool's (#284): quiet until the row is
     hovered, and three letters rather than a glyph that also means close. */
  .src-row-del { padding: 0.16rem 0.35rem; border-radius: 3px;
    font-family: var(--font-sans); font-size: calc(var(--fs-meta) * 0.95);
    letter-spacing: 0.04em; color: var(--text-faint);
    opacity: 0; transition: opacity 0.1s, color 0.1s; cursor: pointer; }
  .src-row:hover .src-row-del { opacity: 1; }
  .src-row-del:hover { color: #e57373; background: rgba(229, 115, 115, 0.12); }
  .src-sheet .seg span { cursor: pointer; }
  .src-empty { color: var(--text-faint); font-size: var(--fs-meta);
    padding: 0.5rem 0.45rem; }
  .tile { position: relative; flex: 0 0 auto; width: 4.6rem; min-width: 0; display: flex;
    flex-direction: column; align-items: center; justify-content: center; gap: 0.28rem;
    background: var(--bg-app); border: none; border-top: 2px solid transparent;
    padding: 0.6rem 0.3rem 0.5rem; cursor: grab; color: var(--text-dim);
    font-family: var(--font-sans); transition: background 0.14s, color 0.14s, border-color 0.14s; }
  .tile:hover { background: var(--bg-panel); color: var(--text-bright); }
  /* Selected-but-idle is ARMED, not sounding: a quiet accent underline. The
     bright fill is reserved for .playing — the moment the gesture is down
     (digit held, or space/pedal painting through the selection). Scopes keep
     the filled active look: a lens is genuinely always on. In design view
     the selected tile goes strong again — there it means "editing this". */
  /* ── The palette and the toolbox (#248) ──────────────────────────────────
     The palette sits in a raised bed at the head of the row: the tiles space
     reaches and Tab cycles. The toolbox follows, grouped by engine, in the
     row's own ground so the two read as different shelves without a rule
     between them. */
  /* ── The palette dock (#250) ──────────────────────────────────────────────
     The palette floats over the lower stage, centred and large: it is the
     instrument, so it belongs on the instrument's surface, at the size of a
     thing you hit rather than a thing you read. Properties has its own grid
     row below the tile row (#251), so the palette has nothing to dodge. */
  .palette-dock { display: none; }
  body .palette-dock {
    display: flex; justify-content: center; max-width: 100%; gap: 0.75rem;
    grid-column: 1; grid-row: 1; align-self: end; justify-self: center;
    margin-bottom: 1.1rem;
    z-index: 4;
    /* Pointer-transparent: the gaps between tiles are still sphere, and
       painting there must work. Tiles opt back in. */
    pointer-events: none;
  }
  /* ── The echo, above the palette (2026-09-26) ──────────────────────────
     One pill, centred on the dock (which is the palette's width), a --sp-4
     above it: the action and its state in the chrome's light value at the
     control size, the input in the footer's engraved caption. The kit's pill
     — 24 tall, r999, the app ground and one hairline, like the stickers. A
     miss is ochre. Opacity only; it holds a second (tile-layout.js). */
  body .palette-dock { position: relative; }
  .pal-echo { display: none; }
  body .pal-echo {
    position: absolute; left: 50%; bottom: calc(100% + var(--sp-4)); transform: translateX(-50%);
    display: flex; align-items: center; gap: var(--sp-3);
    height: 24px; box-sizing: border-box; padding: 0 var(--sp-4); max-width: 100%;
    border-radius: 999px; background: var(--bg-app); border: 1px solid var(--border-subtle);
    white-space: nowrap; pointer-events: none;
    opacity: 0; transition: opacity var(--m-decay) var(--ease-out);
  }
  body .pal-echo.on { opacity: 1; transition-duration: var(--m-touch); }
  /* Each discrete press fades in again from nothing, so a repeat of the same
     words still reads as a new press (tile-layout.js restarts it). */
  body .pal-echo.on.is-again { animation: pal-echo-again var(--m-state) var(--ease-out); }
  @keyframes pal-echo-again { from { opacity: 0; } to { opacity: 1; } }
  body .pal-echo > span { overflow: hidden; text-overflow: ellipsis; line-height: 1; }
  body .pal-echo-what { font-size: var(--fs-control); color: var(--text-light); font-variant-numeric: tabular-nums; }
  body .pal-echo.is-miss .pal-echo-what { color: var(--accent-warn); }
  body .pal-echo-src:empty { display: none; }
  body .pal-echo-addr { text-transform: none; letter-spacing: 0; }
  /* FLAT (#257, Ek): the blur was the one material on screen that existed
     nowhere else, and a drop shadow under a panel on a black stage buys
     nothing. Opaque ground, one hairline, no shadow. */
  .palette { display: flex; flex-wrap: wrap; justify-content: center;
    flex: 0 1 auto; gap: var(--sp-2); padding: var(--sp-2); max-width: 100%;
    pointer-events: auto;
    background: var(--bg-app);
    border: 1px solid var(--border-faint); border-radius: 4px; }
  /* ── The palette is a row of SYMBOLS (2026-08-29) ──────────────────────────
     Palette tiles are the big targets — hit, not read, found with a glance from
     behind an instrument. Portrait tiles spent 95px of the lower stage, a
     third of it, over the sphere, to say three words. They are squares now,
     at the footer's height, so the two strips that frame the stage agree
     instead of one dwarfing the other.
     Nothing but the glyph is in them (Ek): "it's usually an intentional
     design dragged from the tool, so we know what it is by the symbol". The
     name and the digit both said what the glyph and the POSITION already say
     — palette order IS the keymap, so the first square is `1` wherever it sits,
     and the chrome names the armed tool in words at the top left. So the
     glyph carries the engine hue itself; a separate colour mark on a square
     this size would be a second thing to look at for the same answer. */
  /* THE VERB IS THE SHAPE (docs/PALETTE-GUI.md § 3). One 53px cell, one closed
     outline, ONE property — `border-radius`, written per tile by tiles.js as
     `--pal-r` so the table that decides it lives in one place (VERB_RADIUS)
     and palette-audit § I can read the computed value back off the strip:

       bang       999px                one edge, no span — a point; nothing to hold
       momentary  var(--r-hand)  10px  two edges, the span is your finger
       toggle     24px 3px 24px 3px    two edges, the span is open until you close it

     Nothing is missing from the outline and nothing is added inside it, so no
     tile can look broken and none of them grows. The toggle's two rounded and
     two square corners give it a DIRECTION — the one thing a latch has and a
     spring does not. It was a flat 3px for every tile until 2026-09-11, when
     the tile stopped having three verbs at once and got one worth drawing. */
  .palette .tile { width: var(--pal-tile, 53px); height: var(--pal-tile, 53px); padding: 0; gap: 0;
    border-radius: var(--pal-r, 3px); border-top: none; }
  /* HUE IS IDENTITY, BRIGHTNESS IS STATE (PALETTE-GUI § 11.3): the glyph wears
     its family's hue at rest — eight hues computed and eight grey glyphs drawn
     was the bug — and state moves to the tile's ground: rest surface-1, hover
     surface-3, lit the hue at 20 % with a 2px border and a glow. */
  .palette .tile svg { width: 1.85rem; height: 1.85rem; color: var(--c); }
  .palette .tile .tile-nm { display: none; }
  /* The KEY LEGEND (Ek, 2026-09-06) — the rule above held while the digit was
     always the position; the keys page can move it now, and a MIDI pad can
     hold it too, so the tile says what fires it today (tiles.js
     paletteLegend). Bottom-left, mono nano, the row's fourth corner: wet
     top-left, off-factory top-right, the MIDI tag bottom-right. */
  .palette .tile { position: relative; }
  /* The legend LINE under the tile (2026-09-11 → 12) is gone: the binding is
     a STICKER on the tile's bottom-left corner — § 24. */
  /* `.palette .tile.armed` drew the BOX — the one mark that said what space
     would fire. Arming went on 2026-09-11 and the box with it: a palette tile
     carries no idle state at all now, so LIT is the only thing the strip ever
     says, and it no longer has to win a specificity contest to say it (it
     lost one on every property until 2026-09-04, and only the cursor said the
     tool was live — Ek). Filled in the engine's hue, flat. */
  .palette .tile.playing {
    background: color-mix(in srgb, var(--c) 20%, var(--bg-app));
    box-shadow: inset 0 0 0 2px var(--c), 0 0 20px color-mix(in srgb, var(--c) 45%, transparent); color: var(--text-bright); }
  .palette .tile.playing svg { color: var(--c); }

  /* ── The pin pair (#252) ───────────────────────────────────────────────
     Same shelf and same size as the tools, a gap apart. The GROUP is the
     whole state here, so it is said twice — in the label and in the colour,
     both taken live from the layers rail. There is no resting state: these
     flash on the press and that is all. */
  
  /* The press IS the feedback — a pin does not sustain, so there is no
     `playing` state to fall back on. */
  
  .tbx-grp { display: flex; flex-direction: column; min-width: 0; }
  .tbx-lbl { font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow); text-transform: uppercase;
    /* THE HUE ITSELF. This was `color-mix(in srgb, var(--eng) 55%, var(--text-faint))`
       — the engine hue pulled 45% of the way into the faintest grey on the ramp,
       which is what made the section titles read as the older pastel set beside
       their own rows (Ek, 2026-09-13: "the titles of each section in the left rail
       bar are not as vibrant as the colours of each item"). A caption that names a
       group is the one place the hue should be at full strength: it is the legend
       for everything under it. Every hue clears 5.8:1 against --bg-app at full. */
    color: var(--eng);
    padding: 0.15rem 0 0 0.34rem; }
  /* The `+` on an engine's title (2026-09-10) — the ⋯'s box and hover; negative
     vertical margins keep it out of the title's height, the marks' rule.
     Letter-spacing is the eyebrow's, which would push a lone glyph off-centre.

     FLUSH RIGHT (Ek, 2026-09-22, evening). It sat just right of the word for
     twelve days — "just to the right of the title", after a first hour flush
     right — and that was the right answer when the rail had ONE of them. It has
     three now (cursor · shape · voice presets), and measured they landed at x
     136.8, 125.1 and 123.8 inside their cards, because each one follows the
     length of its own label: three buttons doing one job on three edges, in a
     column 294px wide. Flush right puts all three at 259.6 — the column the
     drawer buttons on every row below already hold — so the `+` reads as the
     list's own control rather than as punctuation after a word. `margin-left:
     auto` is the whole mechanism; the label is already a flex box. */
  /* 24, the kit's height (ruled 2026-09-15). */
  .tbx-add { display: flex; flex: none; margin-left: auto; margin-top: -0.3rem; margin-bottom: -0.3rem;
    height: 24px; box-sizing: border-box;
    width: 1.4rem; align-items: center; justify-content: center;
    border-radius: var(--r-2, 3px);
    color: var(--text-tertiary); cursor: pointer; }
  /* Drawn, so the box centres it (2026-09-16); 12px puts its ink at the
     title's cap height. */
  .tbx-add svg { width: 12px; height: 12px; display: block; }

  .tbx-add:hover { color: var(--text-bright); background: var(--surface-3); }
  .tbx-tiles { display: flex; gap: var(--seam); min-width: 0; }
  /* THE VOICE LINE'S DOOR (2026-09-24) sits after the `+`, the kit's 24px box;
     lit in the engine hue while its sheet is up. */
  .tbx-lbl .trow-more { margin-left: 0.1rem; margin-top: -0.3rem; margin-bottom: -0.3rem; }
  /* THE RULED LABEL (Ek, 2026-09-25 — the rails canvas, option A): a section
     title in the rail's cards — Voice, Grain Behaviour — is sentence case at
     the body size, semibold, and a hairline runs from it to the right edge;
     the `+` and the door sit at the rule's end. The engine titles keep the
     eyebrow. */
  /* A CARD'S HEAD (the rails canvas, F): each section is its own card and
     this is its top — 40 tall, sentence case, semibold, light, over a
     hairline; the `+` and the door flush right. */
  .tc-lrail .tbx-lbl--sec { height: var(--card-head); box-sizing: border-box; padding: 0 var(--sp-5); margin-bottom: var(--sp-2);
    font-size: var(--fs-control); font-weight: 600; letter-spacing: normal; text-transform: none;
    color: var(--text-highlight); gap: var(--sp-4); border-bottom: 1px solid var(--border-soft); }
  .tbx-lbl--sec .tbx-lbl-t { flex: 1 1 auto; }
  .tbx-lbl--sec .tbx-add { margin-left: 0; }
  /* THE CARDS (F): the instrument's, then the voice's under it; the cursor's,
     then the grain behaviour's. 8 between the two halves of a region, 12 after. */
  /* THE BEDS TWO STEPS UP THE RAMP (Ek, 2026-09-26: "make the rail beds higher
     contrast i barely notice it"): --surface-1 (5 %) → --surface-3 (8.5 %), in
     both rails, with the tabs moving by the same two steps so the chosen tab
     is still the card and the closed ones still sit one level behind it. */
  .tc-lrail .rail-card { background: var(--surface-3); border-radius: var(--r-hand); padding-bottom: var(--sp-5); }
  /* A card that opens on a ROW gets the 12 it ends with above it too — the
     pinned rail's mode card is the reference (Ek, 2026-09-26: the cursor
     card's scope row sat on its top edge). Head- and tab-led cards keep 0. */
  .tc-lrail .rail-card:has(> .mrow:first-child) { padding-top: var(--sp-5); }
  .tc-lrail .rail-card + .rail-card { margin-top: var(--sp-4); }
  .tc-lrail #voicePanel { margin-bottom: var(--sp-5); padding-bottom: var(--sp-3); }
  .tc-lrail #voicePanel .trow--voice:last-child { border-bottom: none; }
  .tbx-lbl .trow-more.open { color: var(--c, var(--text-bright)); }
  /* A PRESET'S DELETE, at the edge its door left: the same box, red on hover.
     A factory preset draws none (2026-09-24). */
  .trow-del { display: flex; flex: none; margin-left: auto; width: 1.4rem; height: 24px;
    align-items: center; justify-content: center; border-radius: var(--r-2, 3px);
    color: var(--text-tertiary); cursor: pointer; }
  .trow-del svg { width: 0.85rem; height: 0.85rem; }
  .trow-del:hover { color: var(--status-error); background: var(--tint-danger-soft); }
  /* A GROUP WITH NO LABEL STILL NEEDS THE LABEL'S AIR (2026-09-22, evening).
     The instrument's two mode switches lead the card now and the MODE heading
     is gone, so nothing was holding the first switch off the tab above it: a
     label's own `padding-top` was the only thing making that gap and it left
     with the label. Same 12px, stated where the label used to state it.
     (A rule divided the picks from the set-once rows for an hour the same
     evening and came out again — the labels' top padding and the plain fact
     that a switch is not shaped like a preset row were already doing it.)

     Scoped to `.tc-lrail` because the rule that zeroes every group's padding
     is two classes deep, and one class loses to it — measured at 0px. */
  .tc-lrail .tbx-grp--bare { padding-top: var(--sp-5); }
  /* ── THE INSTRUMENT TABS (2026-09-22) ──────────────────────────────────
     ONE row, icons only: each tab wears the thing's own glyph, so the row is
     read by shape and hue like the rest of the instrument rather than by five
     words in a column of eleven-pixel type. The row is the segmented element at
     the kit's 24, r2, no padding on the container; the selected tab takes its
     own engine hue, because hue is identity and a tab IS the engine. */
  .instr-tabs { display: flex; padding: var(--sp-5) var(--sp-5) var(--sp-4); }
  /* THE TABS ARE THE PINS RAIL'S PILL (2026-09-22). The markup is `.seg-pill`
     > `.grain-seg-btn`, so everything about the shape, the ground and the
     chosen state arrives from the kit; only two things are the tabs' own. */
  /* THE EYE IS ITS OWN BAND (Ek, 2026-09-22: "give the lens special design
     treatment"). It is not a tool and not an instrument, so it should read as
     neither: its own ground in its own hue, held in a bounded band above the
     tabs. FLAT — a tinted surface and a hairline, no shadow and no bloom, per
     the build sheet's always-wrong § 7; the bench's plate is a raised object you
     pick up, and the eye is not one.
     The hue is `--eng-lens`, mixed down rather than a new colour (§ 8.3).

     SPECIFICITY, stated because it bit: `.tc-lrail #lensBar` a few hundred lines
     up resets border/margin/padding on these bars (they were horizontal shelves
     once), and an ID beats any number of classes — so this carries the id too.
     The row rule below carries `html body … .lyr-list` for the same reason. */
  /* THE PANEL CARD (Ek, 2026-09-22: "i asked for this simple design, but for
     the lenses"). A ground a step up from the rail and the hand's radius —
     nothing else. The rows still bleed out of it to the rail's edge, so the
     moons sit on the rail and the one glyph column holds; the card is a GROUND
     BEHIND a group, not a box around it.

     It arrived by accident two days after it was wanted: the `height: auto`
     rule above lost its declaration block when a third selector was deleted,
     its two survivors ran on into `html body .palette`, and the presets panel
     came up wearing the hand's plate — gradient, shadow and a 9px backdrop
     blur. Ek saw it and asked for it, so it is stated here instead, FLAT: no
     gradient and no shadow (build sheet § 8.7), one token for the ground and
     one for the radius (§ 8.8). */
  /* (2026-09-25, the rails canvas, F: a SECTION is now a card of its own —
     Voice under the instrument's card, Grain Behaviour under the cursor's —
     side by side, never nested. What follows is the older reading.)
     ONE CARD PER REGION, never a card inside a card. The INSTRUMENT is one —
     mode, its presets — and the things inside it are SECTIONS of that card,
     told apart by their captions, not by grounds of their own. A box drawn
     around boxes is the thing this replaced. (`#lensBar` was a card until
     2026-09-22 night; the lens is a tab of the instrument card.) */
  /* --surface-3 with the other beds (2026-09-26): the contrast round moved
     `.rail-card` and the chosen tab two steps and missed this one, so the
     instrument's card sat at 0.05 under a chosen tab at 0.086 — the tab no
     longer WAS its card. align-audit's mode-card check caught it at release. */
  .tc-lrail #globalModes, .tc-lrail #instrPanel {
    background: var(--surface-3);
    border-radius: var(--r-hand);
  }
  /* ONE GAP BETWEEN BANDS, and it is the same `--sp-5` as the gutter either
     side of them, so the air around a card is equal on all four edges. It was
     16.9 · 12 · 8 · 0 — four values, none of them off the scale on purpose. */
  .tc-lrail #globalModes { margin-bottom: var(--sp-5); }
  /* THE RAIL HEADING (Ek, 2026-09-22: "tool creator should be big like the word
     TOOLS and have the rail wide divider under"). It is the rail's own header
     type — `.lyr-title`, the same object `TOOLS` is — over the same hairline
     `.lyr-bar` carries, and it BLEEDS to the rail's edges so the rule runs the
     full width rather than stopping at the 13→230 column. That is what makes it
     read as a division of the rail instead of a caption inside it. */
  .tc-lrail .tbx-rail-hd {
    margin: var(--sp-6) calc(-1 * var(--sp-5)) var(--sp-6);
    padding: var(--sp-4) var(--sp-5);
    border-bottom: var(--seam) solid var(--border-faint);
  }
  /* THE PANEL IS THE TAB'S, and the outline says so: it carries the lit tab's
     hue and starts directly under the pill, so the colour runs down from it.
     The border sits on `--seam` OUTSIDE the 13→230 column — negative side
     margins of exactly its own width — so the bands inside keep the one column
     every other row in the rail is on. */
  /* THE HUE EXTENDS FROM THE TAB — the one that is lit, at its width, not the
     whole strip (Ek, 2026-09-22: "a big border around both the mode and tool
     creator that extends from the selected tab in the colour of that type").
     The pill sits ON the card's top edge and a 2px bar of the tab's own colour
     bridges the two: `--tab-x` and `--tab-w` are written by `render`, which
     knows which tab is lit and how wide it is. Flat — a bar, not a glow. */
  .tc-lrail .instr-tabs { margin-bottom: 0; }
  /* The strip and the card touch, and the card's TOP corners are square: the
     tabs carry the rounding, and they span the full width so no square corner
     is ever left showing. */
  .tc-lrail #instrPanel { border-radius: 0 0 var(--r-hand) var(--r-hand); }
  .tc-lrail #instrPanel #creatorBand { margin-bottom: 0; }
  /* The card's own air: the first caption sits under the bar, the last row
     off its bottom edge. */
  .tc-lrail #instrPanel { padding-bottom: var(--sp-5); margin-bottom: var(--sp-4); }
  /* THE CURSOR SECTION (Ek, 2026-09-23): the rail's lower half, pinned to its
     foot at the height of its own rows, so the tools' list above takes the
     rest and scrolls. Its bar is the rail header's, with the rule ABOVE as
     well as below — a division of the rail at its full width. Its rows sit on
     the instrument card's ground, fully rounded (no tab sits on it), with the
     same air around them the list gives its card. */
  .tc-lrail .cur-sec { flex: none; }
  .tc-lrail .lyr-bar--sec { border-top: none; }
  .tc-lrail .cur-body { padding: var(--sp-6) var(--sp-5) var(--sp-5); }
  /* The cursor's two cards are drawn inside it (render()), so it is a plain box. */
  .tc-lrail #cursorPanel { background: none; }
  .tc-lrail #cursorPanel > .tbx-grp--bare { padding-top: 0; }
  /* A card's head is its first line, so a card that opens on one carries no
     top inset of its own. */
  .tc-lrail .rail-card:has(> .tbx-lbl--sec:first-child) { padding-top: 0; }
  /* The pill carries no padding of its own: the card above it already ends with
     `--sp-5`, and padding on top of that made 24 above the pill against 12
     below it. Every gap in the stack is one margin now. */
  .tc-lrail .instr-tabs { padding: 0; }
  /* ONE BAND BOX: the caption's 8px at the top, --sp-2 at the bottom, so
     nothing — a row's seated ground or the bench's plate — ever touches the
     card's edge. With the 12 between bands that makes 16 of air under the last
     row of one card and the first caption of the next, everywhere. */
  .tc-lrail #globalModes { padding-bottom: var(--sp-5); }
  /* `#creatorBand` is INSIDE the panel, which already ends with `--sp-5`; its
     own inset made 24 under the last row against 12 everywhere else. */
  .tc-lrail #creatorBand { padding-bottom: 0; }
  /* THE ROWS BLEED, like every other row in this rail. Holding them inside the
     band instead was tried and was wrong twice over: it put the lens glyphs
     13px off the rail's single column, and the shape/voice cards below ALREADY
     let their rows out to the rail edge — so containment made the lens the odd
     one out at the same time as breaking the column. The moon is flush with the
     RAIL, always; the band is a ground behind the rows, not a box around them. */
  /* ── THE TABS ARE THE CARD'S TOP EDGE ─────────────────────────────────
     Not a pill floating above a panel (Ek, 2026-09-22, with a reference): the
     CHOSEN tab IS the card — same ground, square where they meet — so the two
     read as ONE SHAPE, and the rest are marks on the rail. That is also why the
     bar that used to bridge them is deleted: nothing needs bridging when they
     are the same object. The hue appears twice and only twice — the chosen
     tab's glyph, and a short rule under it. No filled chip: a tab that is a
     block of colour shouts louder than the panel it names. */
  .instr-tabs .seg-pill {
    display: flex; width: 100%;
    background: none; border-radius: 0; padding: 0; overflow: visible;
  }
  /* A GLYPH IS CENTRED BY A FLEX BOX, never by a line box (Ek, 2026-09-22: "the
     icons for the tool tab in the tab editor is not centered at all"). The
     button inherits `.grain-seg-btn`, which is text-shaped — uppercase, letter
     spaced, sitting on a baseline — so a `display: block` svg inside it lands
     wherever the line box leaves it, and `justify-content` on a non-flex box is
     ignored outright. The footer's glyph segment states all three; this one
     said one of them. */
  /* EVERY TAB IS DRAWN AS A TAB (Ek, 2026-09-22, evening: "most tabs I can see
     the other unopen tabs in a lighter colour so it's the full tab shown").
     The closed three were bare glyphs on the rail's ground — the row read as
     one tab and three marks beside it, which is why "which of these is a tab?"
     kept being answered by the chosen one alone. They get the tab shape and a
     ground of their own now: `--surface-1`, two steps under the card's
     `--surface-3` (2026-09-26; it was 0 under 1), so the row reads front-to-back — open tab at the card's
     level, closed tabs behind it, rail ground behind them. The 1px `gap` on
     the pill is what separates them, and it is the rail showing through. */
  .instr-tabs .grain-seg-btn {
    flex: 1 1 0;
    display: inline-flex; align-items: center; justify-content: center;
    position: relative;
    border-radius: var(--r-hand) var(--r-hand) 0 0;
    background: var(--surface-1);
    color: var(--text-subtle);
  }
  .instr-tabs .grain-seg-btn:hover { color: var(--text-bright); background: var(--surface-4); }
  /* The glyph is the tile's own 1.05rem, so a tab and a row read as the same
     mark at the same size. `.seg-glyph`'s 20px is the FOOTER's density, and
     this pill sits over a rail of 1.05rem rows. */
  /* CENTRED, now that there is nothing under it to leave room for: the padding
     was `0 --sp-3 --sp-4`, and that bottom 8px was the rule's clearance, not
     spacing — it pushed every glyph 4px above the middle of its own 32px box. */
  /* 40 TALL OVER A RULE, THE CARD HEAD'S OWN SHAPE (Ek, 2026-09-26: "the tabs
     should be the same height as the subtitles (voice/grain behaviour)… add
     that divider line like they have… it's not clear the tabs"). The row is
     the `.tbx-lbl--sec` box — 40 with the rule inside it, --border-soft — so
     a tab strip and a card head read as the same kind of top edge. */
  /* The rule is drawn OVER the foot of the box, not as its border, so the
     tabs fill all 40 — the box they fill is the size they are (the build
     sheet's rule; as a border they came out 39, off the kit, and align-audit's
     R6 said so at the 5.12 release). Same pixels on screen. */
  .instr-tabs .seg-pill { height: var(--card-head); box-sizing: border-box; position: relative; }
  .instr-tabs .seg-pill::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
    background: var(--border-soft); pointer-events: none; }
  .instr-tabs .seg-glyph .grain-seg-btn { height: 100%; padding: 0 var(--sp-3); }
  .instr-tabs .seg-glyph .grain-seg-btn svg { width: 1.05rem; height: 1.05rem; display: block; }
  /* THE CHOSEN TAB CARRIES THE INSTRUMENT'S NAME (2026-09-22, evening), and it
     is the ONE that does: four equal glyphs said which of four you had picked
     and never which one it was. The named tab sizes to its content and the
     other three share what is left, so the row still fills the card's width —
     `flex: 0 1 auto` against their `1 1 0`. The word is the eyebrow, tracked
     like every other label in the rail and in the tab's own hue, which it
     already had from `--c`.

     THE NAME'S OWN AIR (Ek, 2026-09-22, evening: "improve the spacing around
     the tab title"). The word sat in the glyph-only padding — 6px either side
     and 6px off the glyph — which is the spacing of a mark, not of a mark and a
     word together. `--sp-4` between them so the glyph and the name read as one
     object rather than two things touching, and `--sp-5` outside so the chosen
     tab has a margin the plain glyphs do not need. */
  .instr-tabs .grain-seg-btn.active { flex: 0 1 auto; gap: var(--sp-4); padding: 0 var(--sp-5); }
  .itab-nm { font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow);
    text-transform: uppercase; white-space: nowrap; }
  /* THE CHOSEN TAB IS THE CARD: same ground, square at the bottom, so the seam
     between them disappears. Its glyph takes the instrument's hue. */
  .instr-tabs .grain-seg-btn.active {
    background: var(--surface-3);
    color: var(--c, var(--eng-none));
  }
  /* (A short hue rule sat under the chosen glyph until 2026-09-22, evening. It
     was the one mark saying which tab you were in — and the tab says its NAME
     now, in that same hue, so the rule was a third way of saying the same
     thing after the ground and the colour. Removing it is also what let the
     button centre its content: the 8px of bottom padding under the glyph
     existed only to leave room for it.) */
  /* The sampler's panel and the tools panel take turns, and `[hidden]` alone
     cannot do it: `.tc-lrail #srcBar` sets an explicit `display: flex`, and an
     explicit display beats the hidden attribute every time. Stated here, where
     the rule that beat it lives. */
  html body .tc-lrail #srcBar[hidden],
  html body .tc-lrail #tileBar[hidden] { display: none !important; }


  /* ── MODE (2026-09-21) ─────────────────────────────────────────────────
     Two settings in the rail, above the lens. Row height is the rail's own 24,
     and each control fills it or sits inside it at its kit size — the switch is
     32 × 18 with a 14 knob and a 2px inset, the segmented pair is 24 tall with
     no padding and r2, both exactly as the instrument kit states them.

     No engine hue: mode is not an engine. The label takes --eng-none, which is
     the app's own word for that. */
  .mrows { display: flex; flex-direction: column; }
  .mrow { height: 24px; box-sizing: border-box; display: flex; align-items: center;
    gap: var(--sp-4); padding: 0 var(--sp-5); }
  .mrow-l { flex-grow: 1; font-size: var(--fs-control); color: var(--text-secondary); }
  /* A switch's own word when the row's label names something else — k's `all`
     (2026-09-26). The label's type, not growing: it sits against its switch, a step
     further from the number than the row gap so "12 all" does not read as one. */
  .mrow-word { flex: none; margin-left: var(--sp-4); font-size: var(--fs-control); color: var(--text-secondary); }
  /* The switch: 32 × 18, knob 14, 2px inset. Flat, no border — the kit's. */
  .mrow-sw { flex: none; width: 32px; height: 18px; padding: var(--sp-0); box-sizing: border-box;
    border: 0; border-radius: 999px; background: var(--border-soft); cursor: pointer;
    display: flex; align-items: center; justify-content: flex-start;
    transition: background var(--m-state) var(--ease-snap); }
  .mrow-knob { width: 14px; height: 14px; border-radius: 999px; background: var(--text-tertiary);
    transition: background var(--m-state) var(--ease-snap); }
  .mrow-sw.on { background: var(--eng-pins); justify-content: flex-end; }
  .mrow-sw.on .mrow-knob { background: var(--bg-app); }
  /* The segmented pair: 24 tall, container and buttons, r2, selected segment
     NEUTRAL — never ember. Chosen is not exceptional. */
  .mrow-segs { flex: none; display: flex; height: 24px; border-radius: var(--r-2);
    border: 1px solid var(--border-base); overflow: hidden; }
  .mrow-seg { height: 100%; padding: 0 var(--sp-4); border: 0; background: none;
    font-size: var(--fs-eyebrow); color: var(--text-dim); cursor: pointer;
    transition: background var(--m-touch) var(--ease-out), color var(--m-touch) var(--ease-out); }
  .mrow-seg:hover { color: var(--text-bright); }
  .mrow-seg.on { color: var(--bg-app); background: var(--eng-pins); }

  /* ── THE BENCH (2026-09-22) ────────────────────────────────────────────
     ONE TILE, and it is the STRIP'S tile: the bed carries `.palette`, so the
     hand tile inside it is drawn by the same rules, at the same width, with the
     same glyph, name, ground and sticker corner. Nothing here restates any of
     that — the only job left is to sit the bed in the rail.

     The bed keeps the strip's own padding and radius, because a hand tile with
     the strip's air around it IS what the strip shows. */
  /* THE LINE BETWEEN THE INSTRUMENT AND THE TOOL (2026-09-22). Above it: the
     pill and MODE, which are the instrument's. Below it: the bench, the shapes
     and the voices, which are the tool's. The rule is on the SEPARATOR rather
     than on the bench, so the heading sits under the line with its group. */
  /* No rule on it: four bands with equal air between them do not need a line,
     and the line was the only hairline left in the rail. */
  .tc-lrail .tbx-sep { border-top: 0; }
  /* (The BENCH BED's rules stood here until 2026-09-22 — the bed, its equal air
     on three sides, and the grab cursors that said the tile could be dragged
     onto the strip. All of it went with the bench.) */

  /* Box tiles are the same tool, quieter until you point at one. */
  .palette .tile { padding: 0.42rem 0.3rem 0.34rem; }
  /* NOTHING in the strip is dimmed (#257, Ek): a half-opacity control reads
     as muted — that is what dimming means in a DAW — and none of these are.
     A tile that is not current is simply not boxed. */
  .srcbar .tile { padding: 0.42rem 0.3rem 0.34rem; }

  /* The `.tile.armed` box stood here — the one unambiguous mark for the one
     state that decided what space did. Both are gone (2026-09-11). */
  .tile.playing { background: var(--chrome-bg-raised, #262626); color: var(--text-light);
    box-shadow: inset 0 2px 10px -4px var(--c); }
  .tile.fired { background: var(--chrome-bg-raised, #262626); color: var(--text-light); }
  /* A tile whose behaviour is not built says so in its tooltip and its foot,
     not by looking switched off. */
  .tile.ghost .tile-nm { font-style: italic; }
  .tile.dragging { opacity: 0.35; cursor: grabbing; }
  .tile.over { background: var(--chrome-bg-raised, #262626); border-top-color: var(--text-tertiary); }
  .tile svg { width: 1.3rem; height: 1.3rem; color: var(--text-dim); transition: color 0.14s; flex: none; }
  .tile-nm { font-size: var(--fs-control); font-weight: 500; max-width: 100%; overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap; }
  /* `.tile kbd` — the old bottom-left key — is gone; the legend's cap is
     `.leg-cap` under the tile (§ 24). */

  /* ── The .opt vocabulary — chips, tracks, segs ─────────────────────────
     Shared by the properties panel's discrete rows. The options BAR that
     named it is gone (#251); the vocabulary outlived it. */
  
  .opt { display: flex; align-items: center; gap: 0.4rem; font-size: var(--fs-nano); color: var(--text-dim); }
  .opt > i { font-style: normal; }
  .opt > b { color: var(--text-secondary); font-weight: 500;
    font-variant-numeric: tabular-nums; min-width: 2.6rem; }
  /* The visible bar is 2px, the HIT TARGET is the full 18px row — a 2px
     strip cannot be landed on with a mouse, which read as "the settings
     don't work" the first time they were played. */
  .opt .tr { width: 72px; height: 18px; background: transparent; position: relative;
    border-radius: 1px; cursor: ew-resize; flex: none; touch-action: none; }
  .opt .tr::before { content: ''; position: absolute; left: 0; right: 0; top: 8px;
    height: 2px; background: rgba(255,255,255,0.14); border-radius: 1px; }
  .opt .tr .thumb { position: absolute; top: 3px; width: 2px; height: 12px;
    background: var(--text-secondary); border-radius: 1px; }
  .opt .tr.live .thumb { background: var(--grid-teal, #7abcbc); }
  /* The falloff diagram — an x/y plot of gain over distance, dragged
     vertically. The shape carries the meaning, so the styling stays quiet. */
  .opt .fcurve { display: inline-flex; cursor: ns-resize; flex: none; touch-action: none;
    border-radius: 2px; background: rgba(255,255,255,0.03); }
  .opt .fcurve .fc-base { stroke: rgba(255,255,255,0.14); stroke-width: 1; }
  .opt .fcurve .fc-line { stroke: var(--grid-teal, #7abcbc); stroke-width: 1.5;
    fill: none; stroke-linecap: round; }
  .opt .fcurve.fcurve-off .fc-line { stroke: var(--text-faint); }
  /* The rig view's own segments keep the old square chip — the pill rule
     above governs the tile screen, which is the one being designed. */
  /* 24, the kit's height for a segmented container (ruled 2026-09-15) — it
     was 25, one pixel of nobody's choosing. */
  .opt .seg { display: flex; gap: var(--sp-0); background: var(--surface-1); padding: var(--sp-0);
    height: 24px; box-sizing: border-box; border-radius: 3px; }
  .opt .seg span { font-size: var(--fs-nano); padding: 0.22rem 0.5rem; border-radius: 2px;
    color: var(--text-faint); cursor: pointer; }
  .opt .seg span.on { background: var(--chrome-bg-raised, #262626); color: var(--text-light); }


  @media (max-width: 820px) {
    /* Nothing to re-place on a narrow screen any more (#259): the stage is
       one cell and every rail overlays it. They only have to get smaller —
       and the properties rail gives way first, since the tool list is the one
       you navigate with. */
    /* The left rail is DERIVED from the hand tile now and shrinks with it, so it
       states nothing here; only the properties rail, which has no tile to follow. */
    body.prail-open .tc-prail { width: 12rem; margin-left: var(--lrail-w); }
    body.pinned-open .tc-rail { width: 16rem; }

    /* The palette keeps its precedence on a small screen — it is still the
       instrument — but ten squares at full size would overflow a 520px stage,
       and the glyph is now the whole tile, so it gives up the least. */
    /* (the narrow value for --pal-tile lives with the value itself, further down:
       at equal specificity the LATER declaration wins, and this one is earlier) */
    .palette .tile .leg { font-size: 9.5px; }
    .palette .tile .leg-g, .palette .tile .leg-d { font-size: 8.5px; }
    .palette .tile svg { width: 1.55rem; height: 1.55rem; }
  }

  /* ── The engine palette (#257) ───────────────────────────────────────────
   ONE hue per engine, and colour answers exactly one question: which engine
   is this. Fifteen per-tile colours read as confetti on a dark strip; five
   read as a system. Matched chroma and lightness so no group shouts louder
   than another — a tile's own identity is its glyph and its name.
   On :root, NOT on body: the tile row renders before that class
   is set, and a class-scoped palette silently resolved to the fallback. */
:root {
  /* Re-tuned 2026-08-29 to ONE lightness, so no engine shouts over another by
     accident. The roles and the reasoning are unchanged; source and lens lean
     toward the sphere's steel-blue north, grain and erase toward its terracotta
     south, and loop bridges them — so the engine hues are members of the same
     family as the world they are drawn on. */
  /* The --eng-* tokens moved to tokens.css on 2026-09-12 (release 1.15):
     colour comes from tokens.css and nowhere else (docs-audit § 8). */
  /* THE ON-STATE ACCENT — the toggle dot, a focused field, a live value, the
     palette's drop target. `var(--grid-teal, #7abcbc)` is written at ten call
     sites and was never DEFINED, so all ten silently used the fallback: the
     same neon teal the sphere grid just gave up (SPHERE_PALETTE, state.js).
     Declared here, soft enough to sit in this palette and deliberately NOT
     --eng-lens: it answers "this is on", and an engine hue answers only
     "which engine". Renaming it is a separate job — the name is a fossil of
     where the colour came from. */
  --grid-teal:  #85b0a9;
}

/* ── The way back to the tile screen ─────────────────────────────────────
     Top-right of the rig view, accent-bordered, deliberately NOT a
     .top-bar-btn: the return affordance must be at the edge and visibly a
     different kind of thing than fifteen grey chips (the first rig-view
     session ended in a factory reset for want of this). Mirrors the `rig`
     button's corner in the tile chrome. */
  
  
  

  /* ── Engine identity: ONE signal, the glyph's hue ─────────────────────── */
  .tile svg { color: color-mix(in srgb, var(--eng) 62%, var(--text-dim)); }
  .tile.ghost svg { color: var(--text-faint); }

  /* Genuinely unavailable, not muted: it loses its outline and its label
     goes quiet, rather than being drawn at half strength. */
  .tc-btn.tc-dis { color: var(--text-faint); border-color: transparent; cursor: default; }

  /* ── The properties surface ────────────────────────────────────────────
     Fourth form and, I think, the last: page flip (#223) hid the sphere;
     drawer over the stage (#249) moved under you; footer section (#251) held
     still but pushed the stage up; it is now the RIGHT-HAND RAIL of a pair
     (#258) — Ableton's browser and device view — overlaying the stage so
     nothing resizes. The head styles below are shared by every form. */
  /* 24, the kit's height (ruled 2026-09-15). */
  .ds-close { margin-left: auto; flex: none; background: none; border: none;
    height: 24px; box-sizing: border-box;
    color: var(--text-faint); font-size: var(--fs-control); line-height: 1; cursor: pointer;
    padding: 0.1rem 0.3rem; border-radius: 3px; }
  .ds-close:hover { color: var(--text-bright); background: var(--surface-2); }
  .ds-close kbd { font-family: var(--font-mono, monospace); font-size: var(--fs-nano);
    color: var(--text-faint); margin-right: 0.35rem; }
  .ds-head { display: flex; align-items: baseline; gap: 0.7rem; padding: 0.7rem 0.9rem;
    border-bottom: 1px solid var(--border-faint); }
  /* The head is ONE LINE: name, then what it is, then the actions flush right.
     The name never breaks; the description takes the slack and ellipsizes;
     an action pill never wraps. Measured 2026-09-03 on the sampler's sheet,
     where a sentence-long description had wrapped one word per line into a
     46px column beside its two buttons (align-audit "sheet heads"). */
  .ds-head b { font-size: var(--fs-control); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
    flex: none; white-space: nowrap; }
  .ds-head span { font-size: var(--fs-meta); color: var(--text-faint);
    flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* `del`, in every tool's head (2026-09-10): a bare red word beside the
     esc ✕, in the head's own small caps (the WET label's size and tracking),
     no box — the ✕'s own treatment, in --status-error. It was the settings
     Clear-all button, then a second row for it (Ek: "way too big, just a
     simple del red text beside the esc is fine"). Disabled — the last tool
     of a kind, the last lens — it goes faint like the ✕'s kbd. */
  /* 24, the kit's height (ruled 2026-09-15). */
  .ds-del { margin-left: auto; flex: none; background: none; border: none;
    height: 24px; box-sizing: border-box; padding: 0 0.3rem;
    border-radius: 3px; font-family: var(--font-sans); font-size: var(--fs-meta);
    letter-spacing: var(--ls-uppercase); text-transform: uppercase; line-height: 1;
    color: var(--status-error); cursor: pointer; }
  .ds-del:hover { background: var(--tint-danger-soft); }
  .ds-del:disabled { color: var(--text-faint); cursor: default; background: none; }
  .ds-del ~ .ds-close { margin-left: 0; }
  /* Edit-sheet head buttons — mint a tile / release the stroke. */
  /* 24, the kit's height (ruled 2026-09-15). */
  .ds-editbtn { margin-left: auto; flex: none; white-space: nowrap;
    height: 24px; box-sizing: border-box;
    font-family: var(--font-sans); font-size: var(--fs-nano);
    letter-spacing: 0.06em; text-transform: uppercase; background: var(--surface-1);
    border: 1px solid var(--border-soft); color: var(--text-secondary);
    border-radius: 999px; padding: 0.24rem 0.8rem; cursor: pointer; }
  .ds-editbtn + .ds-editbtn { margin-left: 0.5rem; }
  .ds-editbtn:hover { color: var(--text-light); border-color: var(--text-tertiary); }
  /* The drawer's shape is wide and short, so sections COLUMN-PACK rather
     than stack — a section is a self-contained panel, which is exactly what
     `break-inside: avoid` wants. One property does the whole layout, and it
     degrades to a single column on a narrow window without a media query. */
  .ds-body { column-width: 21rem; column-gap: 0.4rem; padding-bottom: 0.5rem; }
  .ds-sec { break-inside: avoid; }
  /* Sections read like device panels: thin uppercase header, a rule, a row
     of cells. Knobs carry the tile's colour in their value arc. */
  .ds-sec { padding: 0.55rem 0.9rem 0.2rem; }
  .ds-sec-h { font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow); text-transform: uppercase;
    color: var(--text-secondary); padding-bottom: 0.3rem; border-bottom: 1px solid var(--border-faint);
    margin-bottom: 0.5rem; }
  /* A section that IS a thing you switch on (the filter) wears its switch on
     the heading, at the right edge, the heading's own rule under both. */
  .ds-sec-h--sw { display: flex; justify-content: space-between; align-items: center; }
  .ds-sec-cells { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; align-items: flex-start; }
  
  
  .ds-chips { display: flex; flex-direction: column; gap: 0.2rem; }

/* ═══════════════════════════════════════════════════════════════════════════
   THE 2026-08-29 DESIGN PASS (#293)
   ───────────────────────────────────────────────────────────────────────────
   Prototyped live over the running app before landing here, so every rule below
   was looked at on the rig rather than reasoned about in a stylesheet.

   Kept as ONE block on purpose while the look is still moving: these rules
   override their originals higher up the file, and the intention is to dissolve
   each one into its home rule once it settles rather than leave a shadow
   section behind. If you are reading this after that has happened, this comment
   is the leftover — delete it.

   The colour and scale work is all in tokens.css; this is only the structure
   those tokens could not reach on their own.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ═══ 1. THE STAGE IS CONTINUOUS ══════════════════════════════════════════
   The bars were #161616 against a #000 canvas: two grey bands framing the
   sphere like a browser window. With --bg-app dropped to #0b0a09 they sit one
   step above the canvas and a hairline does the separating, so the instrument
   reads as one dark field with the sphere burning a hole in the middle. */
html body .tc-bar {
  background: var(--bg-app);
  border-bottom: 1px solid var(--border-faint);
  padding: 1.15rem 0.9rem;
}
html body .bottom-bar {
  background: var(--bg-app);
  border-top: 1px solid var(--border-faint);
  padding-top: 0.1rem;
  align-items: stretch;
}

/* ═══ 2. THE NAME STOPS SHOUTING ══════════════════════════════════════════
   700-weight near-white at 0.92rem, dead centre, was the loudest text on a
   stage whose whole point is that the material is the loudest thing — and a
   performer standing at an instrument has never needed to be told which one it
   is. Same size, same place, so it still titles the window; at 450 weight,
   tracked out and two steps down the ramp it is an engraving on the case
   rather than a headline. It warms on hover, for whoever wants to look. */
html body .tc-brand b {
  font-weight: 450;
  font-size: var(--fs-md);
  letter-spacing: var(--ls-display);
  color: var(--text-tertiary);
  transition: color var(--m-decay) var(--ease-out);
}
html body .tc-brand:hover b { color: var(--text-secondary); }
html body .tc-brand i {
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-display);
  color: var(--text-faint);
}

/* THE OPEN PIECE, and whether it is saved.

   The name is a WORD, so it sits at --text-tertiary — --text-faint is for
   disabled marks and would make the one piece of text here that names your work
   the hardest thing in the chrome to read.

   The unsaved dot is the only dot in the line. A separator dot before the name
   was the first try and it failed the thing it was for: two faint dots either
   side of a word read as decoration, and you could not tell which one meant
   something. The gap does the separating now, and the dot left over says exactly
   one thing. It is brighter and bigger than the name it follows — it is the
   signal, and the name is only what the signal is about. */
html body .tc-doc {
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-display);
  color: var(--text-tertiary);
  margin-left: 0.25rem;
  max-width: 14rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
html body .tc-doc.is-dirty::after {
  content: '\2009•';
  color: var(--text-light);
  font-size: 1.7em;
  line-height: 0;
  vertical-align: -0.04em;
}

/* ═══ 3. ONE LETTERING SYSTEM ═════════════════════════════════════════════
   Ek, 2026-08-29: "I like how SENSOR is written, it's spaced out — use that
   same font style in similar places."

   He is naming the thing the footer was missing. PINNED, SENSOR, AZ/EL/RO, DRY
   and the five level captions were four different sizes at three tracking
   values, so uppercase micro-type read as leftover labels rather than as the
   instrument's engraved lettering. Uppercase at this size only works when it is
   ONE object used consistently. --fs-eyebrow + --ls-widest IS that object; this
   is every place it belongs. */
html body .lyr-title,
html body .bb-cap,
html body .tc-stats,
html body .bottom-bar-label,
html body #tcAudioSlot .grain-label,
html body .bb-ax i,
html body .pal-echo-src,
html body .bb-mute i {
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-widest);
  text-transform: uppercase;
  font-style: normal;
  font-weight: 500;
  color: var(--text-secondary);
}
/* The captions under a meter or an icon are one step quieter than a group
   caption — they name a thing you are already looking at. ONE step: the first
   cut put them three steps down at 2.06:1, which is a mark, not a word. */
html body .bottom-bar-label,
html body .bb-ax i,
html body .pal-echo-src,
html body .bb-mute i { color: var(--text-tertiary); font-weight: 400; }

/* Mute takes its name, so it and DRY read as the pair they are (Ek). The
   button was svg-only, so it needs the axis buttons' column layout to hold a
   caption without the glyph jumping. */
html body .bb-mute {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  height: auto;
}

/* ═══ 4. ONE BASELINE ACROSS THE FOOTER ═══════════════════════════════════
   The footer's raggedness was never misalignment — everything in it was already
   vertically centred. It was that the captions sat on THREE different baselines
   (y847 / y850 / y854 at 1440×900), because each group sized its own stack.
   Bottom-aligning the three sides puts one engraved line of lettering across
   the whole width of the instrument, which is what makes it read as a monitor
   strip rather than as leftovers. */
html body .bottom-bar .bb-side,
html body .bottom-bar-levels { align-items: flex-end; padding-bottom: 0.5rem; }
html body .bottom-bar-meters { align-items: flex-end; gap: 0.5rem !important; }
html body .bottom-bar .bb-ax { padding-bottom: 0; }

/* Every meter column the same width and centred on its own caption, so five
   meters read as one instrument and not as five things that ended up next to
   each other. */
html body .bottom-bar-meters .levels-meter-col {
  min-width: 2.6rem;
  align-items: center;
  justify-content: flex-end;
}
html body .bottom-bar-meters .as-multi-meter-wrap {
  justify-content: center;
  margin: 0 auto;
  background: var(--surface-0);
  border: 1px solid var(--border-faint);
}
html body .bottom-bar-gate .main-gate-meter { margin: 0 auto; }

/* The level rows: the label gets a fixed column so DRY VOL and MASTER start at
   the same x, the readout is tabular and right-aligned, and the fader stops
   being 42px — you are expected to set a master level with it. */
html body .bottom-bar .grain-row { gap: 0.5rem; }
/* In rem, not px: this column held "DRY VOL" at a fixed 68px, which is exactly
   the kind of value that survives a type change by clipping. */
html body .bottom-bar .grain-label { width: 4.8rem; min-width: 0; text-align: right; }
html body #tcAudioSlot .grain-slider { width: 5.2rem; flex: none; }
html body .bottom-bar .grain-numbox {
  min-width: 3rem; text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-meta); color: var(--text-secondary);
}

/* A muted send is worth saying, but ember made it the brightest object in the
   footer — brighter than the meters, which are the thing you actually watch. */
html body .bb-dry.is-mute, html body .bb-rev.is-mute { color: var(--accent-warn); opacity: 0.82; }
html body .bb-dry.is-mute:hover, html body .bb-rev.is-mute:hover { opacity: 1; }

/* The sensor block is a button now (see ui-sensor click wiring): it should look
   like one on hover without becoming a box at rest. */
html body .sensor-group { cursor: pointer; border-radius: var(--r-2); }
html body .sensor-group:hover { background: var(--surface-1); }
html body .sensor-group:hover .bb-cap { color: var(--text-secondary); }

/* ═══ 5. ONE HORIZONTAL SLIDER ════════════════════════════════════════════
   Ek: "the slider design in the footer is not the same design as we made in the
   engine sheets — in the engine sheet you see the colour from the left to the
   tick mark."

   #288 gave the settings pages the engine row's geometry (2px track, 2px
   handle, coloured fill to the handle) but only while a panel was hosted inside
   the settings modal, and never to `.grain-slider` at all. So the app had three
   horizontal sliders: the engine row, the settings copy of it, and a plain grey
   track in the footer and everywhere else.

   One design, everywhere. The fill is the part a native range cannot draw once
   `appearance: none` removes it, so the percentage is written onto the element
   as `--fill` — now maintained globally by a delegated listener in
   ui-settings.js rather than only on settings pages. Without it the track is
   simply flat and the handle still reads correctly: it degrades to a plain
   slider, never to nothing. */
html body .grain-slider,
html body .as-slider {
  -webkit-appearance: none; appearance: none;
  height: 2px; border-radius: 1px; border: none; outline: none;
  padding: 0; cursor: ew-resize; min-width: 0;
  accent-color: auto;
  background: linear-gradient(to right,
    color-mix(in srgb, var(--c, var(--accent-lock)) 75%, transparent) 0 calc(var(--fill, 0) * 1%),
    var(--surface-4) calc(var(--fill, 0) * 1%) 100%);
}
/* margin-top: 0, and it matters. The settings rule this geometry came from
   styles `::-webkit-slider-runnable-track` explicitly, so its thumb is placed
   from the track's TOP edge and needs -4.5px to centre on a 2px track. Here the
   input element IS the track (height: 2px, no runnable-track rule), so the
   browser has already centred the thumb and that same -4.5px shifts it 2.3px
   ABOVE the line — visible as a tick hanging high, which is not what the engine
   rows do: `.prow-h` is absolutely positioned at top:50% with margin-top of
   exactly half its height. Copying the number without the structure it belonged
   to is what broke it. */
html body .grain-slider::-webkit-slider-thumb,
html body .as-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 2px; height: 11px; margin-top: 0;
  border: none; border-radius: 1px;
  background: var(--c, var(--accent-lock));
  cursor: ew-resize;
  transition: height var(--m-touch) var(--ease-out);
}
html body .grain-slider:hover::-webkit-slider-thumb,
html body .as-slider:hover::-webkit-slider-thumb { height: 14px; margin-top: 0; }
html body .grain-slider:disabled,
html body .as-slider:disabled { opacity: 0.45; }

/* The log-mapped sliders carry their tick marks in the element background, so
   the fill has to layer OVER them rather than replace them: ticks first with a
   transparent base, fill underneath. */
html body .grain-slider--log {
  background-image:
    linear-gradient(90deg,
      transparent 0%,
      transparent calc(8.3%  - 1px), var(--surface-6) 8.3%,  transparent calc(8.3%  + 1px),
      transparent calc(29.8% - 1px), var(--surface-6) 29.8%, transparent calc(29.8% + 1px),
      transparent calc(45.1% - 1px), var(--surface-6) 45.1%, transparent calc(45.1% + 1px),
      transparent calc(68.8% - 1px), var(--surface-6) 68.8%, transparent calc(68.8% + 1px),
      transparent calc(77.1% - 1px), var(--surface-6) 77.1%, transparent calc(77.1% + 1px),
      transparent calc(85.4% - 1px), var(--surface-6) 85.4%, transparent calc(85.4% + 1px),
      transparent 100%),
    linear-gradient(to right,
      color-mix(in srgb, var(--c, var(--accent-lock)) 75%, transparent) 0 calc(var(--fill, 0) * 1%),
      var(--surface-4) calc(var(--fill, 0) * 1%) 100%);
}

/* ═══ 6. THE ICON ROW ═════════════════════════════════════════════════════
   Bare glyphs are right; they were resting a shade too bright for something you
   are not meant to look at while playing, and hover was a grey wash that said
   nothing. Hover warms toward the ivory, and the destructive one warms toward
   brick — the colour says what KIND of thing is about to happen before you
   commit to it. */
html body .tc-icon { color: var(--text-subtle); }
html body .tc-icon:hover { color: var(--text-light); background: var(--surface-1); }
html body .tc-icon.tc-danger:hover { color: var(--accent-danger); background: var(--tint-danger-soft); }

/* A drawer handle is not a button. Lighting the open one with an ember FILL
   made it the loudest object on the stage — a slab in the corner pulling the
   eye away from the sphere permanently, for a piece of state the performer can
   already see (the drawer is open, right next to it). Hardware says this with a
   lit line: the glyph warms and a hairline comes up underneath. */
/* An open drawer turns its handle ember and stops there. The first cut added a
   hairline under it as well; two cues for one bit of state is noise, and the
   colour was already carrying it (Ek). */
html body .tc-drawer.on,
html body #tcLearn.on {
  color: var(--accent-action);
  background: none;
}

/* ═══ 7. THE RAILS ════════════════════════════════════════════════════════
   Card on card on card: a grey rail holding grey pills holding grey rows. Flat
   into the stage, hairline to separate, and the three pin buttons become one
   segmented object instead of three floating slabs. */
html body .tc-rail,
html body .tc-lrail,
html body .tc-prail {
  background: var(--bg-app);
  border-left: 1px solid var(--border-faint);
}
/* (The pinned rail's bar had a hairline under it here; the rail titles lost
   theirs to the large word on 2026-09-25 — the rails canvas, F.) */
/* Both rails' header buttons share ONE height, declared rather than inherited
   from whatever line box their contents happen to make. That is what lets the
   pinned rail's settings door (an icon) sit flush beside `all on` (text): an
   icon button sized by its own contents lands 9px short of its neighbour, and
   the difference is invisible to the eye and obvious to audit:align. */
/* 24, the kit height (ruled 2026-09-15) — it was a named 22 that named nothing
   but itself; the buttons it sizes are buttons. */
html body .lyr-bar { --lyr-bar-btn-h: 24px; }
/* The title takes the slack, so the bar's buttons group at the right edge on
   the bar's own gap. `justify-content: space-between` did that while there was
   one button and spread them 42px apart the moment there were two. */
html body .lyr-bar .lyr-title { margin-right: auto; }
html body .lyr-bar button {
  background: none;
  border: 1px solid var(--border-faint);
  border-radius: var(--r-2);
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-uppercase);
  color: var(--text-secondary);
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--lyr-bar-btn-h);
  padding: 0 0.5rem;
}
html body .lyr-bar button:hover { background: var(--surface-1); color: var(--text-bright); }
/* The rail's settings door. It sits beside `all on` and must read as its peer,
   so it is the SAME button — the only overrides are the ones an icon needs: no
   text padding, and a square whose height is pinned to the text button's
   measured box rather than left to fall out of an icon's line box. Both are
   read back by npm run audit:align; a door half a pixel off its neighbour is
   exactly the class of thing this project keeps shipping by eye. */
html body .lyr-bar .lyr-bar-set { padding: 0; width: var(--lyr-bar-btn-h); }
html body .lyr-bar .lyr-bar-set svg { display: block; width: 11px; height: 11px; }
/* The two rails' title bars carry their door at the TITLE's size (Ek,
   2026-09-25: "same with the hamburger settings button for both pinned rail
   and tool rail"): the glyph grows to the title's size inside the kit's 24px
   square, and the pinned rail's max field shares that height. */
html body .tc-rail > .lyr-bar .lyr-bar-set svg, html body .tc-lrail > .lyr-bar .lyr-bar-set svg { width: 18px; height: 18px; }
/* The empty state sits on the RAIL's ramp, not the panel's: it is a caption in
   a 246px column, and --fs-body put it a step above the actions above it. */
html body .lyr-empty { color: var(--text-muted); font-size: var(--fs-eyebrow); letter-spacing: var(--ls-uppercase); }

/* The rail's list scrolls only when there IS something to scroll. `overflow-y:
   auto` on a flex child whose min-height is auto still reserves a scrollbar
   gutter in this layout, which put a permanent track down the tool rail beside
   a list of three items. */
html body .tc-rail .lyr-list,
html body .tc-lrail .lyr-list {
  overflow-y: auto;
  scrollbar-gutter: auto;
  min-height: 0;
}
/* …AND THE PLATFORM WILL NOT DRAW IT. These two rules are kept because they
   cost nothing and are right wherever a classic scrollbar exists, but measured
   in this Electron on macOS they paint NOTHING: `offsetWidth - clientWidth` is
   0, so the scrollbar is an overlay and neither `::-webkit-scrollbar` nor
   `scrollbar-width: thin` + `scrollbar-color` brings it back (both tried
   through the dev bridge, both 0, both invisible in a cropped shot of the
   rail's right edge at rest). `js/tiles.js` `railScrollMark()` draws the mark
   the rail actually shows; the CSS for it is `.lyr-scroll`, below. */
html body .tc-rail .lyr-list::-webkit-scrollbar,
html body .tc-lrail .lyr-list::-webkit-scrollbar { width: 6px; }
html body .tc-rail .lyr-list::-webkit-scrollbar-thumb,
html body .tc-lrail .lyr-list::-webkit-scrollbar-thumb {
  background: var(--text-faint); background-clip: padding-box;
  border: 2px solid transparent; border-radius: var(--r-1);
}

/* THE RAIL'S OWN SCROLL MARK (2026-09-22). The reasoning is on
   `railScrollMark()`; this is its two pixels. `position: relative` on the rail
   is what the mark hangs from — checked first, against the running app: the
   rail had zero absolutely-positioned descendants, so nothing moved. */
html body .tc-lrail { position: relative; }
.lyr-scroll { position: absolute; right: var(--sp-1); width: 2px; pointer-events: none; z-index: 4; }
.lyr-scroll[hidden] { display: none; }
.lyr-scroll > i { position: absolute; left: 0; width: 2px; display: block;
  border-radius: var(--r-1); background: var(--text-faint); }

/* The phantom scrollbar in the tool rail (Ek, 2026-08-29).
   `body .tilebar` carries `height: 100%`, which is correct for what it used to
   be — a horizontal strip filling one grid row of the footer. In the vertical
   rail it makes the tile bar as tall as the WHOLE list on its own, so the list
   measured 918px of content in a 669px box while its four groups actually came
   to 408: a permanent scrollbar over 255px of empty space, next to a catalogue
   that visibly fits. The strip is gone (#291); the leftover is this.

   Scoped to the rail rather than deleted from the base rule, because the strip
   still exists in the cabinet and nothing here has been on screen to check it
   against. */
html body .tc-lrail .tilebar,
html body .tc-lrail .srcbar { height: auto; }

/* ═══ 8. THE PALETTE IS THE HAND ═════════════════════════════════════════════
   Everything else recedes. The palette is the one object actually touched
   mid-performance, so it is the one allowed to sit forward: a warm bed, a real
   shadow so it floats over the sphere instead of resting on it, and an armed
   tile lit by an ember WASH and a glow rather than a hard 1px box — a lit tool
   should look warm, not outlined. */
/* ── The palette's radii are CONCENTRIC, not a scale exception ────────────────
   Nested rounded rectangles need the outer radius larger than the inner by
   about the inset, or the corners pinch and the inner looks squeezed. The palette
   is 181×65 holding 53px tiles — a ~6px inset — so 4 + 6 = 10 is the arithmetic
   the old pair was reaching for. Measured: the inset is ~6px, so the correct
   outer is inner + 6. The original 7/10 was UNDER-concentric by 3; 4/12 (round
   twenty-two) was OVER by 2, and that is the "rounded pill holding square chips"
   the screenshot caught. 4 + 6 = 10 is the pair that is actually concentric, so
   the tile takes --r-3 and the palette takes --r-hand. Two `html body .palette` rules exist (6511, 6816) and the LATER one wins;
   both are set so the pair cannot drift apart. (round twenty-two) */
html body .palette {
  background: linear-gradient(180deg, rgba(30,26,22,0.94), rgba(18,15,13,0.94));
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-hand);
  box-shadow: 0 10px 34px rgba(0,0,0,0.62), 0 0 0 1px rgba(0,0,0,0.34);
  backdrop-filter: blur(9px);
}
/* `--pal-r` has to be threaded through HERE too, not only through the base
   rule: this selector is three classes deep behind `html body` and outranks
   it, so a flat `var(--r-3)` would silently win and every tile would draw the
   same shape however the verb was set. Specificity is not reading order
   (CLAUDE.md's second CSS trap), and this is the rule that would have eaten
   § 3 whole. */
html body .palette .tile {
  /* The SHAPE is the verb, so it has to read unlit: the bed and the tile were
     two dark browns a hairline apart and the outline vanished (Ek,
     2026-09-12: "when the tile is not selected i can barely see the shape"). */
  border: 1px solid var(--border-medium);
  border-radius: var(--pal-r, var(--r-3));
  color: var(--text-subtle);
}
/* The engine sheet's whole left column is parameter NAMES, and they were
   sitting at the bottom of the ramp — the one column you read every time you
   open a page. */
html body .prow-n { color: var(--text-secondary); }
html body .prow-s { color: var(--text-tertiary); }
html body .palette .tile { background: var(--surface-1); }          /* rest is a surface, not a wireframe (§ 11.3) */
html body .palette .tile:hover { background: var(--surface-3); color: var(--text-bright); }
/* ═══ 9. TOGGLES ══════════════════════════════════════════════════════════
   "mic ready" is a STATE, not an action: it reads cool and settled, and turns
   ember only when it is actually hot. The box stays — the shape language says a
   rectangle with a state dot is a toggle, and mic-live is exactly the state you
   want unmistakable — it was just carrying a filled slab's weight next to five
   bare glyphs. */
/* The two "is my rig up" pills. One rule, not two id rules: they are the same
   control asked about two devices, and a second copy is how the sensor pill
   turned up a rounded rectangle beside a capsule (Ek, 2026-08-30). */

/* ═══ 10. MOTION ══════════════════════════════════════════════════════════
   There was none — every transition ran the browser default `ease`, which
   starts slow, and on a control being played that reads as lag. Transform,
   opacity, colour and shadow only: nothing here can trigger layout, because the
   grain scheduler shares this thread. */
html body .tc-icon,
html body .tc-btn,
html body .lyr-bar button,
html body .bb-ax,
html body .bb-mute {
  transition: color var(--m-touch) var(--ease-out),
              background var(--m-touch) var(--ease-out),
              border-color var(--m-touch) var(--ease-out),
              opacity var(--m-touch) var(--ease-out);
}
/* Contact: a control you play acknowledges the press in the frame your finger
   arrives, then returns. That is the whole difference between a button and a
   key. */
html body .tc-icon:active,
html body .bb-ax:active,
html body .bb-mute:active { transform: translateY(0.5px) scale(0.94); transition-duration: 40ms; }
html body .palette .tile {
  transition: color var(--m-state) var(--ease-out),
              background var(--m-state) var(--ease-out),
              border-color var(--m-state) var(--ease-out),
              box-shadow var(--m-decay) var(--ease-out),
              transform 120ms var(--ease-snap);
}
html body .palette .tile:active { transform: scale(0.955); transition-duration: 40ms; }
@media (prefers-reduced-motion: reduce) {
  html body *, html body *::after, html body *::before {
    animation-duration: 1ms !important; transition-duration: 1ms !important;
  }
}

/* ═══ 15. THE FOOTER'S CONTROLS ARE ONE OBJECT (Ek, 2026-08-29) ═══════════
   AZ, EL, RO, DRY and MUTE are the same control: a 20px glyph over an
   engraved caption. Measured, they were three different objects —

     button   width   height   padding        gap     caption line-height
     DRY      40      51.6     7.5px 13.5px   8.25    8.4px
     MUTE     32      33       0              3       normal (10px)
     AZ       40      31.4     2px 0 0        1       8.4px

   — so nothing in the row shared a baseline, a cap height or a centre. DRY and
   AZ were already out of step with each other before MUTE was added, and the
   cause is structural rather than cosmetic: DRY is a direct child of `.bb-side`
   and picks up the group padding from `body .bottom-bar .bb-side > *`, MUTE is
   explicitly exempted from that rule, and AZ is nested a level deeper inside
   `#tcCursorSlot` so it never sees it. Three positions in the DOM, three boxes,
   one intended control.

   Stated once here, so the box no longer depends on where the button happens to
   sit. `line-height: 1` matters as much as the rest: MUTE's caption was in a
   10px line box where the others were in 8.4px, which alone put its text 1.6px
   off every other caption in the footer. */
html body .bottom-bar .bb-ax,
html body .bottom-bar .bb-mute {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  width: 2.7rem;
  height: auto;
  padding-top: 0;
  padding-bottom: 0;
  gap: var(--footer-cap-gap);
}
/* SIZE only — never `display`. Each of these buttons carries all of its states
   as sibling SVGs and shows one by class (`.ax-free` / `.ax-mute` / `.ax-map`,
   `.bb-mute-live` / `.bb-mute-off`). A `display: block` here outranks
   `body .bb-ax .ax-mute { display: none }` and every state renders at once —
   three stacked glyphs per axis. */
html body .bottom-bar .bb-ax > svg,
html body .bottom-bar .bb-mute > svg { width: 20px; height: 20px; flex: none; }
html body .bottom-bar .bb-ax i,
html body .bottom-bar .bb-mute i {
  display: block;
  line-height: 1;
  height: var(--fs-eyebrow);
  flex: none;
}

/* The sensor block is a caption and a value on one line, and they were sitting
   in boxes of different heights (10px against 11px) centred against each other
   — so "SENSOR" and "(not connected)" never shared a baseline. Text beside text
   aligns on the BASELINE, not on the box. */
html body .sensor-group {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
}
html body .sensor-group .bb-cap,
html body .sensor-group .sensor-group-status { line-height: 1; }

/* ═══ 16. THE TWO HAMBURGERS SIT THE SAME (Ek, 2026-08-29) ════════════════
   Both are 13.5px from their window edge, so the OUTER inset was already
   symmetric — the inner one was not, and that is the gap you actually read
   because it is the one with something on the far side of it. Measured at
   1440 wide:

     left    hamburger right edge 45.5  →  divider 58.3     = 12.8px
     right   divider 1385.3             →  hamburger 1394.5 =  9.2px

   The cause is that the rule draws the divider on the FOLLOWING group, so the
   space before it is `.tc-side`'s flex gap plus the group's margin, while the
   space after it is the border plus the group's padding — two different sums
   that only happened to be close. On the left the hamburger is before the
   divider and on the right it is after, so the two ends of the bar could never
   match while both were derived by accident.

   Solved for one number instead: 0.8rem of clear space on both sides of every
   divider in the bar, whichever side the group happens to be on. */
html body .tc-bar .tc-grp + .tc-grp {
  margin-left: 0.5rem;                 /* + .tc-side's 0.3rem gap = 0.8rem */
  padding-left: 0.8rem;                /* no border to account for any more  */
}
/* …except the two that still draw one, where the 1px is part of the 0.8rem. */
html body .tc-bar .tc-grp:has(#tcTools) + .tc-grp,
html body .tc-bar .tc-grp:has(#tcPinned) { padding-left: calc(0.8rem - 1px); }

/* ═══ 17. THE FOOTER HAS ONE BASELINE — MEASURED, NOT ASSERTED ════════════
   §4 above bottom-aligned the three SIDES and called that a baseline. It isn't:
   each group still sized its own internal stack, so the captions landed
   wherever their box happened to end. Measured from the footer's bottom edge:

     meter captions  IN GATE DRY PH OUT    9.5px
     axis captions   AZ EL RO             15.0px
     SENSOR                               16.4px
     DRY                                  16.9px
     MUTE                                 18.6px
     readouts        50%  −6.0 dB         21.0px
     DRY VOL / MASTER                     24.7px

   Fifteen pixels of spread across a 65px strip, which is exactly why it read
   as leftovers rather than as one instrument.

   Aligning BOXES is what aligns baselines here, and only because every caption
   is now the same box: same font-size (--fs-eyebrow), `line-height: 1`, and an
   explicit height. Two captions at different line-heights bottom-aligned to the
   same edge still sit on different baselines — that is what put MUTE 1.6px off
   DRY when it was first added. So the box is stated once, the groups all hang
   from one inset, and nothing is left to each group's own idea of its height. */
html body .bottom-bar {
  /* 16px. Two declarations of this property fought on the same selector —
     0.9rem here and a bare `1.15rem` orphan 83 lines down, which won, so the
     footer actually ran at 18.4px. That 18.4 is one of the six failures this
     file's own align-audit header exists for ("32.4px of air against 18.4").
     Neither could state a derivation, so both snap to the nearest step and the
     orphan is deleted — the inset is stated ONCE, here, where the comment above
     explains why there is only one. */
  --footer-inset: var(--sp-6);
  align-items: flex-end;
  padding-bottom: var(--footer-inset);
}
html body .bottom-bar .bb-side,
html body .bottom-bar .bottom-bar-levels {
  align-items: flex-end;
  padding-bottom: 0;
}

/* ONE caption box for the whole footer. */
html body .bottom-bar-label,
html body .bottom-bar .bb-cap,
html body .bottom-bar .bb-ax i,
html body .bottom-bar .bb-mute i,
html body #tcAudioSlot .grain-label {
  display: block;
  line-height: 1;
  height: var(--fs-eyebrow);
  margin: 0;
}
/* The sensor value was the last thing off the line, and every attempt to keep
   it a size larger cost alignment: box-aligning it left it 0.6px low, and
   `align-items: baseline` inside a bottom-aligned parent floated the whole pair
   1.5px instead, because the group's box bottom then follows the deepest
   descender rather than the footer's edge.

   So it joins the lettering. Two type sizes on one line can only be aligned by
   baseline, and baseline alignment is exactly what a bottom-anchored footer
   cannot give — one size is the way out, and it is the more consistent answer
   anyway: SENSOR and its state are one engraved line, told apart by weight and
   colour rather than by size. */
html body #sensorGroupStatus {
  display: block;
  line-height: 1;
  height: var(--fs-eyebrow);
  margin: 0;
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-uppercase);
  color: var(--text-tertiary);
}

/* Each group's last box IS its caption, so bottom-aligning the group puts the
   caption on the line. */
html body .bottom-bar-meters .levels-meter-col { justify-content: flex-end; }
html body .bottom-bar .sensor-group { align-items: flex-end; }

/* The level rows are a line of lettering with a fader set into it, so the
   fader centres on the captions' x-height rather than sitting on the baseline
   with them — a 2px rule resting on a text baseline reads as an underline. */
html body #tcAudioSlot .grain-row { align-items: flex-end; }
/* translate, not margin: the audio group scrolls on x, which forces its y
   overflow to `auto` as well, so 3px of margin here was 3px of clipped content
   rather than 3px of lift. A transform moves the fader off the baseline without
   asking the row to be any taller. */
html body #tcAudioSlot .grain-slider { transform: translateY(-3px); }
/* The readout joins the lettering, for the same reason the sensor value did:
   it was pinned to the caption's box height while keeping a larger font-size,
   so it overflowed its own box by ~1.4px — which the audio group, scrolling on
   x and therefore clipping on y, quietly cut off. One size on the line, exact
   alignment, nothing clipped. */
html body .bottom-bar .grain-numbox {
  font-size: var(--fs-eyebrow);
  line-height: 1;
  height: var(--fs-eyebrow);
}

/* The inset belongs to the footer, not to each group. `body .bottom-bar
   .bb-side > *:not(.bb-mute)` was adding 7.5px of vertical padding to some
   children and, by its own exemption, not to others — so the caption line was
   7.5px higher on the left and right than in the middle, and MUTE was off from
   DRY standing next to it. Every group hangs from the same edge now and the
   inset is stated once, above. */
html body .bottom-bar .bb-side > *,
html body .bottom-bar .bb-side > *:not(.bb-mute),
html body #tcAudioSlot .device,
html body #tcAudioSlot .device-body,
html body #tcAudioSlot .grain-row {
  padding-top: 0;
  padding-bottom: 0;
  margin-top: 0;
  margin-bottom: 0;
}

/* ═══ 18. FLAT (Ek, 2026-08-29) ═══════════════════════════════════════════
   No gradients as decoration, and no glows. The palette was a 180° gradient with
   a backdrop blur and the armed tile carried an inset ember bloom — three ways
   of saying "depth" on a design whose whole argument is that the sphere is the
   only thing with depth in it. A solid ground and a hairline do the same job
   and read as an object rather than as an effect.

   The gradients that stay are the ones carrying DATA, not surface: a fader's
   fill (a hard stop at the value, not a blend), the log sliders' tick marks,
   and the meter ramp — those are drawings, not lighting. */
html body .palette {
  background: #17140f;
  border: 1px solid var(--border-subtle);
  border-radius: var(--r-hand);
  box-shadow: 0 6px 20px rgba(0,0,0,0.55);
  backdrop-filter: none;
}

/* The two drawer handles were the SAME glyph — three bars each — so neither
   said which side it opened, and the only way to tell them apart was where
   they sat. They are panel toggles now: a pane with its divider on the side
   the rail comes from. */
html body #tcTools svg,
html body #tcPinned svg { width: 20px; height: 20px; }

/* MUTE sat 1.6px above DRY, and 1.6 is exactly half the 3.25px difference in
   their heights — the signature of a CENTRE alignment, not a bottom one. Two
   boxes of different heights centred on a line can never share a baseline, so
   the fix is to stop them being different heights rather than to keep hunting
   the alignment that put them there: one explicit box, built from the parts
   (20px glyph + 5px gap + caption), so the two are the same object by
   construction. */
html body .bottom-bar .bb-side { align-items: flex-end; }
html body .bottom-bar .bb-ax,
html body .bottom-bar .bb-mute {
  height: calc(20px + 5px + var(--fs-eyebrow));
  align-self: flex-end;
}

/* SENSOR and its value are two type sizes on one line, so they align on the
   BASELINE — bottom-aligning their boxes left the value 0.6px low, which is
   the difference in their descents. */
html body .bottom-bar .sensor-group { align-items: flex-end; }

/* The glyphs, not just the captions (Ek). With the captions finally on one
   line, DRY's icon still sat 3.2px above MUTE's and the three axes — because
   the stack packs from the BOTTOM, so the gap between glyph and caption sets
   where the glyph lands, and DRY's gap was 8.25px against everyone else's 5.

   It is a specificity loss, and a narrow one: `body .bottom-bar .bb-side >
   *:not(.bb-mute)` scores three classes, one more than the rule above, and
   DRY is the only one of the five buttons it can reach — MUTE is written out
   of it by the `:not()`, and AZ/EL/RO sit a level deeper inside #tcCursorSlot
   where a child combinator never sees them. One button out of five getting a
   different gap from a rule that names none of them is exactly how a row ends
   up looking hand-placed. */
html body .bottom-bar .bb-side > .bb-ax,
html body .bottom-bar .bb-side > .bb-mute,
html body .bottom-bar .bb-ax,
html body .bottom-bar .bb-mute { gap: var(--footer-cap-gap); }

/* The audio group used to take 5px of top padding as headroom: it scrolled on
   x, which forces `auto` on y as well, and that clipped the fader's 3px lift
   off the baseline. Section 23 stacked the two level rows, which halved the
   group's width so it no longer scrolls, and set `overflow: visible` — so the
   headroom stopped being headroom and became 5px of the group sitting above
   the footer's row. Removed rather than kept "just in case": it was load-
   bearing for a layout that no longer exists, and it put the one nested group
   off the grid every other group is on. */
html body .bottom-bar #tcAudioGroup { padding-top: 0; }

/* ═══ 20. THE SETTINGS SHELL, AND ONE TYPE CONTRACT FOR ITS PAGES ═════════
   #269 made every page look the same by stripping their FRAMES — no cards, no
   nested outlines, a rule only where two sections meet. That worked, and it is
   why the pages sit still. What it never touched was TYPE, and type is what
   was left saying "a bunch of things thrown in together".

   Fingerprinted, all ten pages at once, the sizes in use were:

     audio · pins · viz · view · session   11.0 / 11.5 / 12.5 / 14.1 / 15.7  (tokens)
     sensors                                8 / 9 / 10                       (raw px)
     mapping                                9 / 10                           (raw px)
     osc                                    11.2 / 11.5 / 12.5               (mixed)
     feedback · keys                        14.1 / 15.7 only                 (no hierarchy)

   Three unrelated families. The raw-px pages are the loud ones: because they
   were absolute, they did not move when the scale moved, so raising the type
   everywhere else left them stranded at 8–10px — visibly smaller than the app
   around them, which reads as neglect rather than as hierarchy. Those eleven
   declarations are tokens now (above). This block does the shell. */

/* The nav was 0.76rem — smaller than the body text of the page it indexes,
   which is backwards: it is the thing you read first and aim at. */
html body .set-nav-item {
  font-size: var(--fs-control);
  letter-spacing: var(--ls-normal);
  color: var(--text-secondary);
  padding: 0.5rem 0.7rem;
  border-radius: var(--r-2);
}
html body .set-nav-item:hover { color: var(--text-light); background: var(--surface-2); }
/* The active page is named by an ember rule down its edge, not by a grey slab:
   the same "a lit line says which one" the drawer handles use. */
html body .set-nav-item.active {
  color: var(--text-light);
  background: var(--surface-2);
  box-shadow: inset 2px 0 0 var(--accent-action);
}
html body .set-nav-grp {
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-widest);
  color: var(--text-dim);
  padding: 1.1rem 0.7rem 0.35rem;
}
html body .settings-nav { background: var(--bg-deep); }

html body .settings-head h2 {
  font-size: var(--fs-xl);
  font-weight: 500;
  letter-spacing: var(--ls-wide);
  color: var(--text-light);
}

/* ── One contract for every hosted page ────────────────────────────────────
   A settings page is three kinds of text and nothing else:
     SECTION   the eyebrow that starts a group
     LABEL     the name of a control
     VALUE     what it currently is
   Stated once, so a page borrowed from a modal cannot bring its own idea of
   what a label looks like. */

html body .settings-host .in-settings .grain-label,
html body .settings-host .in-settings label {
  font-size: var(--fs-page);
  letter-spacing: var(--ls-normal);
  text-transform: none;
  color: var(--text-secondary);
  font-weight: 400;
}
html body .settings-host .in-settings .as-val,
html body .settings-host .in-settings .grain-numbox,
html body .settings-host .in-settings .set-count {
  font-size: var(--fs-page);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
/* A hint is a quiet line under its control, at ONE size across every page —
   and QUIETER than the label it explains, which means a step down, not a step
   up. It was --fs-body: bigger than the reference pages' body text, so a note
   outranked the control it belonged to. *//* Rows share a rhythm rather than each page inventing one. */

html body .settings-host .in-settings .grain-row {
  min-height: 1.9rem;
  gap: 0.8rem;
}

/* The shell's close button was the one boxed control in a window full of bare
   glyphs — and it is the least interesting thing on the page. Same treatment as
   the chrome's history icons: a glyph that warms on hover, no box. */
html body .settings-dialog .close-btn {
  background: none;
  border: none;
  color: var(--text-dimmer);
  font-size: var(--fs-md);
  width: 32px; height: 32px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--r-3);
  transition: color var(--m-touch) var(--ease-out), background var(--m-touch) var(--ease-out);
}
html body .settings-dialog .close-btn:hover { color: var(--text-light); background: var(--surface-1); }

/* A settings page that is mostly empty should not stretch its content to fill
   a fixed-height dialog — the height is there so the NAV does not jump between
   pages, not so a three-row page floats in the middle of nothing. */
html body .settings-host .in-settings { align-content: start; }

/* ═══ 21. THE RAIL SAYS HOW IT SELECTS (Ek, 2026-08-29/30) ════════════════
   Four groups, and they do not all behave the same way:

     source   radio — one of them
     lens     radio — wide · spot · cap. Cap is "no lens", which is the third
                      state of one thing, not a switch beside two things.
     filters  MULTI — pinned clouds, pinned loops. Select-a-range, Photoshop's
                      colour-range sense: narrow WHICH material is in play, and
                      stack as many as you like.
     tools    radio — one brush in the hand.

   Every row used to render identically, so the rail said nothing about which
   rule it obeyed and you found out by clicking.

   The first attempt at saying it drew a LINE down the left edge of a seated
   row, and it was wrong twice over (Ek, 2026-08-30). `.trow.on` already
   colours the 2px left border, so a radio row got the border AND the inset
   line — two marks, 4px, subtly doubled. And the source rows never carried
   `.trow--radio` at all, so `in 1` and `sampler` drew only the border: one
   line in one group, two in the next, for choices that obey the same rule.

   So there is now exactly ONE mark, and its SHAPE carries the rule:

     ROUND  ── a half moon, flush at the edge.   One of these.
     SQUARE ── a half square, flush at the edge. As many as you like.

   Round is the shape that cannot tile — you never see two half moons stacked
   and read them as a run. Square is the shape that can: an off filter leaves
   its slot outlined, so a stack of them reads as a column of switches with
   some thrown, which is exactly what it is. Both sit in the same 6px gutter
   at the same height, so the two groups are legible as variations of one
   thing rather than as two unrelated widgets.

   Flush to the edge, in the row's own hue, replacing the line and occupying
   its space — nothing reflows, and the label column stays where it was.

   FLUSH WITH THE RAIL, not with the row (2026-09-22). See the bleed below. */

/* The marks are absolutely placed against the row. */
html body .tc-lrail .trow { position: relative; }

/* THE MARK HANGS OUT; THE ROW DOES NOT. Flush still means flush with the RAIL
   (Ek, 2026-09-22: "there's a space between the half moon and the left side of
   the app"), but the ROW bled left to get it there, which put every tool row
   12px out of step with its own card — starting 12 left of it and stopping 12
   short of its right edge — and 12px out of step with MODE's rows, which did
   not bleed. Measured: trow 1→218, mrow 13→230, card 13→230.

   So the row fills its card and the MARK alone reaches the gutter: `--sp-5` for
   the list's inset plus the row's own 2px border, which is where `left: -2px`
   was already pointing. One box for both row kinds, and the mark still sits on
   the rail's edge. */
html body .tc-lrail .lyr-list .trow--radio.on::before,
html body .tc-lrail .lyr-list .trow--multi::before {
  left: calc(-2px - var(--sp-5));
}

/* One mark, not two: the left border stops colouring itself for any row that
   draws a mark. */
html body .trow--radio.on,
html body .trow--multi.on { border-left-color: transparent; }

/* SEATED — a radio choice sits DOWN into the rail when it is the chosen one.
   The ground is what makes a group read as one control; the half moon is what
   says which member of it you are on. */
html body .trow--radio.on {
  background: var(--surface-2);
  box-shadow: inset 0 1px 2px rgba(0,0,0,0.45);
  color: var(--text-light);
}
html body .trow--radio.on::before {
  content: '';
  /* -2px, because the mark REPLACES the row's 2px left border and occupies its
     space — that is what the rule above this block has always said, and `left:
     0` is the padding edge, which is inside the border. */
  position: absolute; left: -2px; top: 50%;
  width: 6px; height: 12px; margin-top: -6px;
  border-radius: 0 6px 6px 0;          /* flat at the edge, round into the row */
  background: var(--c, var(--accent-action));
  pointer-events: none;
}

/* SWITCHED — an additive row keeps its surface and shows a half SQUARE at the
   edge. Nothing sinks, so several lit at once looks deliberate rather than
   broken, which is the whole difference the old rail could not draw. */
html body .trow--multi::before {
  content: '';
  position: absolute; left: -2px; top: 50%;
  width: 6px; height: 12px; margin-top: -6px;
  border-radius: 0;                     /* the half of a 12px square */
  /* An OFF filter is a dim square, not an outlined one. The first version drew
     three borders around an empty 6px box and — with the fourth side being the
     rail's edge — it read as a bracket, a punctuation mark, not a shape (Ek,
     2026-08-30). Off and on are now the same solid block at two weights, so the
     column reads as switches with some thrown, and the difference you scan for
     is brightness rather than a change of kind. */
  background: color-mix(in srgb, var(--c, var(--accent-action)) 24%, transparent);
  pointer-events: none;
  transition: background var(--m-touch) var(--ease-out);
}
html body .trow--multi.on::before {
  background: var(--c, var(--accent-action));
}
/* An additive row that is ON keeps its surface — it must not read as seated. */
html body .trow--multi.on {
  background: none;
  box-shadow: none;
  color: var(--text-light);
}
html body .trow--multi.on:hover { background: var(--surface-1); }

/* ═══ 22. OFF FACTORY (Ek, 2026-08-29) ════════════════════════════════════
   A tool whose params have been moved this session carries a dot. It is the
   one thing the rail could not tell you: two tiles with the same name and the
   same glyph, one of which no longer sounds like the preset it is named after.

   In the tool's own engine hue, because that is what changed — the sound. Top
   right, away from the glyph and the name so it never crowds either, and small
   enough to be a footnote rather than a warning: going off factory is a normal
   thing to do, not a problem to fix. */
html body .trow.off-factory,
html body .tile.off-factory { position: relative; }
html body .trow.off-factory::after,
html body .tile.off-factory::after {
  content: '';
  position: absolute;
  top: 6px; right: 6px;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--c, var(--accent-action));
  opacity: 0.85;
  pointer-events: none;
}
/* On the palette the tile is a big target with the glyph centred, so the dot sits
   in the corner of the tile rather than the corner of a row. */
/* The off-factory dot takes the BOTTOM-LEFT corner on the palette (2026-09-12,
   night): the property marks own the top-right, the one corner a toggle's
   shape (24px 3px 24px 3px) leaves square besides it. */
html body .palette .tile.off-factory::after { top: auto; bottom: 5px; left: 5px; right: auto; }

/* ═══ 23. THE FOOTER, ON ONE GRID (Ek, 2026-08-30) ════════════════════════
   Three faults, all measured before they were touched.

   1. THE GROUPS OVERLAPPED. `.bottom-bar-levels` ran x 490 → 759 and the right
      group started at x 654 — a hundred and five pixels of collision, which on
      screen was the words PH and OUT printed underneath DRY VOL. The audio
      group was 478px wide because its two faders sat side by side, and the bar
      had no width left to give. Stacking them is both what Ek asked for and
      the fix: the group halves, and the row fits.

   2. DRY VOL AND MASTER SAT SIDE BY SIDE. They are the same kind of thing —
      an output level with a number — and side by side they read as two halves
      of one wide control. Stacked, master lands on the footer's caption line
      with everything else and dry sits directly over it: two rows of one
      column, which is what a pair of faders on a desk looks like.

   3. THE SPACE ABOVE THE METERS WAS NOT THE SPACE BELOW THE CAPTIONS. Above:
      10.6px. Below: 18.4px. The bar had `padding-top: 0.1rem` against a
      1.15rem bottom inset, from when the meters were shorter — so the whole
      rail sat high in its own bar and the footer read as top-heavy. One inset,
      stated once, used on both edges: the tallest thing in the footer now has
      the same air above it as the caption line has below it, and the bar takes
      whatever height that costs. */
html body .bottom-bar {
  padding-top: var(--footer-inset);
  padding-bottom: var(--footer-inset);
}
/* The levels block carried 8px of its own top space, which is a second opinion
   about the footer's top edge. The bar's inset is the only one. */
html body .bottom-bar .bottom-bar-levels { padding-top: 0; }

/* The audio group is a COLUMN of level rows: dry over master, right-aligned so
   the two faders and the two readouts line up as columns rather than as two
   rows that happen to start in the same place. */
html body #tcAudioSlot .audio-body {
  flex-direction: column;
  align-items: flex-end;
  gap: 0.34rem;
  overflow: visible;          /* it no longer needs to scroll on x */
}
html body .bottom-bar-audio { overflow: visible; }
/* Both rows are one width, so `dry vol` sits exactly over `master`, the two
   faders share a left edge and the two readouts share a right one. */
html body #tcAudioSlot .audio-body > .grain-row { width: 13.8rem; }

/* Nothing may claim the space the others need. The levels rail is the fixed
   thing in the middle; the two side groups take what is left and stop. */
html body .bottom-bar .bottom-bar-levels { flex: 0 0 auto; }
html body .bottom-bar .bb-side { flex: 0 1 auto; min-width: 0; }

/* ═══ 24. RETIRED (2026-08-30, round ten) ════════════════════════════════
   This was the settings type contract: `--fs-page` / `--fs-hint` on
   `.settings-host`, then ~120 lines of `.settings-host .in-settings X` sweeps
   pulling every hosted page down to an 11.04px body.

   It existed because the pages were UNCONVERTED — each one carried its own
   markup from the rig view, so the only way to give the shell one voice was to
   name every class that could put text on a page and sweep it. Every page is
   the row model now (docs/SETTINGS-GUI.md), the kit states its own sizes off
   its own tokens, and this block had no users left except the five specificity
   restatements written to beat it. Deleting it took those with it.

   It is also the reason the kit kept losing to its own page contract: the
   sweep was (0,2,3) and a kit rule one level shallower is (0,2,2), so a plain
   `.settings-host .set-x` was outranked by `.settings-host .in-settings span`
   on every span, div and p. That cost two rounds of measurement to find twice.
   `git log --diff-filter=D` has the whole block if it is ever wanted back. */

/* ═══ 25. THE SETTINGS BOX HAS THE APP'S CORNER (Ek, 2026-08-30) ══════════
   `.mu-dialog` was `border-radius: 0` — square, against a stage whose own
   corner is --r-card. The canvas wrapper is the only thing in the app carrying
   that radius, and the settings window is the only other surface of that kind:
   a big lit panel floating over the instrument. Same corner, so the two hero
   surfaces are the same object at two moments rather than two design systems.
   The nav's own right edge stays square — it is an internal seam, and rounding
   a seam is what makes a panel look like two panels. */
html body .mu-dialog { border-radius: var(--r-card); }
html body .mu-dialog.settings-dialog { border-radius: var(--r-card); overflow: hidden; }

/* ═══ 26. ONE ROW HEIGHT, SO THE AIR IS THE SAME EVERYWHERE ═══════════════
   (Ek, 2026-08-30: "there should be equal space between the top and bottom in
   the footer, there's way more space on top of the items like the mute icon
   dry etc.")

   Section 23 balanced the footer's insets and measured 18.4 above and 18.4
   below — and that measurement was true of the TALLEST thing in the bar and of
   nothing else. Every group hangs from the caption line, so a group shorter
   than the meters gets the difference added to its own top:

     meter column   34 + 4 + 11 = 49   →  18.4 above, 18.4 below   ✓
     az / el / ro   20 + 5 + 11 = 36   →  32.4 above, 18.4 below   ✗
     dry / mute     20 + 5 + 11 = 36   →  32.4 above, 18.4 below   ✗

   Balancing the BAR cannot fix that, because the imbalance is between the
   groups. So the groups are one height, stated once — and then the bar's own
   inset is the only air in the footer, above and below, for everything in it.

   The number is met from both sides rather than by shrinking the meters to the
   icons: the glyphs go 20 → 24 (which the footer wanted anyway — they are the
   controls you hit without looking) and the meters take what is left. */
html body .bottom-bar {
  --footer-row: 40px;
  --footer-glyph: 24px;
  --footer-cap-gap: 5px;
}
html body .bottom-bar .bb-ax,
html body .bottom-bar .bb-mute {
  height: var(--footer-row);
  gap: var(--footer-cap-gap);
}
html body .bottom-bar .bb-ax > svg,
html body .bottom-bar .bb-mute > svg {
  width: var(--footer-glyph);
  height: var(--footer-glyph);
}
/* The meter column is the same box: caption, gap, and the meter takes the
   rest — so its height is DERIVED from the row rather than set beside it and
   left to drift the next time either number moves. `--footer-meter` is read by
   the `!important` rule that owns this height (search --footer-meter): the
   number lives here, the precedence lives there. */
html body .bottom-bar-meters .levels-meter-col { height: var(--footer-row); }
html body .bottom-bar {
  --footer-meter: calc(var(--footer-row) - var(--fs-eyebrow) - 4px);
}
/* The audio group is two stacked rows, and it is the one group that was not on
   the footer's grid: 27.5px tall in a 40px row, bottom-anchored, so it started
   12.5px below every other group's top edge and the two rows sat bunched
   5.4px apart at the bottom of their own space (measured 2026-08-30 — Ek: "the
   dry vol and master can be spaced out more, and also vertically aligned").

   It takes the row height like everything else and distributes into it. Then
   `dry vol` lands on the row's TOP edge — the same line the glyphs and the
   meters start on — and `master` lands on the caption line at the bottom, so
   the pair brackets the footer's two horizontals instead of floating between
   them. The gap between the rows is what is left over (40 − 11.04 − 11.04 =
   17.9px), which is the right way round: the grid decides the spacing, rather
   than a spacing being chosen and the grid asked to accommodate it. Move the
   row height or the caption size and this stays true with no second edit. */
html body .bottom-bar-audio { align-self: flex-end; }
html body #tcAudioSlot .audio-body {
  height: var(--footer-row);
  justify-content: space-between;
  gap: 0;
}

/* With the sensor block gone the left group is the three cursor axes alone. */
html body .bottom-bar .sensor-group[hidden] { display: none !important; }

/* Two instruments are two things. Without a rule the second block's first row
   butts straight onto the first block's last one and the pair reads as one
   long list of contradictory numbers. */
.syg-instrument + .syg-instrument { margin-top: 26px; }

/* (.syg-check is gone — Ek chose three stacked kit toggles for the
   calibration flags, 2026-09-01; .imu-cal-stack in settings-gui.css.) */

/* The two layers a sensor card is made of: what mubone does with any sensor,
   and what this particular instrument can be told. Ek read them as one page
   and asked which settings belonged to which box. */
.set-layer-head { margin-top: 22px; }
.imu-setup-card > .set-layer-head:first-of-type { margin-top: 4px; }

/* The diagnostics page's one non-kit element: a measurement result. It is
   fixed-width because the numbers are meant to be compared down the column,
   and it collapses when empty so a tool that has not been run leaves no gap. */
.diag-out {
  margin: 8px 0 0;
  font: var(--fs-set-sub)/1.5 var(--font-mono);
  color: var(--text-muted);
  white-space: pre-wrap;
  overflow-x: auto;
}
.diag-out:empty { display: none; }

/* ═══ 23. THE PALETTE (Ek, 2026-09-03; the list, 2026-09-11) ══════════════
   One strip, one row of identical boxes, exactly as wide as what is on it:
   the badge, then every tile in order — tools, lenses and the pin pair
   mixed as the performer placed them. No beds and no hairlines: nothing is
   grouped by kind any more, because a position is agnostic to what sits in
   it. A tool wears the armed box and lights while it sounds; a lens lights
   its glyph while it is on (no lens on is the cap); a pin tile has only the
   180 ms flash. Dragging: the insertion caret is a 2px bar on the left edge
   of the tile the drop lands before, or on the strip's right edge for the
   end — shape, not dimming. */
/* The bed holds the tile row AND the legend line under it (2026-09-11): 5px
   inset, the tiles, --pal-leg-gap, the line, 5px. The line's height is the
   legend's biggest type (10.5px at 1) plus the descender the drawn spacebar
   hangs into — nothing in it is taller. */
/* THE STRIP'S MEASURES (Ek, 2026-09-12, night: "make the whole palette tile a
   bit more spacious … it's generally too crowded"): the tile, the gap between
   tiles, the bed's inset, the legend row and its gap above — one set of
   tokens, read by every rule below and by tiles.js for the hand tile's
   width. The bed's radius is inset + the tile's smallest radius (3) — the
   concentric rule of INSTRUMENT-GUI § 4 — so 8 + 3 ≈ --r-card. */
/* Air in the strip (Ek, 2026-09-13: "spread out the tiles a bit in the palette
   bar to give it some breathing room, bigger margins as well"). Gap 7 → 11,
   pad 8 → 13; the tile itself is untouched at 53, so every sticker, ring and
   hairline measured against it still holds. The dock is 83px tall now, from
   71 — 13 pad + 1 border + 53 tile + 13 pad + 1 border. */
/* The palette's metrics live on the BODY, not on `.palette`, because the TOOL
   EDITOR has to read them too (2026-09-22). The bench holds the hand tile, and
   the hand tile is three tiles and two gaps — so if the strip's tile shrinks on
   a narrow screen and the bench's does not, the two are different objects. They
   are the same object; one set of numbers says so.

   `--lrail-w` was that tile plus the editor's own gutters until 2026-09-22 —
   243px, derived from a BENCH TILE that no longer exists. The bench went and
   nothing was left sizing this rail, so it kept a width measured for an object
   it no longer holds, and came up one line narrow beside the pin rail (Ek:
   "make the tools rail as wide as the pin rail again"). ONE token states both
   now — and it is the PINS rail's 20rem, the width Ek set on 2026-09-16
   ("wider — ~320px"), NOT the drawer's 15.5rem. The tools rail and the pins
   rail are the two you WORK in, one on each edge; the drawer is a sheet that
   opens over the stage and keeps its own. Matching the drawer first is how
   this came out 248 against the pins rail's 320. */
html body { --rail-w: 20rem; --pal-tile: 53px; --pal-gap: 11px; --pal-pad: 13px;
  --hand-tile-w: calc(3 * var(--pal-tile) + 2 * var(--pal-gap));
  --lrail-w: var(--rail-w); }
/* The narrow screen's tile, stated HERE rather than up in the layout media
   block: `html body` in both places is the same specificity, so the later
   declaration wins — and the layout block is earlier in this file. Keeping the
   value and its breakpoint together is what makes that impossible to get wrong
   twice (it was wrong once, 2026-09-22: the strip shrank and the bench did not). */
@media (max-width: 820px) { html body { --pal-tile: 44px; } }
/* The STATIONS (2026-10-04): three windows on one screen are 480–570 wide, and
   the strip at 44 is 567. The spacebar gives first: two tiles wide, the glyph
   alone and centred. The NAMES go, not the glyph — the binding sticker sits on
   the tile's lower-left corner, and at two tiles wide it covered whichever of
   the three was on the left (measured, both ways). Centred, the glyph meets the
   sticker exactly as it does at three wide. The voice is still the rail tab's.
   Under 470 the strip's air tightens to the next step down, holding it to 410. */
@media (max-width: 580px) {
  html body { --hand-tile-w: calc(2 * var(--pal-tile) + var(--pal-gap)); }
  html body .palette .tile.tile--hand .tile-nm { display: none; }   /* .tile: the name's own rule is later in the file */
}
@media (max-width: 470px) {
  html body { --pal-gap: var(--sp-3); --pal-pad: var(--sp-3); }
}
html body .palette {
  align-items: center; flex-wrap: nowrap; gap: var(--pal-gap); padding: var(--pal-pad); border-radius: var(--r-card); }
/* 71px: 8 pad + 1 border + 53 tile + 8 pad + 1 border (§ 11.4). The ledger's
   reserved lines under the tiles are gone; the binding is a sticker on the
   tile's corner and takes no room of its own. */
/* A LENS tile is a state: ON is a filled box in the lens hue with the glyph
   in the hue; OFF is a grey glyph and name with no box at all — the whole
   tile reads as off, not a lighter version of on (Ek, 2026-09-11: "it needs
   to be way clearer that it's off … the whole tiles should be greyed out").
   Shape and hue, both changing, so neither is the only signal. */
/* A lens tile OFF keeps its hue, as its rail row does (Ek, 2026-09-12: "go back to
   the same non-greyed out design when it's off"); ON is the filled box below. */
html body .palette .tile--lens.on { box-shadow: inset 0 0 0 2px var(--c);
  background: color-mix(in srgb, var(--c) 16%, var(--bg-app)); color: var(--text-bright); }
/* The pin pair as tiles (2026-09-10). Nothing arms them and nothing
   sustains, so the only states are rest, hover and the flash; the glyph is
   the chrome's grey, not an engine hue, because a pin holds any engine's
   material. */
html body .palette .tile--act { cursor: pointer; }
html body .palette .tile--act.fired svg { color: var(--accent-action); }   /* the pin tiles wear --eng-pins, bone */
html body .palette .tile.over { box-shadow: inset 2px 0 0 var(--text-tertiary); }
html body .palette.zone-over { box-shadow: inset -2px 0 0 var(--text-tertiary); }
html body .palette .tile--lens { cursor: pointer; }
/* A keys-page row whose verb the tile at that position has not (or an
   empty position): listed, so all nine positions always show, with blank
   cells — blank means impossible, a dash means unbound. Never dimmed. */
html body .set-table-row--na .set-row-title { color: var(--text-tertiary); }
html body .palette .tile--lens.on svg { color: var(--eng-lens); }
/* The drawer's handle — ⋯ at the right edge of EVERY tool and lens row
   (2026-09-10; it was the armed row's alone, and a handle you have to pick
   the row up to see is one you cannot find from behind an instrument).
   Always visible, never on hover. tiles.js decides which rows draw it. */
/* 24, the kit's height (ruled 2026-09-15). */
html body .trow-more { display: flex; flex: none; margin-left: auto; width: 1.4rem; height: 24px;
  align-items: center; justify-content: center; border-radius: var(--r-2, 3px);
  color: var(--text-tertiary); cursor: pointer; }
html body .trow-more svg { width: 1rem; height: 1rem; }
/* The palette badge that headed the tile row is gone (Ek, 2026-09-12,
   evening); the hand TILE heads the row — § 24. */
/* The last of its kind on the palette cannot be taken off: the mark keeps
   its filled shape (it IS on the palette) but offers nothing — no hover, no
   pointer — the way an unavailable control does here (§ .tc-dis). */
html body .trow-more:hover { color: var(--text-bright); background: var(--surface-3); }
/* The marks are 1.4rem targets in a 17px line, and they were the row's tallest
   children: the armed row measured 33.3px against 27.9 for the rest, so every
   pick moved the list under the cursor (Ek, 2026-09-03). Negative vertical
   margins keep the target and take the mark out of the row's height —
   align-audit "every rail row is one height". */
html body .trow-more { margin-top: -0.3rem; margin-bottom: -0.3rem; }
/* The WET mark (Ek, 2026-09-03) — a drop after the name of a brush whose
   knobs keep moving every stroke it painted. Engine hue, like the glyph: it
   says "this brush", not "warning". On the row it sits right after the name;
   on the palette tile, where the name is hidden, it sits in the top-left corner
   opposite the off-factory dot. Shape, never dimming (§ engine hue). */
html body .trow .tile-wet { display: flex; flex: none; width: 0.9rem; height: 0.9rem;
  margin-left: 0.2rem; color: var(--c); }
html body .trow .tile-wet svg { width: 0.9rem; height: 0.9rem; }
/* On the rail the drop is a BUTTON on every grain brush's row (Ek,
   2026-09-06): outlined is dry, filled is wet — the cycle mark's shape rule,
   the cycle mark's target size and hover, and the same negative margins so
   the row stays one height (align-audit). */
html body .trow .tile-wet--btn { width: 1.4rem; height: 1.4rem; align-items: center; justify-content: center;
  margin-top: -0.3rem; margin-bottom: -0.3rem; border-radius: var(--r-2, 3px); cursor: pointer; }
html body .trow .tile-wet--btn:hover { background: var(--surface-3); }
html body .palette .tile .tile-wet { position: absolute; top: 4px; left: 5px; display: flex;
  width: 0.75rem; height: 0.75rem; color: var(--c); }
html body .palette .tile .tile-wet svg { width: 0.75rem; height: 0.75rem; }
html body .palette .tile.wet { position: relative; }
/* THE PIN MARK (Ek, 2026-09-12, night) — the tile pins what it plays on
   release, read off its own on-end flag. Wet's twin in every rule: engine
   hue, a button on the row (outlined off, filled on), a mark on the tile.
   The two marks share one corner box (`.tile-marks`, top-left) so a tile
   that is both wears both side by side. */
html body .trow .tile-pin { display: flex; flex: none; width: 0.9rem; height: 0.9rem; margin-left: 0.2rem; color: var(--c); }
html body .trow .tile-pin svg { width: 0.9rem; height: 0.9rem; }
html body .trow .tile-pin--btn { width: 1.4rem; height: 1.4rem; align-items: center; justify-content: center;
  margin-top: -0.3rem; margin-bottom: -0.3rem; border-radius: var(--r-2, 3px); cursor: pointer; }
html body .trow .tile-pin--btn:hover { background: var(--surface-3); }
/* Dub's pin is a fact, not a switch: the same box, no hand, no hover. */
html body .trow .tile-pin--fixed { cursor: default; }
html body .trow .tile-pin--fixed:hover { background: none; }
/* TOP-RIGHT (Ek, 2026-09-12, night: "put the property flag … on the top right
   of the tile mainly to work with that corner which doesn't cut off the
   margins"): a toggle tile's top-left corner is the 24px one and ate the
   mark's inset; its top-right is 3px. */
/* A STICKER on the corner (Ek, night: "kinda on top of the tile like if it
   was going over … not enough space in the tile with the design of the logo,
   it messes up the margin, it's more of a sticker on top of the top right").
   The marks sit half outside the outline, each on its own round disc of the
   app ground so the outline reads as passing UNDER it; the glyph itself is
   unchanged. The bed's 8px inset and 7px gap hold the overhang. */
/* Two corners, one each (Ek, night: "always have the pin sticker on the top
   right of the tile and the wet on the top left"): the wrapper lays nothing
   out; each sticker is placed on its own corner. */
html body .palette .tile .tile-marks { display: contents; }
html body .palette .tile .tile-marks .tile-wet, html body .palette .tile .tile-marks .tile-pin { position: absolute; top: -5px; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  width: 1.15rem; height: 1.15rem; border-radius: 999px; background: var(--bg-app); color: var(--c);
  border: 1px solid var(--border-medium); box-sizing: border-box; }   /* the sticker's edge — the tile's own hairline */
html body .palette .tile .tile-marks .tile-wet { left: -5px; right: auto; }
html body .palette .tile .tile-marks .tile-pin { right: -5px; left: auto; }
html body .palette .tile .tile-marks .tile-wet svg, html body .palette .tile .tile-marks .tile-pin svg { width: 0.75rem; height: 0.75rem; }
/* THE FLAGS' ROW (2026-09-22): the top-right corner is a ROW the stickers
   sit in, static, keeping only their shape — the pin, and audition on the
   cursor tile. (Overdub's flag shared it until 2026-09-26, when overdub
   stopped being a mode.) */
html body .palette .tile .tile-marks .tile-flags { position: absolute; top: -5px; right: -5px; z-index: 2;
  display: flex; align-items: center; gap: var(--sp-0); }
html body .palette .tile .tile-marks .tile-flags .tile-pin,
html body .palette .tile .tile-marks .tile-flags .tile-aud { position: static; right: auto; left: auto; top: auto; }
/* AUDITION on the cursor tile (2026-09-24): the same sticker disc as the pin,
   the headphones in the eye's white; the reticle wears the same shape. */
html body .palette .tile .tile-marks .tile-aud { display: flex; align-items: center; justify-content: center;
  width: 1.15rem; height: 1.15rem; border-radius: 999px; background: var(--bg-app); color: var(--c);
  border: 1px solid var(--border-medium); box-sizing: border-box; }
html body .palette .tile .tile-marks .tile-aud svg { width: 0.75rem; height: 0.75rem; }
/* THE EYE OFF (2026-09-24): the cursor tile's glyph is struck through — the
   unpin slash — while the tile is not lit. On, the slash is not drawn. */
html body .palette .tile--lens .lens-slash { display: none; }
html body .palette .tile--lens:not(.on) .lens-slash { display: block; }
/* The drop on a grain tile is a BUTTON (Ek, 2026-09-14: "the wet icon should
   be clickable in the palette tile … all grain tools should have the wet/dry
   toggle on the palette tile"), so every grain tile wears one — outlined dry,
   filled wet, the rail row's shape rule. It says it is pressable the way the
   row's does, with the pointer and a ground on hover; the sticker's disc is
   already the tile's hairline, so nothing else changes. */
html body .palette .tile .tile-marks .tile-wet--btn { cursor: pointer; }
html body .palette .tile .tile-marks .tile-wet--btn:hover { background: var(--surface-3); }
/* The SWITCH — the kit's "do you want it?" boolean (docs/INSTRUMENT-GUI.md
   § 3: 32 × 18, knob 14, 2px inset), built for the first time for a grain
   brush's WET setting in its sheet head. Flat: a track and a knob, the
   track in the engine hue when on, the knob the page ground so it reads as
   a hole in the hue rather than a second colour. No border, no gradient. */
/* The head's own wet button (`.ds-wet`, 2026-09-03 → 2026-09-10) is gone:
   wet is a `.prow--sw` row at the top of the grain brush's DEPOSIT section,
   the sheet's ordinary switch (Ek: "move the wet toggle into the actual
   deposit params not as a header item"). */
html body .mu-switch { position: relative; display: inline-block; flex: none; width: 32px; height: 18px;
  border-radius: 999px; background: var(--surface-2);
  transition: background var(--m-state) var(--ease-out); }
html body .mu-switch b { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
  border-radius: 50%; background: var(--text-faint);
  transition: left var(--m-state) var(--ease-out), background var(--m-state) var(--ease-out); }
/* ── An OFF switch has to be readable (Ek, 2026-09-07: "the on off toggles in
   the engine sheets are too dark when off") ────────────────────────────────
   The shared track is `--surface-2` at 6.5% — right at a desk, under a lamp,
   for the settings window it was drawn for. The instrument is played at arm's
   length with the lights down, which is the whole reason INSTRUMENT-GUI is a
   second kit (§ 1). So the engine sheet's switches get a track you can find
   with the lamp off and a knob at the resting text weight, and OFF stops
   reading as absent — which is what "never dim to mean anything" is about. */
html body .ds-sw .mu-switch { background: var(--border-soft); }
html body .ds-sw .mu-switch b { background: var(--text-tertiary); }
html body .trow.open .trow-more { color: var(--c, var(--text-bright)); }

/* ═══ 24. THE HAND (Ek, 2026-09-12) ═══════════════════════════════════════
   "you pick the tool with the mouse i.e. click it, then you should use it.
   in hand just should mean what the spacebar or click does." Two things:
   the SPACEBAR PLATE under the strip, naming the tool in hand, and the
   LEDGER — the legend as one row per binding kind, each a learn cell.
   Appended, so every rule here is later than the § 23 rule it refines at
   the same specificity (`html body .palette …`). */

/* THE HAND TILE (Ek, 2026-09-12, night: "make the spacebar to the left of the
   tile group, tiles wide, and of course moving the keyboard binding under it
   like the same design as the tiles"). The hand is a TILE at the head of the
   row — the same 53px box, the in-hand tool's glyph in its engine hue, its
   SHAPE the hand's verb in the tile's own radii, lit while the hand plays —
   and under it, in the ledger's own cap, what plays it: the drawn spacebar
   in a wide keycap and the mouse in a keycap. It stands one extra gap off
   the quick-access group: the double gap is the whole separator. (It was a
   plate the row's width — under the bed, then across its head — for one
   evening; `.palette-row` and `.hand-key` went with it.) */
/* THREE TILES WIDE, like a spacebar (Ek, night: "3 tiles wide in design, like
   a spacebar to look special"), at the row's own gap — no extra air: the
   width is the separator. Room for the name beside the glyph. */
html body .palette .tile--hand { width: var(--hand-tile-w); cursor: default;
  display: flex; flex-direction: row; align-items: center; justify-content: center; gap: var(--sp-5); }
/* BOTH NAMES (Ek, 2026-09-22: "the tile should say the shape and the voice in
   text"). Two lines, and the hierarchy is the model's: the SHAPE on top, the
   tool's identity, in the uppercase the tile has always used; the VOICE under
   it in the engine's hue, because hue is identity and the voice is the half no
   other channel carries. A tool with no voice draws one line — the builder
   omits the span rather than leaving an empty one. */
html body .palette .tile--hand .tile-nm { display: flex; flex-direction: column;
  align-items: flex-start; gap: var(--sp-0); min-width: 0; }
html body .palette .tile--hand .tile-nm-shape { font-size: 12px; font-weight: 500;
  letter-spacing: var(--ls-uppercase); text-transform: uppercase; color: var(--text-bright); }
html body .palette .tile--hand .tile-nm-voice { font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-wide); color: var(--c); }

/* IN HAND, on the rail row: the row's ground lifted one step and a 2px left
   inset in the hue — how you find the tool in the library. The strip's tile
   wore a ring too until the hand became a tile of its own; the same glyph at
   the head of the row says it, so the ring said it twice. */

/* THE BINDING STICKER (PALETTE-GUI § 11.5–11.6, Ek 2026-09-12). The same
   device as wet and pin, bottom-left: a disc of the app ground with the
   tile's hairline, 18 tall, a circle at one character and a stadium beyond,
   overhanging the outline by 5px on every shape — a circle holds it by 5.1px
   where a plate holds it fully, and Ek ruled that acceptable, so there is
   one offset and no per-shape exception. Corners: wet top-left, pin
   top-right, binding bottom-left. The hue says the kind: key --text-bright,
   button --leg-button, note --leg-midi. The gesture rides INSIDE at 9.5px;
   long and xlong are a bar; the delay mark follows the gesture.

   THE OVERFLOW TRAP (§ 11.6): the sticker is position:absolute at an offset,
   so it is shrink-to-fit — left at white-space:normal with the default
   flex-shrink it does not overhang, it silently compresses its label to
   min-content and nothing clips, so nothing looks wrong. `flex: none;
   white-space: nowrap` on the sticker AND on every span inside it. */
html body .palette .tile .tile-binds { position: absolute; left: -5px; bottom: -5px; z-index: 2;
  display: flex; flex: none; gap: var(--sp-1); align-items: center; white-space: nowrap; }
html body .palette .tile .tile-bind { display: flex; flex: none; white-space: nowrap; align-items: center; justify-content: center; gap: var(--sp-2);
  height: 18px; min-width: 18px; padding: 0 var(--sp-3); box-sizing: border-box; border-radius: 999px;
  background: var(--bg-app); border: 1px solid var(--border-medium);
  font: inherit; font-size: 11px; font-weight: 400; line-height: 1; letter-spacing: 0; text-transform: none;
  color: var(--text-bright); cursor: pointer; }
html body .palette .tile .tile-bind--button { color: var(--leg-button); }
html body .palette .tile .tile-bind--midi   { color: var(--leg-midi); }
html body .palette .tile .tile-bind--key { text-transform: uppercase; }
/* Unbound: the sticker stays as the learn cell, faint, a dash through it. */
html body .palette .tile .tile-bind--none { color: var(--text-faint); border-color: var(--border-faint); }
html body .palette .tile .tile-bind .leg { display: inline-flex; flex: none; white-space: nowrap; align-items: center; gap: var(--sp-2); }
html body .palette .tile .tile-bind .leg-g { flex: none; white-space: nowrap; font-style: normal; font-size: 9.5px; text-transform: none; color: var(--leg-midi); }
html body .palette .tile .tile-bind .leg-g--bar { display: inline-block; width: 8px; height: 2px; border-radius: 1px; background: var(--leg-midi); }
/* EXTRA LONG IS THICKER, NOT LONGER (Ek, 2026-09-22: "for the long line
   instead of making it longer, make the line thicker"). It was 14px against
   long's 8, which grew the sticker sideways in a pill whose whole constraint is
   that nothing may be wider than the tile (§ N) — and beside a key cap and a
   pointer the extra 6px read as a second mark rather than as more of the same
   one. Same 8px length, twice the ink: the bar says HOW LONG by weight now, and
   the pill's width no longer depends on which gesture is bound. */
html body .palette .tile .tile-bind .leg-g--xlong { height: 4px; border-radius: 2px; }
html body .palette .tile .tile-bind .leg-d { flex: none; white-space: nowrap; font-style: normal; font-size: 9.5px; color: var(--leg-delay); letter-spacing: -1.5px; }
html body .palette .tile .tile-bind .leg-space { display: inline-block; flex: none; width: 16px; height: 8px; color: inherit; }
/* The pointer beside it — square, cap-height, the same ink. */
html body .palette .tile .tile-bind .leg-click { display: inline-block; flex: none; width: 10px; height: 10px; color: inherit; }
html body .palette .tile .tile-bind:hover { border-color: var(--text-bright); }
html body .palette .tile .tile-bind.learning { color: var(--accent-action); border-color: var(--accent-action); animation: leg-learn 0.9s ease-in-out infinite; }
@keyframes leg-learn { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) { html body .palette .tile .tile-bind.learning { animation: none; } }

/* ── THE STICKER OFF THE PALETTE (2026-09-25) ────────────────────────────────
   Ek: "any gui that has a key binding shortcut by factory default should also
   have the same glyph design on the palette tile, and be learnable". The
   palette's `.tile-bind` pill, on a rail row after its label and on the
   chrome's performance buttons at their corner — the same 18px pill, app
   ground, the kit's hairline, key text in the bright tone. */
.row-binds { display: inline-flex; flex: none; gap: var(--sp-1); }
.mrow-l:has(+ .row-binds) { flex-grow: 0; }
.mrow .row-binds { margin-right: auto; }
.row-binds .tile-bind { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  height: 18px; min-width: 18px; padding: 0 var(--sp-3); box-sizing: border-box; border-radius: 999px;
  background: var(--bg-app); border: 1px solid var(--border-medium);
  font: inherit; font-size: 11px; font-weight: 400; line-height: 1; letter-spacing: 0; text-transform: uppercase;
  color: var(--text-bright); cursor: pointer; white-space: nowrap; }
.row-binds .tile-bind--button { color: var(--leg-button); }
.row-binds .tile-bind--midi   { color: var(--leg-midi); text-transform: none; }
.row-binds .tile-bind--none   { color: var(--text-faint); border-color: var(--border-faint); }
.row-binds .tile-bind.learning { border-color: var(--text-bright); }
.row-binds .tile-bind:hover { border-color: var(--text-tertiary); }
/* The GESTURE inside it, as on the palette's pill: a word for tap · ×2 · ×3,
   the BAR for long, the thicker bar for extra long. These rules were scoped to
   `.palette .tile`, so off the palette the bar was an empty element — unpin
   all's ↑ held extra long read as a plain ↑ press (Ek, 2026-09-25). */
.row-binds .tile-bind .leg { display: inline-flex; flex: none; align-items: center; gap: var(--sp-2); }
.row-binds .tile-bind .leg-g { flex: none; font-style: normal; font-size: 9.5px; text-transform: none; color: var(--leg-midi); }
.row-binds .tile-bind .leg-g--bar { display: inline-block; width: 8px; height: 2px; border-radius: 1px; background: var(--leg-midi); }
.row-binds .tile-bind .leg-g--xlong { height: 4px; border-radius: 2px; }
/* (The chrome's buttons wore one too, on their top-right corner, 2026-09-25 —
   gone the same day, Ek: "it's obvious when i hover to see the tool tip". The
   tooltip carries the shortcut; Settings → Keys learns it.) */
/* On a rail action row (unpin all), the sticker follows the name as on a .mrow. */
.trow .tile-nm:has(+ .row-binds) { flex: none; }
.trow .row-binds { margin-right: auto; }

/* ═══ NARROW WINDOWS — the chrome and the footer give way, in tiers ═════════
   The stations (`npm run stations`) tile three windows across one screen, 570px
   each on the 15" laptop and 480 on a 1440 display (Ek, 2026-10-04: one display
   for now). Every rule above lays both bars out as `1fr auto 1fr` — the centre
   dead centre — and nothing gave when the window was narrower than its
   contents: measured at 570, the brand sat 91px into the left group and the
   meters 250px into the level sliders. The old NARROW-WINDOW MODE (max-width
   700, above) styles the cabinet's .top-bar, which nobody sees.

   Nothing is hidden that does something: only the NAME goes, which is what
   § 2 says a performer never needed, and the piece's name stays as long as it
   fits. Measured collision widths, which are the breakpoints:
     footer  levels × audio below 1150   → the centre column takes what is left
     footer  one row cannot hold 956px   → the meters drop to their own row
     footer  cursor + audio + mute 638   → the level sliders take a third row
     chrome  brand × left group below ~780 → the brand shrinks to fit, then goes
     chrome  the two sides need ~580     → the right side takes a second row  */

/* ── The footer ─────────────────────────────────────────────────────────── */
@media (max-width: 1160px) {
  /* The meters stay between the sides instead of on the window's centre line. */
  html body .bottom-bar { grid-template-columns: auto minmax(0, 1fr) auto; }
  html body .bottom-bar > .bottom-bar-levels { justify-self: center; }
}
@media (max-width: 960px) {
  /* A row of air between the rows: each group is exactly its 40px row, so
     without it the meters' bars start on the axis captions' line. */
  html body .bottom-bar { grid-template-columns: minmax(0, 1fr) auto; row-gap: var(--sp-5); }
  html body .bb-side-l { grid-row: 1; grid-column: 1; }
  html body .bb-side-r { grid-row: 1; grid-column: 2; }
  html body .bottom-bar > .bottom-bar-levels { grid-row: 2; grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  /* The right side's own children join the grid, so the level sliders can
     leave the mute's row; the mute keeps its place at the far right. */
  html body .bottom-bar { grid-template-columns: minmax(0, 1fr) auto auto auto; }
  html body .bb-side-r { display: contents; }
  html body .bb-side-r > #tcDryCycle { grid-row: 1; grid-column: 2; }
  html body .bb-side-r > #tcReverb   { grid-row: 1; grid-column: 3; }
  html body .bb-side-r > #tcMute     { grid-row: 1; grid-column: 4; }
  html body .bb-side-r > #tcAudioGroup { grid-row: 3; grid-column: 1 / -1; justify-self: center; }
}

/* ── The chrome ─────────────────────────────────────────────────────────── */
@media (max-width: 800px) {
  /* The middle column is what the sides leave, and the brand is sized by it:
     a container, so what it shows is decided by the room it actually has. */
  html body .tc-bar { grid-template-columns: auto minmax(0, 1fr) auto; }
  html body .tc-brand {
    justify-self: stretch; justify-content: center; min-width: 0; overflow: hidden;
    container-type: inline-size;
  }
  html body .tc-brand b, html body .tc-brand i { display: none; }
  html body .tc-doc { min-width: 0; flex: 0 1 auto; }
}
@container (max-width: 90px) { html body .tc-doc { display: none; } }
@container (max-width: 24px) { html body .tc-mark { display: none; } }
@media (max-width: 600px) {
  /* The two sides no longer fit one row: the right side takes the second,
     flush right, under the brand. */
  html body .tc-bar {
    grid-template-columns: auto minmax(0, 1fr);
    row-gap: var(--sp-4);
    padding-top: var(--sp-5); padding-bottom: var(--sp-5);
  }
  html body .tc-side-r { grid-row: 2; grid-column: 1 / -1; }
}
