/* =============================================================================
   SONGBUILDER — the studio's own sheet
   -----------------------------------------------------------------------------
   Everything here sits on top of assets/css/site.css, which is the house paper
   edition, and under assets/css/dark.css, which is the house dark override.

   THE ONE RULE, the same one the house sheet states: prefer a token. A
   declaration that reads var(--ink) or var(--line) is already correct in both
   editions and needs no second line anywhere. There is exactly one place in
   this file where a token cannot express what is needed — a cell whose fill is
   the ink at an OPACITY that changes per step — and it is answered with two
   lines at the top rather than with a shadow palette scattered through the
   sheet.

   A control panel drawn on white paper is the interesting problem this file
   solves. The instinct is chrome, glow and black plastic; on paper all of that
   is a smear. So the console here is built the way the rest of labs.llc is
   built — hairlines, air, and type doing the work — and the only thing that
   glows is the accent, on the handful of controls that are live.
   ========================================================================== */

/* The ink as CHANNELS, so a fill can be built at an arbitrary alpha. This is
   the one thing --ink cannot do: it is a hex, and a step's velocity is an
   opacity. Two lines here rather than a literal in every rule that needs one. */
:root            { --cell-ink: 30, 38, 43;    --panel: rgba(30, 38, 43, .025); }
html[data-dark]  { --cell-ink: 255, 255, 255; --panel: rgba(255, 255, 255, .04); }

/* A band that is a control panel rather than a piece of writing wants less air
   above and below it than a band of prose does. */
.band--tight { padding-block: clamp(38px, 5vw, 64px); }

.grow1 { flex: 1 1 18rem; min-width: 0; }

/* Fields the house sheet does not have. --mini is NARROWER, never smaller:
   under 16px iOS Safari zooms the page on focus and carries a fixed panel off
   the side of the screen with it. */
.field--num  { width: 6.5rem; font-variant-numeric: tabular-nums; }
.field--mini { padding: .55rem .7rem; font-size: 1rem; }
select.field { cursor: pointer; }

.tbtn--warn { color: var(--taken); border-color: color-mix(in srgb, var(--taken) 40%, transparent); }
.tbtn--warn:hover { border-color: var(--taken); color: var(--taken); }

/* =============================================================================
   THE TRANSPORT

   Sticky under the header, because it is the one panel you reach for from
   anywhere else on the page. It sits at the header's own height so the two
   read as one bar rather than as a bar with a shelf under it.
   ========================================================================== */
.transport {
    position: sticky; top: var(--nav-h); z-index: 60;
    background: var(--paper);
    border-block: 1px solid var(--line);
    /* The blur is what keeps the grid legible as it scrolls underneath. */
    -webkit-backdrop-filter: saturate(1.6) blur(9px);
    backdrop-filter: saturate(1.6) blur(9px);
}
/* Two rows at a laptop width, never three. The bar is sticky and it sits under
   a 74px header: at three rows the two of them together take a third of a
   720px viewport, and the grid the bar exists to control is off the screen.
   Every width below was pulled in until the third row folded away. */
.transport__in {
    display: flex; align-items: flex-end; gap: .55rem .7rem;
    flex-wrap: wrap; padding-block: .6rem;
}
.tfield { display: flex; flex-direction: column; gap: .2rem; }
.tfield .lbl { margin: 0; white-space: nowrap; font-size: .58rem; letter-spacing: .14em; }
.tfield .lbl b { color: var(--accent); font-variant-numeric: tabular-nums; }
.tfield input[type="range"] { margin: 0; }
.transport .field--num { width: 5rem; }
.transport .field--mini { padding: .42rem .5rem; font-size: 1rem; }
.transport .btn--sm, .transport .tbtn { padding-block: .45rem; }

.btn--play { min-width: 7.4rem; }
.btn--play[aria-pressed="true"] { background: var(--accent-fill); color: var(--on-accent); }
.btn--play[aria-pressed="true"]:hover { background: var(--accent); color: #fff; }
.btn--play i { font-style: normal; }
.btn--play[aria-pressed="true"] i { animation: beat 1.2s var(--ease) infinite; }
@keyframes beat { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
@media (prefers-reduced-motion: reduce) { .btn--play[aria-pressed="true"] i { animation: none; } }

/* =============================================================================
   THE GRID

   A scroll container of its own, and this is not decoration. One unwrappable
   row of sixty-four cells widens the whole DOCUMENT if it is allowed to — and
   with it every fixed element on the page, including the header. The grid
   scrolls inside its box; the page never does.
   ========================================================================== */
.gridwrap {
    overflow-x: auto; overflow-y: hidden;
    margin-inline: calc(var(--gutter) * -.5);
    padding-inline: calc(var(--gutter) * .5);
    padding-bottom: .4rem;
    overscroll-behavior-x: contain;
}
/* The 3px is the GAP, and it has to be in this sum. Without it the tracks are
   sized from a width the gaps then eat, and sixty-four steps come out at 21px
   each on a desktop and 27px on a phone — under the size a finger can hit,
   from arithmetic that looked right. */
.grid { display: grid; gap: 3px; min-width: calc(var(--cols) * 27px + 9rem); }

.grow {
    display: grid;
    /* minmax(0, 1fr) and not 1fr: a 1fr track grows to its content's minimum
       width, so one long row would set the width of every other row and the
       whole grid would stop matching its own columns. */
    grid-template-columns: 9rem minmax(0, 1fr);
    gap: 3px; align-items: stretch;
}
.grow__name {
    font: inherit; text-align: left; cursor: pointer;
    background: transparent; color: var(--ink-77);
    border: 1px solid var(--line); border-radius: 7px;
    padding: .3rem .55rem; overflow: hidden;
    transition: border-color .2s var(--ease), background .2s var(--ease);
}
.grow__name b {
    display: block; color: var(--ink); font-size: .8rem; font-weight: 600;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.grow__name span {
    display: block; font-size: .625rem; font-weight: 600; letter-spacing: .1em;
    text-transform: uppercase; color: var(--ink-55);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.grow__name:hover { border-color: var(--ink); }
.grow.is-sel .grow__name { border-color: var(--accent); background: var(--panel); }

.grow__cells { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); gap: 3px; }

.gcell {
    --v: 0;
    position: relative;
    font: inherit; font-size: .58rem; font-weight: 700;
    font-variant-numeric: tabular-nums;
    /* 34px on a phone, so a step is a target a thumb can hit. */
    min-height: 34px;
    padding: 0; cursor: pointer;
    color: var(--ink-55);
    background: transparent;
    border: 1px solid var(--line-soft);
    border-radius: 5px;
    touch-action: none;      /* the drag paints; it must not scroll the page */
    transition: background .09s linear, border-color .12s var(--ease);
}
.gcell.is-beat { border-color: var(--line); background: var(--panel); }
.gcell:hover { border-color: var(--ink-55); }
.gcell.is-on {
    background: rgba(var(--cell-ink), var(--v));
    border-color: rgba(var(--cell-ink), calc(var(--v) + .2));
    color: var(--paper);
}
/* A step that will not always fire is drawn as an edge rather than as a
   different colour: chance is a property of a step, not a kind of step. */
.gcell.is-chance { border-style: dashed; border-color: var(--accent); }
.gcell.is-past { opacity: .3; }
.gcell.is-head { box-shadow: inset 0 0 0 2px var(--accent); }
.gcell.is-head:not(.is-on) { background: color-mix(in srgb, var(--accent) 12%, transparent); }

/* =============================================================================
   THE SCREENS
   ========================================================================== */
.screens { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: 1.1rem; }
.console__body--flush { padding: 0; gap: 0; }
.screen {
    display: block; width: 100%; height: clamp(180px, 26vw, 300px);
    background: transparent;
    cursor: zoom-in;
}

/* FULL SCREEN, ON A DOUBLE CLICK. Two things have to be said here that the
   browser's own fullscreen styling does not do for a canvas:

   THE HEIGHT. The rule above is an explicit `height`, and an author height
   beats the user agent's fullscreen sizing — without this the canvas fills the
   screen's width and stays 300px tall in the middle of a black field.

   THE GROUND. Every screen draws on a TRANSPARENT canvas and lets the page
   show through, which is what makes the same eight visualisers work in both
   editions. Fullscreen has no page behind it, only the browser's backdrop, so
   the paper edition would be drawing dark ink on black. The canvas takes the
   paper's own colour while it is up. */
.screen:fullscreen, .screen:-webkit-full-screen {
    width: 100vw; height: 100vh; height: 100dvh;
    max-height: none; cursor: zoom-out;
    background: var(--paper);
}
.seg--pick {
    padding: .7rem .9rem; border-top: 1px solid var(--line);
    background: var(--paper);
}
.seg--pick .seg__b { font-size: .68rem; padding: .38rem .6rem; }

/* =============================================================================
   THE CHANNEL
   ========================================================================== */
.chgrid {
    display: grid; gap: .9rem 1.1rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
}
.chgrid .lbl b { color: var(--accent); font-variant-numeric: tabular-nums; }
.euclid__row {
    display: grid; gap: .9rem 1.1rem; align-items: end; margin-top: .5rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));
}
.euclid__row .lbl b { color: var(--accent); }

/* =============================================================================
   THE PADS

   Four rows of eight, coloured by row the way every pad controller is. The hue
   is a custom property set per row in JS and everything else is derived from
   it here — so the two editions move lightness and saturation without ever
   naming a colour twice, and a fifth row would be one number.

   Comma syntax inside hsl() on purpose: the space-separated form is fine
   everywhere that matters now, but this one interpolates a var() into a colour
   function and the comma form is the one with no history of disagreement
   about that.
   ========================================================================== */
/* .pad.pad--big, NOT .pad--big, and every rule below says it twice on purpose.

   The deck's small cue pads and these both carry .pad, and `.pad` sets
   `background: transparent`. Two single-class selectors have identical
   specificity, so the winner is whichever is LATER in the file — and .pad
   lives in the deck section, further down. Every pad rendered with no colour
   at all, in both editions, from a stylesheet where both rules were correct.

   Doubling the class takes these to (0,2,0) and settles it by specificity
   instead of by source order, which is the only version of this that survives
   somebody reordering the sheet.
   -------------------------------------------------------------------------- */
.padgrid {
    display: grid; gap: .4rem;
    grid-template-columns: repeat(8, minmax(0, 1fr));
}
.pad.pad--big {
    --pad-h: 265;
    --pad-s: 58%;
    --pad-l: 95%;
    --pad-line: 74%;
    --pad-ink: 30%;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: .1rem; min-height: 56px; padding: .3rem .2rem;
    border: 1px solid hsl(var(--pad-h), var(--pad-s), var(--pad-line));
    background: hsl(var(--pad-h), var(--pad-s), var(--pad-l));
    color: hsl(var(--pad-h), 42%, var(--pad-ink));
    border-radius: 9px; cursor: pointer;
    touch-action: none;          /* the drag plays; it must not scroll */
    user-select: none;
    transition: transform .06s linear, background .1s linear, box-shadow .12s var(--ease);
}
.pad.pad--big b {
    font-size: .72rem; font-weight: 700; line-height: 1.1;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pad.pad--big span {
    font-size: .56rem; font-weight: 600; letter-spacing: .08em;
    text-transform: uppercase; opacity: .62;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pad.pad--big:hover { border-color: hsl(var(--pad-h), var(--pad-s), 52%); }
/* Pressed is a real jump in lightness, not a subtle one: on a pad grid the
   feedback IS the instrument, and a 4% shift is invisible under a finger. */
.pad.pad--big.is-down {
    background: hsl(var(--pad-h), 68%, 58%);
    border-color: hsl(var(--pad-h), 68%, 44%);
    color: #fff;
    transform: scale(.96);
}
.pad.pad--big.is-lit { box-shadow: inset 0 0 0 2px hsl(var(--pad-h), 68%, 50%); }
.pad.pad--big.is-off { opacity: .4; }

html[data-dark] .pad.pad--big {
    --pad-l: 17%;
    --pad-line: 32%;
    --pad-ink: 84%;
}
html[data-dark] .pad.pad--big.is-down { color: #0C1116; }

@media (max-width: 700px) {
    .padgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* The patch picker sits beside its dice, and the pair spans two columns of the
   channel grid — a patch name is a name, and 11rem truncates most of them. */
.chgrid__wide { grid-column: span 2; }
@media (max-width: 560px) { .chgrid__wide { grid-column: span 1; } }
.chpreset { display: flex; gap: .35rem; align-items: stretch; }
.chpreset select { flex: 1 1 auto; min-width: 0; }

/* =============================================================================
   THE RACK

   Sixteen cards. Closed they are a header — a power switch, a name and two
   arrows — and that is the state they live in, because sixteen open cards is
   ninety-six dials and a page nobody can find anything on. Tapping the name
   opens one.
   ========================================================================== */
.rack { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr)); gap: .8rem; }

.fx {
    border: 1px solid var(--line); border-radius: 12px;
    background: var(--panel); overflow: hidden;
    transition: border-color .2s var(--ease);
    align-self: start;
}
.fx.is-on { border-color: var(--accent); }
.fx__head { display: flex; align-items: center; gap: .6rem; padding: .6rem .7rem; }

.fx__pow {
    flex: 0 0 auto; width: 34px; height: 20px; padding: 0;
    border-radius: 999px; cursor: pointer;
    background: transparent; border: 1px solid var(--line);
    transition: background .2s var(--ease), border-color .2s var(--ease);
}
.fx__pow i {
    display: block; width: 12px; height: 12px; border-radius: 50%;
    background: var(--ink-55); margin-left: 3px;
    transition: transform .2s var(--ease), background .2s var(--ease);
}
.fx__pow[aria-pressed="true"] { border-color: var(--accent); background: var(--panel); }
.fx__pow[aria-pressed="true"] i { transform: translateX(14px); background: var(--accent); }

.fx__name {
    flex: 1 1 auto; min-width: 0; text-align: left; cursor: pointer;
    font: inherit; background: transparent; border: 0; padding: 0; color: inherit;
}
.fx__name b { display: block; font-size: .86rem; font-weight: 600; color: var(--ink); }
.fx__name span {
    display: block; font-size: .6rem; font-weight: 700; letter-spacing: .16em;
    text-transform: uppercase; color: var(--ink-55);
}
.fx__ord { display: flex; gap: 2px; flex: 0 0 auto; }
.fx__arrow {
    font: inherit; font-size: .75rem; line-height: 1;
    width: 26px; height: 26px; padding: 0; cursor: pointer;
    background: transparent; color: var(--ink-55);
    border: 1px solid var(--line); border-radius: 6px;
}
.fx__arrow:hover { border-color: var(--ink); color: var(--ink); }

.fx__body { display: none; padding: 0 .7rem .8rem; }
.fx.is-open .fx__body { display: grid; gap: .9rem; }
.fx__note { margin: 0; font-size: .78rem; line-height: 1.5; color: var(--ink-55); }
.fx__acts { display: flex; gap: .4rem; flex-wrap: wrap; }
.dial--mix .dial__val { color: var(--ink); }
.fx .dials { grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr)); gap: .75rem; }

/* =============================================================================
   THE DECK
   ========================================================================== */
.dk-drop {
    display: flex; align-items: center; gap: .8rem; flex-wrap: wrap;
    padding: 1rem; border: 1px dashed var(--line); border-radius: 10px;
    transition: border-color .2s var(--ease), background .2s var(--ease);
}
.dk-drop.is-over { border-color: var(--accent); background: var(--panel); }
.dk-drop .hint { margin: 0; }
.dk-name { font-size: .82rem; font-weight: 600; color: var(--ink); }
.dk-meta { font-size: .7rem; color: var(--ink-55); font-variant-numeric: tabular-nums; }

.dk-wave {
    display: block; width: 100%; height: clamp(90px, 13vw, 150px);
    cursor: crosshair; border-radius: 8px; border: 1px solid var(--line);
}
.dk-main {
    display: grid; gap: 1.1rem; margin-top: 1rem;
    grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
    align-items: start;
}
@media (max-width: 760px) { .dk-main { grid-template-columns: minmax(0, 1fr); } }

.dk-platter {
    display: block; width: 100%; aspect-ratio: 1 / 1; max-width: 15rem;
    margin-inline: auto;
    cursor: grab; touch-action: none; border-radius: 50%;
    transition: box-shadow .2s var(--ease);
}
.dk-platter.is-held { cursor: grabbing; box-shadow: 0 0 0 3px var(--accent); }

.dk-controls { display: grid; gap: .8rem; min-width: 0; }
.dk-row { display: flex; gap: .45rem; flex-wrap: wrap; align-items: center; }
.dk-row .lbl { margin: 0; flex: 0 0 auto; }
.dk-row .hint { margin: 0; flex: 1 1 14rem; }
.dk-inline { display: flex; flex-direction: column; gap: .2rem; flex: 0 1 11rem; min-width: 8rem; }
.dk-inline .lbl b { color: var(--accent); }
.dk-inline input[type="range"] { margin: 0; }
.dk-pos { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
          font-size: .74rem; color: var(--ink-55); margin-left: auto; }

.pads { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: .35rem; flex: 1 1 100%; }
.pad {
    font: inherit; font-size: .78rem; font-weight: 700;
    min-height: 42px; padding: 0; cursor: pointer;
    background: transparent; color: var(--ink-55);
    border: 1px solid var(--line); border-radius: 8px;
    transition: border-color .18s var(--ease), color .18s var(--ease);
}
.pad:hover  { border-color: var(--ink); color: var(--ink); }
.pad.is-set { border-color: var(--info); color: var(--info); }
@media (max-width: 560px) { .pads { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* =============================================================================
   THE OUT
   ========================================================================== */
.xfade { display: flex; align-items: center; gap: .8rem; }
.xfade .lbl { margin: 0; flex: 0 0 auto; }
.xfade input[type="range"] { flex: 1 1 auto; margin: 0; }
.xfade__v {
    flex: 0 0 auto; min-width: 6.5rem; text-align: right;
    font-size: .74rem; color: var(--accent); font-variant-numeric: tabular-nums;
}
#o-rec.is-rec { background: var(--taken); color: #fff; }
#o-rec.is-rec:hover { background: var(--taken); }

/* =============================================================================
   SAVE
   ========================================================================== */
.slots { display: grid; gap: .5rem; }
.slot {
    display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
    padding: .6rem .75rem; border: 1px solid var(--line); border-radius: 9px;
}
.slot__l { flex: 1 1 12rem; min-width: 0; }
.slot__l b { display: block; font-size: .86rem; color: var(--ink); }
.slot__l span { display: block; font-size: .68rem; color: var(--ink-55); }

/* =============================================================================
   THE MANUAL
   ========================================================================== */
.man {
    display: grid; gap: 1.6rem 2.4rem; margin-bottom: 2.4rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
}
.man h3 {
    font-size: .95rem; font-weight: 600; margin-bottom: .45rem;
    padding-top: .7rem; border-top: 1px solid var(--line);
}
.man p { margin: 0; font-size: .89rem; line-height: 1.62; color: var(--ink-77); }
.man code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: .92em; padding: .05em .35em; border-radius: 4px;
    border: 1px solid var(--line);
}

/* =============================================================================
   THE DARK EDITION — the few places a token could not answer
   -----------------------------------------------------------------------------
   Short on purpose. Everything above is written in tokens, so this is only the
   surfaces: the sticky bar, whose ground is the page's own colour, and the
   pick strip under a screen, which is the same case.
   ========================================================================== */
html[data-dark] .transport { background: rgba(12, 17, 22, .88); }
html[data-dark] .seg--pick { background: var(--paper); }
html[data-dark] .btn--play[aria-pressed="true"] { background: var(--accent); color: #1A0E04; }
html[data-dark] .btn--play[aria-pressed="true"]:hover { background: var(--accent); }
html[data-dark] #o-rec.is-rec { color: #1A0E04; }

/* =============================================================================
   TOUCH

   Matched on POINTER, not on width. A phone held sideways is 800px across and
   a laptop window dragged narrow is 400px; sizing a control off the viewport
   gets both of them wrong. Every number in this block exists because a finger
   is about 9mm across and a mouse is one pixel.
   ========================================================================== */
@media (pointer: coarse) {
    /* THE 16px FLOOR. Under it, iOS Safari zooms the page when the field takes
       focus — and a zoomed page carries every fixed element off the side of
       the screen with it. The house .field is .95rem, and a rem here is 16px,
       so it lands at 15.2 and trips it by four fifths of a pixel. */
    .field, .field--mini, select.field, textarea.field,
    .transport .field--num, .transport .field--mini { font-size: 16px; }

    input[type="range"] { height: 30px; }
    /* min-WIDTH as well as height: a button whose label is a single glyph is
       34px tall and 28px wide, which passes a height check and still misses
       under a thumb. */
    .tbtn, .seg__b { min-height: 34px; min-width: 38px; }
    .fx__arrow { width: 32px; height: 32px; }

    /* The rack's power switch keeps its drawing — the thumb travel is written
       against its width, so growing the button would leave the dot short of
       the end. It gets a bigger HIT AREA instead, which is invisible. */
    .fx__pow { position: relative; }
    .fx__pow::after { content: ''; position: absolute; inset: -9px; }
}

/* =============================================================================
   PHONES
   ========================================================================== */
@media (max-width: 620px) {
    .transport { position: static; }   /* two sticky bars on a phone is the
                                          whole viewport gone before the music */
    .transport__in { gap: .6rem .7rem; }
    .grow { grid-template-columns: 6.6rem minmax(0, 1fr); }
    .grid { min-width: calc(var(--cols) * 36px + 6.6rem); }
    .dk-pos { margin-left: 0; }
    .man h3 { padding-top: .4rem; }
}

/* =============================================================================
   PRESS

   Every control on this page had a :hover and none of them had an :active, and
   the house sheet went further and said `.btn:active { transform: none; }` —
   so a button that was doing a real amount of work looked, for the whole time
   it was doing it, exactly like a button that had not been pressed. That reads
   as a page that is not listening, and no amount of making the work faster
   fixes it, because the work was never the thing you were waiting for.

   Two rules and one non-rule:

   TOUCH-ACTION: MANIPULATION on everything that is tapped. Without it a
   browser holds every tap for about 300ms to see whether a second one is
   coming and turns it into a double-tap zoom. That delay is not the page being
   slow; it is the page being told nothing about what a tap means. The grid
   cells and the dials keep `touch-action: none` because they are DRAGGED —
   painting a row of steps must not scroll the page underneath it.

   NO TRANSITION ON THE WAY DOWN. A press has to land on the frame the finger
   does. Fading in over 120ms is exactly as late as no feedback at all for the
   first 60 of them; the release is where an ease belongs, and it is the only
   half that has one here.

   And -webkit-tap-highlight-color: transparent, because the grey box iOS draws
   arrives after its own delay and in a colour that belongs to neither edition.
   Replacing it means drawing our own, which is what the rules below are.
   ========================================================================== */
.btn, .tbtn, .seg__b, .fx__pow, .fx__arrow, .pad, select.field,
button[data-roll], .grow__name, .mode-pill__seg, .chip, .patb {
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}
/* A long press on a mix key must not raise the text-selection callout. */
.patb { user-select: none; -webkit-user-select: none; }

.btn:active, .tbtn:active, .seg__b:active, .fx__arrow:active, .grow__name:active {
    transition: none;
    transform: translateY(1px) scale(.985);
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 14%, transparent);
}
/* The house sheet cancels the transform on .btn:active site-wide. This page is
   an instrument: a control that does not answer the finger is a broken one, so
   here it is put back — later in the cascade, and said out loud so nobody
   removes it again wondering what it was for. */
.btn:active { transform: translateY(1px) scale(.985); }

.gcell:active { transition: none; border-color: var(--accent); }
.pad:active, .pad.pad--big:active { transition: none; transform: scale(.97); }

/* THE ROLL, WHICH IS THE ONE THAT DOES ENOUGH WORK TO NEED IT.

   A roll rewrites eight rows, sixteen effect slots and the whole grid. On a
   desktop that is about fifteen milliseconds and on a phone it can be ten
   times that — long enough to wonder whether the tap registered. So the
   button says so: it takes .is-rolling on pointerdown, which is before the
   work starts rather than after it finishes, and drops it on the next frame
   after the work is done. */
.is-rolling {
    transition: none;
    border-color: var(--accent) !important;
    background: color-mix(in srgb, var(--accent) 18%, transparent) !important;
}
@media (prefers-reduced-motion: no-preference) {
    .is-rolling { animation: sb-roll .5s var(--ease); }
}
@keyframes sb-roll {
    from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }
    to   { box-shadow: 0 0 0 9px transparent; }
}

/* =============================================================================
   THE DESK — the bank, the finder, the steppers and the transport locks
   =============================================================================
   Everything here is house tokens; nothing is a literal. The dark edition is a
   palette swap and this file is never edited for it.
   ========================================================================== */

/* ------------------------------------------------------------- the steppers
   The arrows are a column beside the field, each half the field's height, so
   the pair occupies no more vertical space than the control it steps. They are
   deliberately outside the <label> — see the note in index.html. */
/* THE BAR IS STICKY, SO EVERY PIXEL IT GAINS IS A PIXEL OF GRID LOST FOR THE
   WHOLE SESSION. Arrows in a column beside each field pushed it from two rows
   to three — 91px to 159px, measured — so on the number fields they sit INSIDE
   the box instead, over padding the field gives up for them. The selects keep
   an outside column because the right edge of a select already belongs to the
   operating system's own chevron, and two arrows on top of it is a mess. */
.stepr { position: relative; display: flex; align-items: stretch; gap: .2rem; }
.stepr > .field { flex: 1 1 auto; min-width: 0; }
.stepr:has(> input) > .field { padding-right: 1.5rem; }
.stepr:has(> input) > .stepr__c {
    position: absolute; right: 3px; top: 3px; bottom: 3px; gap: 1px;
}
.stepr:has(> input) > .stepr__c button { width: 1.15rem; border-color: transparent; }
.stepr__c { display: flex; flex-direction: column; gap: 2px; flex: 0 0 auto; }
.stepr__c button {
    -webkit-appearance: none; appearance: none;
    flex: 1 1 0;
    width: 1.15rem;
    min-height: .95rem;
    display: flex; align-items: center; justify-content: center;
    padding: 0;
    font-size: .5rem;
    line-height: 1;
    color: var(--ink-55);
    background: none;
    border: 1px solid var(--line);
    border-radius: 4px;
    cursor: pointer;
    transition: color .15s var(--ease), border-color .15s var(--ease), background .15s var(--ease);
}
.stepr__c button:hover {
    color: var(--on-accent); background: var(--accent-fill); border-color: var(--accent-fill);
}
.stepr__c button:active { transform: translateY(.5px); }
.stepr__c button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }


.tbtn--die { font-size: 1.05rem; line-height: 1; padding-inline: .6rem; }

/* -------------------------------------------------------------- the finder
   A field in the transport and a list under it. The list is absolute so
   opening it cannot push the sticky bar taller and shove the grid down the
   page mid-search. */
.find { position: relative; }
.find > .field { width: 100%; }
.find__out {
    position: absolute;
    top: calc(100% + .3rem);
    left: 0;
    min-width: 17rem;
    max-width: 24rem;
    max-height: 17rem;
    overflow-y: auto;
    z-index: 70;
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 8px;
    box-shadow: 0 14px 40px rgba(30, 38, 43, .16);
    padding: .25rem;
}
.find__hit {
    -webkit-appearance: none; appearance: none;
    display: block; width: 100%;
    text-align: left;
    padding: .42rem .55rem;
    background: none; border: 0; border-radius: 5px;
    font: inherit; color: var(--ink-77);
    cursor: pointer;
}
.find__hit b { display: block; font-size: .82rem; font-weight: 500; color: var(--ink); }
.find__hit span { display: block; font-size: .66rem; letter-spacing: .06em; color: var(--ink-55); }
.find__hit:hover, .find__hit.is-on { background: color-mix(in srgb, var(--accent) 10%, transparent); }

/* What the finder does when it arrives. A flash, not a selection. */
.is-found {
    animation: found 1.8s var(--ease);
    border-radius: 8px;
}
@keyframes found {
    0%, 70% { box-shadow: 0 0 0 3px var(--accent), 0 0 0 8px color-mix(in srgb, var(--accent) 22%, transparent); }
    100%    { box-shadow: 0 0 0 3px transparent, 0 0 0 8px transparent; }
}
@media (prefers-reduced-motion: reduce) { .is-found { animation: none; outline: 2px solid var(--accent); } }

/* ---------------------------------------------------------------- the bank
   Ten slots on one row on a desk, five and five on a phone. Tall enough to hit
   and wide enough to carry the name of what is in them. */
.bank {
    display: grid;
    grid-template-columns: repeat(10, minmax(0, 1fr));
    gap: .4rem;
    margin-bottom: .8rem;
}
.bank__b {
    -webkit-appearance: none; appearance: none;
    position: relative;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: .12rem;
    /* SQUARE. A patch slot is a key on an instrument, not a card. */
    aspect-ratio: 1;
    min-height: 0;
    padding: .35rem .3rem;
    background: var(--tint);
    border: 1px solid var(--line);
    border-radius: 5px;
    font: inherit;
    color: var(--ink-55);
    cursor: pointer;
    overflow: hidden;
    transition: border-color .15s var(--ease), background .15s var(--ease), color .15s var(--ease);
}
.bank__i { font-size: .58rem; letter-spacing: .14em; opacity: .7; }
.bank__n {
    font-size: .68rem;
    line-height: 1.15;
    text-align: center;
    /* A slot name is as long as the person made it; the row must not widen. */
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
    word-break: break-word;
}
.bank__b:hover { border-color: var(--accent); color: var(--ink); }
.bank__b.is-full { background: var(--paper); color: var(--ink); border-color: var(--line); }
.bank__b.is-full .bank__i { color: var(--accent); opacity: 1; }
.bank__b.is-sel { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.bank__b:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
html[data-dark] .bank__b { background: var(--deep-soft); }
html[data-dark] .bank__b.is-full { background: var(--deep); }

@media (max-width: 720px) {
    .bank { grid-template-columns: repeat(5, minmax(0, 1fr)); }
        .find__out { min-width: 0; width: 100%; max-width: none; }
}

/* The number fields give up 1.5rem of their inside edge to the arrows, so they
   need it back: at the transport's 5rem a tempo of 131.1 was rendering as
   "110." with the last glyph clipped. Measured against the widest value each
   field can hold — 131.1 and -24. */
.transport .stepr > .field--num { width: 6.4rem; }
.transport .stepr > #t-pitch    { width: 5.4rem; }


/* =============================================================================
   THE TRANSPORT, REBUILT
   =============================================================================
   It had grown to thirteen controls in a wrapping flex row and was spilling
   onto three rows of a STICKY bar — 159px of viewport gone before a single
   step of the grid. The fix is grouping, not deletion: five values in one
   frame read as one instrument and occupy about what two of them used to.

   The dice come first. A song here starts with a roll far more often than it
   starts with a blank grid.
   ========================================================================== */
.transport__in {
    display: flex;
    align-items: center;
    gap: .42rem .55rem;
    flex-wrap: wrap;
    padding-block: .42rem;
}

/* A group is a run of controls that belong together and must not be split by
   a wrap. */
.tgrp { display: flex; align-items: center; gap: .35rem; flex: 0 0 auto; }
.tgrp--flow { flex: 1 1 16rem; min-width: 0; gap: 1rem; align-items: flex-end; }
/* The row break. Zero height, full width: the flex row after it starts fresh. */
.tbreak { flex: 0 0 100%; height: 0; margin: 0; }

/* ------------------------------------------------------------------- dice */
.dieb {
    -webkit-appearance: none; appearance: none;
    display: inline-flex; align-items: center; gap: .4rem;
    padding: .5rem .8rem;
    background: none;
    border: 1px solid var(--line);
    border-radius: 9px;
    font: inherit; font-size: .68rem; font-weight: 600;
    letter-spacing: .12em; text-transform: uppercase;
    color: var(--ink-77);
    cursor: pointer;
    transition: color .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease);
}
.dieb:hover { color: var(--on-accent); background: var(--accent-fill); border-color: var(--accent-fill); }
.dieb:active { transform: translateY(.5px); }
.dieb:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.dieb--main {
    background: var(--ink); color: var(--paper); border-color: var(--ink);
    padding-inline: 1rem;
}
.dieb--main:hover { background: var(--accent); border-color: var(--accent); color: #fff; }
.dieb__i { flex: 0 0 auto; }

/* ------------------------------------------------------------- square keys
   One job, no word for it. 34px is the smallest square that is still a
   comfortable target beside a 33px field. */
.sqb {
    -webkit-appearance: none; appearance: none;
    width: 2.1rem; height: 2.1rem;
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0;
    background: none;
    border: 1px solid var(--line);
    border-radius: 7px;
    font: inherit; font-size: .6rem; font-weight: 600;
    letter-spacing: .08em; text-transform: uppercase;
    color: var(--ink-55);
    cursor: pointer;
    flex: 0 0 auto;
    transition: color .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease);
}
.sqb:hover { color: var(--on-accent); background: var(--accent-fill); border-color: var(--accent-fill); }
.sqb:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sqb--tap { align-self: flex-end; }

/* A <label> for a hidden file input is the only control the browser will let
   open a file picker, so OPEN is a label wearing a button's clothes. */
.sqb--file { cursor: pointer; }

/* ------------------------------------------------------------------ the tape
   A square with a dot in it, like every record button since 1965, and it GROWS
   a clock while it is running — a recorder that does not say how long it has
   been recording is a recorder you stop too early or forget entirely. */
/* THE SQUARE NEVER CHANGES. Recording used to widen the button for a clock,
   which shoved the serial field along; the clock lives OVER the serial box
   now (below) and the button's whole recording face is its blinking light. */
/* Pressed, the whole key goes red and its light pulses on it — the armed
   record button every deck has had since decks. The dot rides currentColor,
   so on the filled key it pulses in the fill's own ink. */
.sqb--rec[aria-pressed="true"] {
    color: var(--on-accent);
    background: var(--accent-fill);
    border-color: var(--accent-fill);
}
.recdot {
    width: 9px; height: 9px; border-radius: 50%;
    background: currentColor; flex: 0 0 auto;
}
.sqb--rec[aria-pressed="true"] .recdot { animation: recpulse 1.4s infinite; }
.sqb--rec[aria-pressed="true"]:hover .recdot { background: currentColor; }
.sqb__t {
    font-size: .62rem; font-weight: 600; letter-spacing: .04em;
    font-variant-numeric: tabular-nums;
    font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
}
.sqb__t[hidden] { display: none; }
@keyframes recpulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) {
    .sqb--rec[aria-pressed="true"] .recdot { animation: none; }
}

/* =============================================================================
   THE HEAD ROW, FITTED EDGE TO EDGE

   The bar looked lop-sided because both of its top rows stopped short: the
   buttons ended a third of the way across and the value panel ended two thirds,
   leaving a ragged right margin down the whole control panel. A row that stops
   where its content happens to stop is not a panel, it is a pile.

   Three things fix it and none of them stretches a square:

     · PLAY IS THE SAME BUTTON AS ROLL. It was 102x46 against Roll's 88x36 —
       taller AND wider than the control it sits beside, for no reason but that
       it inherited the house .btn. They are one size now, because they are one
       kind of thing: the two buttons that start something.
     · FOUR IS A MARK. Its word was costing more width than the control is
       worth; the four pips against Roll's five are the whole label.
     · THE FILE CLUSTER GOES TO THE RIGHT EDGE. Save, open, link, record and
       the toast T are not transport actions — they are what you do WITH a song
       — so they take the far end and the row spans the full measure with one
       deliberate gap in the middle, which is where hardware puts it too. A
       margin rather than a stretch: eight buttons pulled evenly apart would be
       a row of buttons nobody can hit twice in the same place.
   ========================================================================== */
.transport .btn--play,
.transport .dieb--main {
    min-width: 5.9rem;
    height: 2.25rem;
    padding: .4rem .7rem;
    font-size: .68rem;
    letter-spacing: .12em;
    border-radius: 9px;
    line-height: 1;
}
.transport .btn--play {
    display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
}
/* The break in the row: everything from here sits at the far end. */
/* The file block: three across, two deep, even gaps — and IT carries the
   margin-left:auto that pins the cluster to the row's right edge. */
.t-files {
    margin-left: auto;
    display: grid; grid-template-columns: repeat(3, auto);
    gap: 4px; justify-content: end; align-self: center;
}

/* The T is a letter, not a drawing — the only square in the row that is. */
.sqb--t > b { font-size: .8rem; font-weight: 700; font-style: normal; letter-spacing: 0; }
.sqb--t[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }

/* THE SQUARES ARE THE SAME FAMILY AS ROLL AND PLAY.

   They were 34px tall with a 7px radius beside two 36px buttons with a 9px one:
   two pixels and two pixels, which is exactly the size of difference the eye
   reads as "these were drawn by different people". Same height, same corner,
   same hairline — the row is one set of controls now, and the only thing still
   telling Roll and Play apart from the rest is the fill, which is the thing
   that should: those two start something, these five do something with what
   has started.

   Squares, still: `width` tracks `height`, so the mark stays centred in a
   square rather than in a slightly wide box. */
.transport .sqb {
    width: 2.25rem; height: 2.25rem;
    border-radius: 9px;
}

/* ------------------------------------------------- the toast switch, in the
   menu foot, beside the edition pill.

   site.css centres the foot's single child; a second one needs the gap, and
   the gap belongs HERE rather than there — assets/css/site.css is the shared
   shell this property carries a byte-identical copy of, and a rule added to it
   for one page is a copy that has stopped being a copy.

   The mark is a speech bubble with a stroke through it, and the stroke is what
   changes: on, it is not drawn; off, it is. A control that reports its state by
   the PRESENCE of a mark rather than by a colour still reports it to anyone who
   cannot separate the two colours. */
.menu__foot { gap: .6rem; flex-wrap: wrap; }
.toastb {
    -webkit-appearance: none; appearance: none;
    display: inline-flex; align-items: center; gap: .42rem;
    padding: .6rem .8rem;
    font: inherit; font-size: .68rem; font-weight: 600;
    letter-spacing: .13em; text-transform: uppercase;
    color: var(--ink-55);
    background: var(--tint);
    border: 1px solid var(--line);
    border-radius: 999px;
    cursor: pointer;
    transition: color .15s, border-color .15s, background .15s;
}
.toastb:hover { color: var(--ink); border-color: var(--ink-55); }
.toastb__i { width: 15px; height: 15px; flex: 0 0 auto; }
.toastb[aria-pressed="true"] { color: var(--ink); border-color: var(--accent); }
.toastb[aria-pressed="true"] .toastb__i { color: var(--accent); }
/* The stroke is drawn only when they are OFF. */
.toastb[aria-pressed="true"] .toastb__slash { display: none; }
html[data-dark] .toastb { background: var(--deep-soft); }

/* ------------------------------------------------------------ the roll name
   A full-width flex item, so it takes the first row of the transport on its
   own and the dice start fresh underneath it.

   ONE LINE IS THE BUDGET. This bar is sticky and everything it takes it takes
   for the whole session, so the caption is the same .55rem the field captions
   are, the name sits just above it at .82rem, and the whole strip is about
   thirteen pixels tall — a caption's worth of bar for the one string that
   tells two rolls apart.

   The name TRUNCATES and the mix does not: "Neon Vigil" and "Neon Vigil ·
   Half-time" are two different records and a row that drops the half that
   distinguishes them is a row that lies. min-width:0 on the name is what lets
   it shrink at all — a flex item's floor is its content otherwise, and one
   long name would push the mix off the end of the bar. */
.tname {
    flex: 0 0 100%;
    display: flex; align-items: baseline; gap: .42rem;
    min-width: 0;
    line-height: 1.15;
    padding-inline: .1rem;
    /* The name sits ON its buttons — the row gap below it was a floor of
       air between the title and the instrument it names. */
    margin-bottom: -.45rem;
}
.tname__cap {
    flex: 0 0 auto;
    font-size: .55rem; font-weight: 500;
    letter-spacing: .15em; text-transform: uppercase;
    color: var(--ink-55);
}
.tname__n {
    flex: 0 1 auto; min-width: 0;
    font-size: .95rem; font-weight: 650; letter-spacing: .005em;
    color: var(--ink);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tname__mix {
    flex: 0 0 auto;
    font-size: .58rem; font-weight: 600;
    letter-spacing: .1em; text-transform: uppercase;
    color: var(--accent);
    white-space: nowrap;
}
.tname__mix::before { content: "\00b7"; margin-inline-end: .38rem; color: var(--ink-55); }

/* ----------------------------------------------------------- the value bank
   Five fields in one frame with one hairline between each, so the eye reads a
   panel rather than five floating columns. */
.tgrp--vals {
    align-items: flex-end;
    gap: .5rem;
    padding: .3rem .55rem;
    border: 1px solid var(--line);
    border-radius: 11px;
    background: var(--tint);
}
html[data-dark] .tgrp--vals { background: var(--deep-soft); }

.tf { display: flex; flex-direction: column; gap: .18rem; min-width: 0; }
.tf__cap {
    display: flex; align-items: center;
    font-size: .55rem; font-weight: 500;
    letter-spacing: .15em; text-transform: uppercase;
    white-space: nowrap;
    color: var(--ink-55);
}
.tf__cap b { color: var(--accent); font-variant-numeric: tabular-nums; margin-left: .3em; }
.tf--wide  { flex: 0 1 8.2rem; }
.tf--swing { flex: 0 1 8rem; min-width: 5.5rem; }
.tf--pats  { flex: 0 0 auto; min-width: 0; margin-left: calc(var(--t-left) + .55rem); }
.tf--find  { flex: 0 0 12rem; }
.tf input[type="range"] { margin: 0; }
.transport .field--mini { padding: .38rem .5rem; font-size: 1rem; }
.transport .field--num  { width: 4.4rem; }
.transport .stepr > .field--num { width: 5.6rem; }
.transport .stepr > #t-pitch    { width: 5rem; }

@media (max-width: 900px) {
    .tgrp--vals { flex-wrap: wrap; }
    .tf--find { flex: 1 1 100%; }
    .find__out { min-width: 0; width: 100%; max-width: none; }
}


/* =============================================================================
   THE VALUE PANEL, THE LOCK, AND THE TWENTY PATTERNS
   ========================================================================== */

/* ------------------------------------------------- every box the same height
   The five fields were three different heights: a number input, a select with
   the OS chevron, and a select with an arrow column beside it all size
   themselves differently, and lined up along their baselines the panel read as
   a skyline. One height token, set once, applied to every control and to both
   halves of every arrow column — so the tops line up, the bottoms line up, and
   the arrows are exactly half the field each. */
.tgrp--vals { --tf-h: 2.35rem; align-items: flex-end; }
.tgrp--vals .field,
.tgrp--vals .stepr,
.tgrp--vals .sqb {
    height: var(--tf-h);
    box-sizing: border-box;
}
.tgrp--vals .field { display: flex; align-items: center; }
.tgrp--vals select.field { padding-block: 0; }
.tgrp--vals .stepr__c { height: var(--tf-h); }
.tgrp--vals .stepr:has(> select) > .stepr__c { justify-content: stretch; }
.tgrp--vals .stepr__c button { min-height: 0; height: 50%; }
.tgrp--vals .stepr:has(> input) > .stepr__c { top: 3px; bottom: 3px; height: auto; }

/* --------------------------------------------------------------- the lock
   A padlock that shuts. It was an unlabelled dot, which is a control that has
   to be discovered rather than read; it says LOCK now, in the smallest type on
   the bar, and the shackle lifts when it is open. */
.lockb {
    -webkit-appearance: none; appearance: none;
    display: inline-flex; align-items: center; gap: .18rem;
    margin-left: .3rem;
    padding: .08rem .3rem .08rem .22rem;
    background: none;
    border: 1px solid var(--line);
    border-radius: 999px;
    font: inherit;
    font-size: .46rem;
    font-weight: 600;
    letter-spacing: .1em;
    text-transform: uppercase;
    line-height: 1;
    color: var(--ink-55);
    cursor: pointer;
    transition: color .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease);
}
.lockb svg { width: .58rem; height: .58rem; overflow: visible; }
.lockb .lockb__shackle { transition: transform .18s var(--ease); transform-origin: 12px 10px; }
/* Open: the shackle is up and off to one side. Shut: it drops onto the body. */
.lockb:not(.is-on) .lockb__shackle { transform: translate(2.4px, -1.6px) rotate(14deg); }
.lockb:hover { color: var(--accent); border-color: var(--accent); }
.lockb.is-on {
    color: var(--on-accent);
    background: var(--accent-fill);
    border-color: var(--accent-fill);
}
.lockb:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --------------------------------------------------- the two tiny switches */
.tf--tiny { justify-content: flex-end; }
.tinyb {
    -webkit-appearance: none; appearance: none;
    height: var(--tf-h);
    padding: 0 .6rem;
    background: none;
    border: 1px solid var(--line);
    border-radius: 7px;
    font: inherit; font-size: .58rem; font-weight: 600;
    letter-spacing: .1em; text-transform: uppercase;
    color: var(--ink-55);
    cursor: pointer;
    transition: color .16s var(--ease), border-color .16s var(--ease), background .16s var(--ease);
}
.tinyb:hover { color: var(--accent); border-color: var(--accent); }
.tinyb.is-on { color: var(--on-accent); background: var(--accent-fill); border-color: var(--accent-fill); }
.tinyb:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The icon-only pair. Square rather than wide, because a key with a mark on it
   and no word has nothing to set its width but the mark. */
.tinyb--i {
    display: inline-flex; align-items: center; justify-content: center;
    width: var(--tf-h); padding: 0;
}
.tinyb--i svg { width: 15px; height: 15px; display: block; }

/* ========================================================== THE TWENTY KEYS
   Square, because a pattern slot is a key on an instrument. Ten across, two
   down, and every one of them carries its own hue for life — --patc is set
   from the index by studio.js, so pattern 7 is the same green in every session
   and in somebody else's browser. That is what makes the row a memory: after
   ten minutes you reach for a colour, not for a position.

   Three states, and they say three different things:
     outline      · empty
     tinted       · has something in it
     lit + raised · the one you are editing
   ========================================================================== */
#t-pats {
    display: grid;
    /* A FIXED KEY, NOT A FRACTION. `1fr` plus `aspect-ratio: 1` means the key
       is as tall as a tenth of whatever width the flex row happens to hand it
       — which on a wide screen was 56px a side and pushed the sticky bar to
       271px. A key is a key: 26px, and the row is as wide as twenty of them. */
    grid-template-columns: repeat(10, 26px);
    gap: 4px;
    justify-content: start;
}
.patb {
    -webkit-appearance: none; appearance: none;
    position: relative;
    aspect-ratio: 1;
    min-height: 26px;
    display: flex; align-items: center; justify-content: center;
    padding: 0;
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 6px;
    font: inherit;
    font-size: .66rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--ink-55);
    cursor: pointer;
    /* The press. A key that does not move under the finger does not read as a
       key, and 60ms is the shortest travel the eye still registers. */
    transition: transform .06s linear,
                background .16s var(--ease),
                border-color .16s var(--ease),
                color .16s var(--ease),
                box-shadow .16s var(--ease);
}
html[data-dark] .patb { background: var(--deep-soft); }

/* SCOPED TO #t-pats, and that is not decoration.

   `html[data-dark] .patb { background: var(--deep-soft) }` is an element, an
   attribute and a class — specificity (0,2,1) — and every state rule below was
   a plain pair of classes at (0,2,0). The base won, so a key that was full and
   a key that was empty and the key being edited all painted the same grey and
   the colours never appeared at all. An id in the selector settles it once for
   every state, and these keys exist nowhere else. */
#t-pats .patb:hover {
    border-color: hsl(var(--patc) 70% 46%);
    color: var(--ink);
    transform: translateY(-1px);
}
#t-pats .patb:active { transform: translateY(1px) scale(.97); }
#t-pats .patb:focus-visible { outline: 2px solid hsl(var(--patc) 70% 46%); outline-offset: 2px; }

/* Has content. A wash of its own colour, no more — this is a hint, not a
   selection. */
#t-pats .patb.is-full {
    background: hsl(var(--patc) 70% 46% / .16);
    border-color: hsl(var(--patc) 70% 46% / .42);
    color: var(--ink);
}
html[data-dark] #t-pats .patb.is-full { background: hsl(var(--patc) 72% 58% / .2); }

/* The one being edited. Filled, and standing slightly off the surface. */
/* Doubled class: the dark has-content rule carries an element in its
   selector and outscored a plain attribute one, so the key being edited
   was painted in the 20%-alpha wash instead of its own solid colour. */
#t-pats .patb[aria-pressed="true"] {
    background: hsl(var(--patc) 68% 42%);
    border-color: hsl(var(--patc) 68% 42%);
    color: #fff;
    box-shadow: 0 2px 0 hsl(var(--patc) 68% 30%), 0 3px 9px hsl(var(--patc) 68% 42% / .34);
    transform: translateY(-1px);
}
html[data-dark] #t-pats .patb[aria-pressed="true"] {
    background: hsl(var(--patc) 74% 56%);
    border-color: hsl(var(--patc) 74% 56%);
    color: #101318;
    box-shadow: 0 2px 0 hsl(var(--patc) 74% 38%), 0 3px 12px hsl(var(--patc) 74% 56% / .4);
}
#t-pats .patb[aria-pressed="true"]:active { transform: translateY(1px) scale(.97); box-shadow: none; }

/* ------------------------------------------------------------- the marks
   The key carries a drawn symbol rather than its number. 15px inside a 26px
   key, which leaves the same 5.5px of air a numeral had — the row's rhythm is
   unchanged and only what is standing in it is different.

   `overflow: visible` because several of the marks are drawn to the very edge
   of their 24-unit box (the scratch zigzag, the fast-forward pair) and a
   viewBox that clips is a mark with a flat side nobody drew. */
.patb__i { width: 15px; height: 15px; display: block; overflow: visible; }
@media (max-width: 900px) { .patb__i { width: 14px; height: 14px; } }

/* ------------------------------------------------------ the five held keys
   The effects are a different kind of control and they say so before they are
   pressed, because pressing one to find out is exactly the thing you cannot
   undo: a latching key changes the song and a held key changes the moment.

   A dashed border is the whole signal — the same key, the same hue, the same
   place in the row, drawn as something that is not permanent. They never take
   the filled `aria-pressed` state (paintPats skips them), so nothing about them
   can read as "this is the mix you are on". */
#t-pats .patb--fx {
    border-style: dashed;
    border-color: hsl(var(--patc) 70% 46% / .5);
    color: var(--ink-55);
}
html[data-dark] #t-pats .patb--fx { border-color: hsl(var(--patc) 72% 58% / .5); }
#t-pats .patb--fx:hover { border-style: dashed; }

/* DOWN. Not a toggle state and not a hover — the key is being held, and it is
   filled for exactly as long as the finger is on it. `.is-held` is added on
   pointerdown and removed by every way a press can end, so it cannot outlive
   the effect it is showing. */
#t-pats .patb--fx.is-held {
    /* INK, not the hue: engaged mixes are painted with their hue, and a held
       effect painted the same way made a latched brake indistinguishable
       from the lit arrangement. Ink is the one colour no mix key wears. */
    background: var(--ink);
    border-style: solid;
    border-color: var(--ink);
    color: var(--paper);
    transform: translateY(1px) scale(.96);
    box-shadow: none;
}
html[data-dark] #t-pats .patb--fx.is-held {
    /* Same rule as paper: ink flips to white there, and white is likewise a
       colour no engaged mix key wears on the dark ground. */
    background: var(--ink);
    border-color: var(--ink);
    color: var(--paper);
}
/* A held key must not be draggable or selectable: a scratch is a press that
   moves, and on a touch screen a press that moves is a scroll unless it is
   told otherwise. */
#t-pats .patb--fx { touch-action: none; -webkit-user-select: none; user-select: none; }

@media (prefers-reduced-motion: reduce) {
    .patb, .lockb .lockb__shackle { transition: none; }
    #t-pats .patb:hover, #t-pats .patb[aria-pressed="true"] { transform: none; }
    #t-pats .patb--fx.is-held { transform: none; }
}

/* ================================================================== MOBILE
   The bar is sticky, so on a phone every row it gains is a row of grid lost
   for the whole session. Two things happen: the value panel wraps INSIDE its
   own frame rather than pushing the bar wider, and the twenty keys go to ten
   across two rows of larger squares — 34px minimum, which is the floor for a
   thing you hit while a song is running.
   ========================================================================== */
@media (max-width: 760px) {
    /* THE BAR STOPS BEING STICKY. Thirteen controls and twenty keys stack to
       roughly three hundred pixels on a phone; pinned to the top of an 812px
       screen that is more than a third of the device given over permanently to
       the controls, with the grid — the thing being controlled — squeezed into
       what is left. It scrolls away here, and the grid gets the screen. */
    .transport { position: static; }
    .transport__in { gap: .38rem .4rem; padding-block: .45rem; }

    .tgrp--dice { flex: 1 1 100%; }
    .dieb { flex: 1 1 auto; justify-content: center; padding-block: .6rem; }
    .tgrp--vals {
        flex: 1 1 100%;
        flex-wrap: wrap;
        justify-content: flex-start;
        gap: .4rem .45rem;
        /* Taller fields, so the halves of a spinner are still worth aiming at:
           at 2.35rem each arrow is 15px, which is a target nobody hits with a
           thumb while a song is running. */
        --tf-h: 2.9rem;
    }
    .tgrp--vals .tf { flex: 1 1 4.8rem; }
    .tf--wide { flex: 1 1 7.5rem; }
    .tf--tiny { flex: 1 1 4.4rem; }
    .tf--find { flex: 1 1 100%; }
    .tgrp--flow { flex: 1 1 100%; gap: .55rem; flex-wrap: wrap; }
    .tf--swing { flex: 1 1 100%; }
    .tf--pats  { flex: 1 1 100%; }

    /* Wider arrows to make up for the shorter halves. */
    .stepr__c button { width: 1.6rem; }
    .tgrp--vals .stepr:has(> input) > .field { padding-right: 1.9rem; }

    /* A lock in a caption cannot be 32px without pushing the caption off the
       row, so it gets width instead of height and a bigger hit area than its
       ink: 24px tall and half a rem of padding either side. */
    .lockb { padding: .3rem .45rem; font-size: .5rem; }
    .lockb svg { width: .68rem; height: .68rem; }

    /* THE KEYS KEEP THEIR SIZE. `1fr` plus aspect-ratio made them 65px squares
       here — two rows of those is 130px of a phone screen for a pattern
       selector. Ten fixed 30px keys and nine 3px gaps is 327px, which fits a
       375px screen with room either side. */
    /* Ten fractions, not ten fixed pixels: aspect-ratio then makes each key
       exactly a tenth of the row, which is 30px on a 375px screen and 26px on a
       320px one — both fine, and neither needs a breakpoint of its own. */
    #t-pats { grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 3px; }
    .patb { min-height: 26px; font-size: .62rem; border-radius: 5px; }

    .transport .btn--play { min-width: 5.4rem; }
    .btn--play, .sqb { min-height: 2.6rem; }
}


/* =============================================================================
   THE PANEL, REWORKED
   ========================================================================== */

/* --------------------------------------------------- no second set of arrows
   A number input draws its OWN spinner, and beside a drawn pair of arrows that
   is four arrows on one field doing two different amounts. The native one goes;
   ours is the one that knows a tempo steps by 1 and not by 0.1. */
/* Unscoped on purpose: prefixed with .transport this rule stops matching the
   moment the field is moved into a wrapper that is not a descendant of it, and
   the native spinner walks straight back in beside ours. The page has no other
   number input that wants one. */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none; appearance: none; margin: 0;
}
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }

/* ------------------------------------------------------------- the symmetry
   ONE FIXED LEFT COLUMN, BOTH ROWS. The dice and the transport on top, Find
   underneath, and both exactly this wide — so the two panels start at the same
   x and the bar reads as one block. Before this, Find was 12rem against a
   340px group above it and left a hole the width of a hand. */
.transport__in { --t-left: 21.5rem; }
.tgrp--head { flex: 0 0 var(--t-left); display: flex; align-items: center; gap: .35rem; }
.tf--find   { flex: 0 0 var(--t-left); }

.tgrp--feel { flex: 1 1 auto; }
.tgrp--feel .tf { justify-content: flex-end; }

/* The four grouped locks. `flex: 0 0 auto` could not shrink and the row's
   FIXED height meant that when the chips wrapped to a second line they
   painted straight over whatever stood beside them — the field grows with
   its content now, so an overlap is impossible at any width. */
.tf--locks { flex: 0 1 auto; min-width: 0; }
.lockrow { display: flex; flex-wrap: wrap; gap: .25rem; align-items: center; min-height: var(--tf-h); }
.tf--locks .tf__cap { color: var(--ink-55); }

/* --------------------------------------------------- a die on a single field */
.rndb {
    -webkit-appearance: none; appearance: none;
    display: inline-flex; align-items: center; justify-content: center;
    width: .95rem; height: .95rem;
    margin-left: .28rem;
    padding: 0;
    background: none; border: 0;
    color: var(--ink-55);
    cursor: pointer;
    transition: color .16s var(--ease), transform .16s var(--ease);
}
.rndb svg { width: .8rem; height: .8rem; }
.rndb:hover { color: var(--accent); transform: rotate(-18deg); }
.rndb:active { transform: rotate(-18deg) scale(.9); }
.rndb:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The panel's ranges want the same height as its fields so the two panels line
   up row for row. */
.tgrp--feel input[type="range"] { height: var(--tf-h); }
/* Desktop only: the FEEL panel is parked under the song panel for the whole
   session, and a mouse does not need thumb-sized targets — slimmer sliders,
   smaller Feel/Chain keys. The coarse sizes above 760 stay untouched. */
@media (min-width: 761px) and (pointer: fine) {
    .tgrp--feel input[type="range"] { height: 1.7rem; }
    /* Scoped to the FEEL panel: a bare .tinyb--i also hit the lyric row's
       keys and the modal close buttons, which have their own sizes to keep. */
    .tgrp--feel .tinyb--i { width: 1.9rem; height: 1.9rem; }
    .tgrp--feel .tinyb--i svg { width: 13px; height: 13px; }
}
/* The modal closes sit outside every --tf-h scope, where var(--tf-h) is
   invalid and the key collapsed to its text. A stated size, everywhere. */
.lymodal .tinyb--i { width: 2rem; height: 2rem; }

@media (max-width: 760px) {
    .transport__in { --t-left: 100%; }
    .tgrp--head, .tf--find { flex: 1 1 100%; }
    .tgrp--feel { flex: 1 1 100%; }
    .tf--locks { flex: 1 1 100%; }
    .lockrow { height: auto; flex-wrap: wrap; }
    .rndb { width: 1.3rem; height: 1.3rem; }
    .rndb svg { width: 1rem; height: 1rem; }
}


/* =============================================================================
   TWO ROWS, AND THEY FIT
   =============================================================================
   Row one  · the dice, the transport, what a song IS, and Find.
   Row two  · how it FEELS, and the twenty keys.

   The fixed left column is gone: with only two rows there is nothing beneath
   the head to line up with, and reserving 21.5rem for a Find box that needs 12
   was the hole it was meant to close.
   ========================================================================== */
.transport__in { --t-left: auto; align-items: center; }
.tgrp--head { flex: 0 0 auto; }
/* MEASURED TO FIT. Row one is the head (322px) and the SONG panel (683) — a
   Find box wider than about 130px pushes it past the 1165px the measure gives
   this bar and drops it onto a line of its own, which is the third row we are
   trying not to have. */
.tf--find   { flex: 0 1 8.2rem; min-width: 6.6rem; }
.tf--find .field { font-size: .82rem; }
.tf--pats   { flex: 0 0 auto; margin-left: 0; }
.tgrp--feel { flex: 1 1 26rem; }

/* Everything on row two is tightened by the amount it takes to fit beside
   twenty keys: the sliders lose a little width, the locks lose their padding,
   and the keys come down from 26px to 22. Measured against a 1265px measure,
   which is what .wrap gives this bar. */
.tgrp--feel .tf--swing { flex: 0 1 6.4rem; min-width: 4.6rem; }
.tgrp--feel .tf--tiny  { flex: 0 0 auto; }
.tgrp--feel .lockb     { padding: .26rem .34rem; }
.tgrp--feel .lockrow   {
    gap: .25rem;
    display: grid; grid-template-columns: repeat(3, auto);
    justify-content: center;
}
/* Twenty-eight keys since the DJ eight arrived: fourteen across on the wide
   bar (two rows), ten across below 1100 (three rows). */
#t-pats { grid-template-columns: repeat(14, 26px); gap: 3px; }
@media (max-width: 1100px) { #t-pats { grid-template-columns: repeat(10, 26px); } }
.patb   { min-height: 26px; font-size: .58rem; border-radius: 5px; }

/* The pattern block carries its own caption, and on this row it reads better
   above the keys than beside them. */
.tf--pats .tf__cap { margin-bottom: .1rem; }

@media (max-width: 1100px) {
    /* Below this the keys and the feel panel stop fitting on one line, and a
       third row is the honest answer — better than shrinking a key to
       something nobody can hit. */
    .tf--pats { flex: 1 1 100%; margin-top: .2rem; }
    #t-pats { grid-template-columns: repeat(20, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
    .tf--find { flex: 1 1 100%; }
    #t-pats { grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 3px; }
    .patb { min-height: 26px; }
}


/* ------------------------------------------------------- row one, with room
   Measured on the last reliable layout pass: head 322 + SONG panel 683 + Find
   176 + three gaps = 1208 against the 1165 the measure gives this bar, and
   Find dropped to a line of its own. Find came down to ~131, which lands it at
   1163 — inside, but by two pixels, and a two-pixel margin is a layout that
   breaks on the next font tweak.

   So the two widest fields in the panel give up a little as well, and the panel
   is allowed to SHRINK rather than shove: if anything still does not fit, the
   scale and style boxes narrow by a few characters instead of the search box
   wrapping and taking a whole row with it. */
.tgrp--vals { flex: 0 1 auto; min-width: 0; }
.tgrp--vals .field--mini { min-width: 0; }

/* THE TWO FIELDS WHOSE VALUE IS A WORD — and the rule that was actually in
   charge of them.

   At 7.2rem the two longest values were cut to "Natural mi" and "EBM / bod". A
   control whose value you cannot read is a control you have to open in order to
   find out what it says, which is the one thing a closed select is for.

   THE FIRST FIX WENT IN THE WRONG PLACE. `.tf--wide` further up is (0,1,0) and
   this line is (0,2,0), so editing the short one changed nothing at all — the
   same specificity trap the pattern keys hit when `html[data-dark] .patb` beat
   every state rule. If a width will not move, find the rule that is winning
   before writing a bigger number into the one that is losing.

   MEASURED, not guessed. In DM Sans, "Phrygian dominant" is 142px at 16px and
   134px at 15.04px; "Open (anything)" is 123 and 116. These selects are
   `appearance: none` with no background image, so a field's room is simply its
   client width less its own padding — there is no chevron to leave space for,
   and assuming one is what made the first two attempts land short.

   BOTH HALVES WERE NEEDED. The boxes grew to 13.6rem and 12.4rem AND the type
   came down a step: either alone still clipped, because this row is at its
   limit and the panel shrinks its fields to fit whatever is left over.

   AND THE SMALL FIELDS NEEDED A FLOOR. `.tgrp--vals .field--mini` sets
   `min-width: 0` — that is what lets a field shrink below its own content, and
   it is why widening two fields silently squeezed Steps to 31px and started
   clipping "64". Everything that is not one of the two word fields now has
   2.7rem it cannot be pushed under, which holds "G#" and "64" with room to
   spare. Widen one field in a saturated flex row and something else pays.

   Verified at wrap widths 1153, 1040, 960, 880, 800, 700 and 620: the longest
   option in EVERY field of the panel fits at every one, nothing overflows, and
   the bar is 303px — SHORTER than the 372 it was before, because a panel that
   is not being squeezed wraps more cleanly than one that is.

   The smaller type is behind `pointer: fine` on purpose. The 16px floor above
   exists because iOS Safari zooms the page on a field under it and carries
   every fixed element off the screen; this rule is (0,3,0) and would have beaten
   that floor on touch, where the row wraps and has the width anyway. */
/* EVERY FIELD GROWS, THE TWO WORD FIELDS GROW MOST.

   Giving the row's slack to Scale and Style alone made them 380px boxes holding
   a 134px word while the numbers beside them stayed at their minimum — the
   ragged edge moved inside the panel instead of going away. So every field has
   a grow factor and the two word fields have twice one: the panel reaches the
   edge, the growth is shared, and the fields that hold sentences still take the
   largest share of it. Measured at 1440: 135 · 128 · 137 · 333 · 113 · 294. */
.tgrp--vals .tf { flex: 1 1 auto; }
.tgrp--vals .tf--wide  { flex: 2 1 12.4rem; min-width: 7rem; }
.tgrp--vals .tf--scale { flex: 2.4 1 13.6rem; }
.tgrp--vals .stepr { width: 100%; }
.transport .tgrp--vals .field--num { width: 100%; }
.tgrp--vals .tf:not(.tf--wide) .field--mini { min-width: 2.7rem; }
@media (pointer: fine) {
    .tgrp--vals .tf--wide .field--mini { font-size: .94rem; padding-inline: .42rem; }
}

/* =============================================================================
   THE PANEL IS ONE PIECE, AND EVERY ROW REACHES BOTH EDGES

   Each row used to stop where its content ran out, which left a ragged right
   margin down the whole bar — three rows ending in three different places reads
   as a pile of controls rather than an instrument.

   Each row is `1 1 100%` so it owns its line, and the SLACK IS GIVEN AWAY
   DELIBERATELY rather than left at the end:

     · the SONG row hands it to Scale and Style, the only two fields whose value
       is a word and the only two that were being cut short (`1 1` above, so
       they grow into whatever the numbers and note names do not need)
     · the FIND row was already a three-column grid and already reached

   Nothing is stretched that should not be: a square key pulled to fill a gap is
   a target that moves between window widths.

   THE FEEL ROW IS NOT TOUCHED, because it was already right: `.tgrp--feel
   { flex: 1 1 26rem }` with `.tf--pats { margin-left: 0 }` further up fills
   that row from the left — the panel grows into the gap, the twenty keys sit at
   the far end, and on a narrow window where the keys wrap they start at the
   left margin like everything else. Adding `margin-left: auto` to them pinned
   them right on their own line and opened 507px of white on the LEFT instead:
   the same ragged edge, mirrored. Fixing a row that is already correct is how
   you break it. */
.tgrp--head { flex: 1 1 100%; }
.tgrp--vals:not(.tgrp--feel) { flex: 1 1 100%; }

/* THE FRAME. One hairline around the whole control panel, so it reads as a
   single object rather than as loose rows floating under the header — the two
   inner panels already carry this exact border and radius, and this is the same
   drawing one level out. Light, because it is a container and not a control:
   `--line-soft` where they use `--line`. */
.transport__in {
    border: 1px solid var(--line-soft);
    border-radius: 14px;
    padding: .6rem .7rem;
    background: var(--tint-soft, transparent);
}

/* AND IT IS INSET FROM THE MEASURE. A framed panel that runs exactly as wide as
   the prose under it reads as a band the page happens to have, not as an object
   sitting on the page — the frame needs air on both sides to be a frame at all.
   93% of the measure, centred: 3.5% a side, which is the middle of the 5–9%
   this was asked to lose and lands the panel's edge just inside the grid's. */
.transport .transport__in { width: 93%; }
html[data-dark] .transport__in { border-color: rgba(255, 255, 255, .1); }

/* =============================================================================
   ROW THREE — FIND, AND THE TWO SCREENS IN MINIATURE
   =============================================================================
   Three equal columns. Find had been measured to FIT row one rather than to be
   used — 8.2rem, chosen because 176px pushed the row over the measure — and it
   has a third of the bar now, which is the width a search box wants.

   THE MINIS ARE THE SAME TWO SCREENS. Each shares its mode with the full-size
   screen it is lettered for, in both directions: press the caption here and the
   big one changes with it, press a mode button down there and this caption
   follows. They are 2.35rem tall, the height of a field in the panel above, so
   the row reads as one strip rather than as a bar with two pictures stuck to
   it — and `align-items: end` is what puts Find's FIELD on the same line as
   them, since Find carries a caption above it and they do not.

   Why they earn the space: the screens are two thousand pixels down the page,
   and the moment you want to see what the sound is doing is while your hand is
   on the dial that is changing it.
   ========================================================================== */
.tstrip {
    flex: 1 1 100%;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: .5rem;
    align-items: end;
}
/* In a grid the flex-basis above means nothing; what it still needs is
   permission to be narrower than its content. */
.tstrip .tf--find { flex: initial; min-width: 0; }

.tsc {
    display: flex; align-items: center; gap: .35rem;
    height: 2.35rem; min-width: 0;
    padding: 0 .25rem 0 .4rem;
    border: 1px solid var(--line); border-radius: 8px;
    background: var(--paper);
    overflow: hidden;
}
/* The caption IS the switch. A row this short has no room for eight mode
   buttons, and a screen with no label is a moving picture nobody can name. */
.tsc__b {
    -webkit-appearance: none; appearance: none;
    display: flex; align-items: baseline; gap: .32rem;
    /* A STATED WIDTH, not `auto`. The caption is the mode name and the mode
       names are not the same length — with an auto caption, screen A reading
       OSCILLOSCOPE had a canvas 24px narrower than screen B reading SPECTRUM,
       in two cells that are exactly the same size. 6.6rem is OSCILLOSCOPE, the
       longest of the eight, set at this size. */
    flex: 0 0 6.6rem; min-width: 0; overflow: hidden;
    padding: 0; background: none; border: 0;
    font: inherit; text-align: left; cursor: pointer;
    color: var(--ink-55);
    transition: color .16s var(--ease);
}
.tsc__b:hover { color: var(--accent); }
.tsc__b:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tsc__b b {
    font-size: .58rem; font-weight: 700; letter-spacing: .12em;
    color: var(--accent);
}
.tsc__n {
    font-size: .55rem; font-weight: 500; letter-spacing: .12em; text-transform: uppercase;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The canvas takes whatever the caption leaves. min-width: 0 is load-bearing
   in a flex row — without it a canvas refuses to go below its own width and
   pushes the caption out of the box. */
.tsc__c {
    flex: 1 1 auto; min-width: 0;
    display: block; width: 100%; height: 100%;
    cursor: pointer;
}

/* =============================================================================
   THE TOUCH RULES, RE-ASSERTED LAST — AND WHY THEY HAVE TO BE

   Everything above this point is the DESKTOP refinement of the transport, and
   it was written after the phone rules. That ordering is the bug: three of its
   rules are more specific than the phone rules they contradict, so they won on
   a phone as well, and each one undid a fix that is documented above as done.

   * `.tf--find .field { font-size: .82rem }` is (0,2,0) and beats the coarse-
     pointer floor `.field { font-size: 16px }` at (0,1,0). It rendered the
     Find box at 13.12px — and under 16px iOS Safari ZOOMS THE PAGE on focus,
     which carries every fixed element off the side of the screen. That is the
     exact failure the floor exists to prevent, reintroduced by four fifths of
     a pixel's worth of design.
   * `.tgrp--feel .tf--swing { flex: 0 1 6.4rem }` beats `.tf--swing { flex:
     1 1 100% }` for the same reason, so the four FEEL sliders stayed 102px
     wide on a 375px screen: four faders, each a thumb and a half long.
   * `#t-pats { repeat(10, 22px) }` ties the phone rule on specificity and
     wins on order, so the pattern keys stayed 22px instead of the 30 the
     phone rule was written to give them.

   The lesson is the one the grid track note makes elsewhere in this file: a
   later unscoped rule defeats an earlier scoped one no matter how carefully
   the earlier one was reasoned about. So the touch rules go LAST, and they say
   so, and anything added below them has to think about phones or move up.
   ========================================================================== */
@media (pointer: coarse) {
    /* 16px, and it is not a design preference — it is the threshold below
       which iOS Safari zooms. */
    .transport .tf--find .field,
    .transport .field--num,
    .transport .field--mini { font-size: 16px; }

    /* A die on a single field: 20px of ink is right, 20px of TARGET is not.
       The ink stays and the box grows around it.

       `flex: 0 0 auto` is load-bearing. These sit in flex captions, and a flex
       item's default `min-width: auto` does not stop it being SHRUNK below the
       width you gave it when the row is tight — the key's die measured 16px
       wide against 32 tall, a target half the size it declares, and only on the
       one row that happened to be full. A width in a flex row is a suggestion
       until you say it is not. */
    .transport .rndb { flex: 0 0 auto; width: 2rem; height: 2rem; margin-left: .15rem; }
    .transport .rndb svg { width: 1rem; height: 1rem; }
    .transport .lockb { min-height: 2rem; padding: .35rem .5rem; }
}

@media (max-width: 760px) {
    /* ---------------------------------------------------- the panel, on a phone
       THE FILE CLUSTER MUST NOT PIN THE ROW. `#t-save { margin-left: auto }`
       gives the head row its flush right edge on a laptop, and on a 375px screen
       it did the opposite: auto margin on a wrapping row holds all nine buttons
       on one line, and the row ran 109px past the frame. It is dropped here and
       the row wraps into two, spread rather than left-packed, so both lines
       reach both edges and the panel stays one rectangle.

       Roll and Play give up their laptop minimum for the same reason: at 5.9rem
       each they forced a third line on their own. */
    .transport .tgrp--head { flex-wrap: wrap; row-gap: .4rem; justify-content: space-between; }
    .transport .tgrp--head #t-save { margin-left: 0; }
    .transport .btn--play,
    .transport .dieb--main { min-width: 0; flex: 1 1 6rem; }

    /* Tighter everywhere it costs nothing: the frame, the panels and the gaps
       between fields. Target sizes are untouched — `--tf-h` and the coarse
       pointer rules above own those, and shrinking a control you hit with a
       thumb is not compactness, it is a miss. */
    .transport__in { padding: .5rem .5rem; gap: .4rem; border-radius: 12px; }
    .transport .tgrp--vals { gap: .35rem .4rem; padding: .3rem .4rem; }

    /* Full width, so a fader is a fader rather than a token. */
    .transport .tgrp--feel .tf--swing { flex: 1 1 100%; min-width: 0; }
    /* Two switches side by side, not two slabs: `1 1` let them grow to half the
       screen each once the faders stopped competing for the row. */
    .transport .tgrp--feel .tf--tiny  { flex: 0 1 7.5rem; }
    .transport .tgrp--feel .lockb     { padding: .35rem .5rem; }
    .transport .tgrp--feel .lockrow   { gap: .3rem; flex-wrap: wrap; }

    /* The four locks take a row of their own and their caption is allowed to
       wrap. Squeezed into a column beside the switches it was four chips in a
       vertical stack under a heading reading "THE DICE MAY NOT TO", with the
       rest of the sentence off the side of the screen. */
    .transport .tgrp--feel .tf--locks { flex: 1 1 100%; }
    .transport .tf--locks .tf__cap { white-space: normal; }
    .transport .tf--pats  .tf__cap { white-space: normal; }

    /* Ten to a row, each a tenth of the width: 30px on a 375px screen, 26 on
       a 320px one. Two rows of ten rather than one row of twenty, because
       twenty keys across a phone is 15px each. */
    .transport #t-pats { grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 4px; }
    .transport .patb { min-height: 30px; font-size: .68rem; }

    /* The transport stops being sticky here: thirteen controls and twenty keys
       stack to about 300px, and pinning a third of an 812px screen to the top
       for the whole session leaves the grid squeezed into what is left. */
    .transport { position: static; }

    /* ROW THREE STACKS. Three equal columns of a 375px screen is 118px each:
       a search box that fits four characters and two screens the size of a
       thumbnail. Find takes the width, the two screens keep each other
       company on the line below, and both stay legible. */
    .transport .tstrip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .transport .tstrip .tf--find { grid-column: 1 / -1; }
}

/* ---------------------------------------------------------------- the shelf
   Twelve cards. minmax(0, 1fr) rather than 1fr on the track, because a 1fr
   track grows to min-content and one long patch name would then set the width
   of every other card in the row. */
/* ============================================================== THE RECENTS
   The row above the twelve. Pills, because forty of anything card-shaped would
   bury the shelf it sits on top of — see the header in js/shelf.js.

   It is separated from the shelf by a rule rather than by space alone: the two
   are the same KIND of thing (songs you can open in one press) and the only
   difference between them is whose choices they are, which is exactly what a
   hairline says and a gap does not.
   ========================================================================== */
.recents {
    margin: 0 0 .95rem;
    padding: 0 0 .85rem;
    border-bottom: 1px solid var(--line);
}
.recents__head {
    display: flex; align-items: baseline; gap: .45rem;
    margin-bottom: .5rem;
}
.recents__cap {
    font-size: .58rem; font-weight: 600;
    letter-spacing: .15em; text-transform: uppercase;
    color: var(--ink-55);
}
.recents__n {
    font-size: .58rem; font-variant-numeric: tabular-nums;
    color: var(--accent); letter-spacing: .06em;
}
.recents__clear {
    -webkit-appearance: none; appearance: none;
    margin-left: auto; padding: 0;
    background: none; border: 0;
    font: inherit; font-size: .58rem; font-weight: 600;
    letter-spacing: .12em; text-transform: uppercase;
    color: var(--ink-55); cursor: pointer;
}
.recents__clear:hover { color: var(--accent); }
.recents__clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* WRAPS, NEVER SCROLLS. A horizontal scroller hides its own contents behind an
   edge with no affordance, and on a page that is already one long column the
   thing forty pills must not do is add a second axis. Three or four wrapped
   rows of these is about 90px — less than one shelf card. */
.recents__row { display: flex; flex-wrap: wrap; gap: .3rem; }

.rpill {
    -webkit-appearance: none; appearance: none;
    display: inline-flex; align-items: center; gap: .3rem;
    max-width: 13.5rem; min-width: 0;
    padding: .22rem .55rem .22rem .4rem;
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: 999px;
    font: inherit; font-size: .72rem; line-height: 1.35;
    color: var(--ink);
    cursor: pointer;
    transition: border-color .16s var(--ease), background .16s var(--ease), color .16s var(--ease);
}
.rpill:hover { border-color: var(--accent); color: var(--accent); }
.rpill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.rpill:active { transform: translateY(1px); }

.rpill__m { display: inline-flex; flex: 0 0 auto; opacity: .6; }
.rpill__m svg { width: 11px; height: 11px; display: block; }
.rpill:hover .rpill__m { opacity: 1; }
/* A link is the one of the three that outlived the session that made it, so it
   is the one the eye should find first. */
.rpill--link .rpill__m { color: var(--accent); opacity: .9; }

/* TRUNCATED IN CSS AND NEVER IN JAVASCRIPT. The full name stays in the DOM and
   in the title, so a browser's find-in-page still lands on it. */
.rpill__n {
    min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rpill__x {
    flex: 0 0 auto;
    padding-left: .34rem;
    border-left: 1px solid var(--line);
    font-size: .6rem; letter-spacing: .04em;
    text-transform: uppercase;
    opacity: .62;
    max-width: 5.4rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rpill:hover .rpill__x { border-left-color: currentColor; }

@media (prefers-reduced-motion: reduce) {
    .rpill { transition: none; }
    .rpill:active { transform: none; }
}

.shelf {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
    gap: .75rem;
}
.shelfc {
    display: flex;
    flex-direction: column;
    gap: .4rem;
    padding: .8rem .85rem;
    border: 1px solid var(--line);
    border-radius: 10px;
    /* --paper, NOT a literal. dark.css redefines --paper and does not define
       --card at all, so `var(--card, #fff)` would have painted twelve white
       cards onto the dark edition — the same whiteout a literal colour in a
       :hover rule causes, and just as invisible from the paper edition. */
    background: var(--paper);
    min-width: 0;
}
.shelfc__top { display: flex; align-items: baseline; gap: .5rem; justify-content: space-between; }
.shelfc__n { font-weight: 600; letter-spacing: -.01em; min-width: 0; }
.shelfc__style {
    font-size: .68rem; text-transform: uppercase; letter-spacing: .08em;
    opacity: .62; white-space: nowrap;
}
.shelfc__note { margin: 0; font-size: .82rem; line-height: 1.45; opacity: .82; }
.shelfc__meta {
    display: flex; gap: .6rem; font-size: .72rem; opacity: .62;
    font-variant-numeric: tabular-nums; margin-top: auto;
}
.shelfc__go { align-self: flex-start; margin-top: .2rem; }

/* =============================================================================
   LYRICS — the sheet and its controls
   -----------------------------------------------------------------------------
   The console itself is the house panel; this styles only what is new: the
   two-column sheet the words are set on, the line the performance is lighting
   right now, and the compact control row. Everything is tokens, so the dark
   edition needs nothing extra.
   ========================================================================== */
/* The console's control row: CELLS, each caption over its control, packed
   left and content-sized — the fader is the one thing with a track to give
   and even it is capped. This is the same contract as the transport row. */
.ly-controls { display: flex; flex-wrap: wrap; gap: .8rem 1.1rem; align-items: flex-end; }
.lyc { display: flex; flex-direction: column; gap: .3rem; }
.lyc > .lbl { font-size: .58rem; }
.lyc--voice select { width: 12.5rem; }
.lyc--vol { flex: 0 1 14rem; min-width: 9rem; }
.lyc--vol input[type="range"] { width: 100%; }
.tbtn--sq { width: 34px; height: 34px; padding: 0; display: inline-flex; align-items: center; justify-content: center; }
.tbtn--sq svg { width: 15px; height: 15px; }
/* Pressed is performing and wears the pause bars; up is the play triangle. */
.vox-play .vox-play__wait { display: none; }
.vox-play[aria-pressed="true"] .vox-play__go { display: none; }
.vox-play[aria-pressed="true"] .vox-play__wait { display: block; }
@media (max-width: 700px) {
    .lyc--voice { flex: 1 1 100%; }
    .lyc--voice select { width: 100%; }
    .lyc--vol { flex: 1 1 100%; }
}

.ly-sheet {
    position: relative;
    max-height: 340px; overflow-y: auto; overscroll-behavior: contain;
    border: 1px solid var(--line); border-radius: 10px;
    padding: .9rem 1rem; background: var(--paper);
    columns: 2; column-gap: 2rem; column-rule: 1px solid var(--line);
}
.ly-sheet:empty { display: none; }
.ly-title {
    column-span: all; display: flex; flex-wrap: wrap; align-items: baseline;
    gap: .6rem; margin: 0 0 .6rem; padding-bottom: .5rem;
    border-bottom: 1px solid var(--line);
}
.ly-title b { font-size: 1.02rem; letter-spacing: .01em; }
.ly-title span { font-size: .72rem; color: var(--ink-55); text-transform: uppercase; letter-spacing: .14em; }
.ly-sec {
    font-size: .62rem; font-weight: 800; letter-spacing: .17em;
    color: var(--ink-55); margin: .7rem 0 .25rem;
    break-after: avoid;
}
.ly-sec:first-of-type { margin-top: 0; }
.ly-line {
    margin: 0 0 .18rem; font-size: .86rem; line-height: 1.45;
    overflow-wrap: anywhere; break-inside: avoid;
    padding-left: .55rem; border-left: 2px solid transparent;
    transition: border-color .25s, color .25s, background .25s;
}
/* SING ALONG: the line being performed. The border is the playhead, the
   ground is the accent as a WASH — the .st--taken idiom, because a solid
   token behind body ink is how dark slabs end up on paper pages. Both
   editions get their own alpha: the dark accent needs more of it to read
   over the deep ground. */
.ly-line.is-now {
    border-left-color: var(--accent);
    background: rgba(194, 65, 12, .07);
    font-weight: 600;
}
html[data-dark] .ly-line.is-now { background: rgba(249, 115, 22, .13); }

@media (max-width: 700px) {
    .ly-sheet { columns: 1; max-height: 300px; }
    .ly-vol { min-width: 0; }
}

/* The mute is a state, so it must LOOK like one when pressed. */
#ly-mute[aria-pressed="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }

/* The rack card's two buttons: open and the link that reproduces the roll. */
.shelfc__row { display: flex; gap: .45rem; align-self: flex-start; margin-top: .2rem; }
.shelfc__row .btn { margin: 0; }

/* A finger is not a cursor: the mix keys keep at least 30px under a coarse
   pointer, including on a wide tablet, where the desktop rule above would
   otherwise hand them 26px. */
@media (pointer: coarse) {
    #t-pats .patb { min-height: 30px; min-width: 30px; }
}
/* Hover states stay behind a hover-capable pointer, or the last tapped key
   keeps its lift on iOS and reads as a second, phantom selection. */
@media (hover: none) {
    #t-pats .patb:hover { transform: none; }
}

/* =============================================================================
   THE LYRIC ROW, AND THE MIX IN ROW ONE
   -----------------------------------------------------------------------------
   The twenty keys moved into the first row's dead air between Stop and the
   file cluster: the block grows, the cluster keeps its margin-left:auto, and
   the row's height is set by the two key-rows. The lyric row sits where the
   mix used to, built entirely from the transport's own idioms — tf, tinyb,
   stepr — so it reads as a native row and not an annex.
   ========================================================================== */
/* The grid is ten 26px columns plus gaps — ~300px it will not shrink below.
   Claim that much or wrap: with less than a keys-width of dead air the block
   takes a full line of its own instead of crushing into a column. */
.tgrp--head .tf--pats { flex: 1 1 336px; min-width: 336px; margin-left: .25rem; align-self: center; }
@media (max-width: 1100px) {
    .tgrp--head .tf--pats { flex: 1 1 100%; min-width: 0; margin-left: 0; }
}

/* The lyric row is a CLUSTER, not a spread: every cell is content-sized and
   the row packs left, so ten controls read as one instrument. Only the two
   value cells carry fixed widths — the voice needs room for CHAMILLIONAIRE
   and the fader needs a track worth dragging; nothing is allowed to grow. */
.tgrp--lyrics { justify-content: flex-start; gap: .5rem .9rem; }
.tgrp--lyrics .tf { flex: 0 0 auto; }
.tf--voice .field--mini { width: 11.5rem; }
.tf--vvol { width: 11rem; }
.tf--vvol input[type="range"] { width: 100%; }
.tgrp--lyrics .seg--pick .seg__b { min-height: 28px; }
@media (max-width: 700px) {
    .tgrp--lyrics { gap: .45rem .6rem; }
    .tf--voice .field--mini { width: 100%; }
    .tf--voice { flex: 1 1 100%; }
    .tf--vvol { flex: 1 1 100%; width: auto; }
}

/* The sing-along drawer: the same sheet the Lyrics console shows, folded out
   under the bar. Kept short — this bar is sticky and every pixel is grid. */
.t-lyrics { flex: 1 1 100%; border-top: 1px solid var(--line-soft); padding-top: .5rem; }
.t-lyrics .ly-sheet--t { max-height: 190px; columns: 2; }
@media (max-width: 700px) { .t-lyrics .ly-sheet--t { max-height: 150px; columns: 1; } }

/* SING ALONG, TO THE WORD. The lit line carries its words as spans; the one
   being performed right now takes the accent underline. */
.ly-w { font-style: normal; }
.ly-line.is-now .ly-w.is-w {
    color: var(--accent);
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-thickness: 2px;
}

/* The serial: the save's own number, shown beside the keep button, and the
   door you can type one into. */
.sv-serial { display: inline-flex; align-items: center; gap: .45rem; flex-wrap: wrap; }
.sv-serial .lbl b { font-variant-numeric: tabular-nums; letter-spacing: .08em; }
.sv-serial .field--mini { width: 7.5rem; text-transform: uppercase; }

/* This page's nav carries seven links, the injected magnifier, the pill and
   the CTA — at the house 1rem gap the brand wraps and the CTA takes a second
   line. A slightly tighter bar keeps every control on one row. */
.nav__in { gap: .7rem; }

/* The tiny live dot beside Studio: the studio is always running, and the dot
   is part of the link, so the signal and the door are the same thing. Still
   under prefers-reduced-motion and the page's own motion switch. */
.nav__live-link { display: inline-flex; align-items: center; gap: .38rem; }
.nav__live {
    width: 6px; height: 6px; border-radius: 50%;
    /* GREEN, the house "available" green — a live lamp, not an accent mark.
       The dark edition takes the brighter cut of the same green. */
    background: var(--free, #15803D); flex: 0 0 auto;
    animation: navLive 2.2s ease-in-out infinite;
}
html[data-dark] .nav__live { background: var(--free, #4ADE80); }
@keyframes navLive { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .nav__live { animation: none; } }
html[data-motion="off"] .nav__live { animation: none; }

/* =============================================================================
   THE SERIAL DESK — futuristic, minimal, exact.
   -----------------------------------------------------------------------------
   One red-framed window of Orbitron digits at the head of the file cluster:
   the loaded roll's number, always current. Click it and the number is on
   your clipboard — the frame answers in green. Type over it and press Enter
   and that roll opens. Two glyphs live INSIDE the frame: clear, and this
   song's own number back.
   ========================================================================== */
@font-face {
    font-family: 'Orbitron';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url('assets/fonts/orbitron-600.woff2') format('woff2');
}
.t-serialbox { position: relative; display: inline-flex; align-items: center; margin-left: 4px; }
/* THE TAPE CLOCK — while the tape rolls the time sits dead centre above the
   serial field, in the field's own Orbitron and the accent red. Absolute
   against the box so no row ever moves; pointer-events none so it can never
   swallow a click meant for the field. */
.t-serialbox__clock {
    position: absolute; left: 0; right: 0;
    bottom: calc(100% + .22rem);
    text-align: center;
    font-family: 'Orbitron', ui-monospace, 'SF Mono', Menlo, monospace;
    font-size: .66rem; font-weight: 600; letter-spacing: .14em;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
    pointer-events: none;
    white-space: nowrap;
}
.t-serialbox__clock[hidden] { display: none; }
.t-serialbox .field--mini {
    width: 10.05rem; height: 30px; padding: 0 3rem 0 .5rem;
    font-family: 'Orbitron', ui-monospace, 'SF Mono', Menlo, monospace;
    font-size: .72rem; font-weight: 600; letter-spacing: .1em;
    text-transform: uppercase; font-variant-numeric: tabular-nums;
    color: var(--accent); border: 1px solid var(--accent); border-radius: 8px;
    background: var(--paper); cursor: pointer;
    transition: color .25s var(--ease), border-color .25s var(--ease);
}
.t-serialbox .field--mini:focus { cursor: text; }
.t-serialbox .field--mini::placeholder { color: var(--accent); opacity: .45; }
/* Copied: the frame answers in the house green, then settles back. */
.t-serialbox.is-copied .field--mini { color: var(--free, #15803D); border-color: var(--free, #15803D); }
html[data-dark] .t-serialbox.is-copied .field--mini { color: var(--free, #4ADE80); border-color: var(--free, #4ADE80); }
/* The three, TINY, inside the frame: clear, this song's number back, go. */
.t-serialbox__b {
    position: absolute; top: 50%; transform: translateY(-50%);
    background: none; border: 0; cursor: pointer; padding: .08rem .12rem;
    font: inherit; font-size: .66rem; line-height: 1;
    /* The frame's own red, quiet until the hand arrives. */
    color: var(--accent); opacity: .7;
    transition: opacity .2s var(--ease);
}
.t-serialbox__b:hover { opacity: 1; }
.t-serialbox__b:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.t-serialbox__x  { right: 2.1rem; font-size: .8rem; }
.t-serialbox__r  { right: 1.2rem; }
.t-serialbox__go { right: .34rem; }
@media (max-width: 1100px) { .t-serialbox .field--mini { width: 9.4rem; } }
@media (pointer: coarse) { .t-serialbox .field--mini { font-size: 16px; width: 11.5rem; } }

/* =============================================================================
   THE TASTE KEYS — four algorithms, four answers.
   Quiet outlines until engaged; engaged, each wears its own colour: E in the
   house green, futurepop purple, EBM red, trance amber. They change the
   dice, so they sit beside the mix — same square language, one always down.
   ========================================================================== */
/* The taste block OWNS the flexible middle of row one and centres its
   eight keys in it — the mix keys sit content-sized to its left, the serial
   desk and the files to its right. */
.tgrp--head .tf--pats { flex: 0 0 auto; min-width: 0; margin-left: .25rem; }
.tf--taste {
    flex: 1 1 auto; align-self: center;
    /* Ten keys, five to a row, ALWAYS: the flex row may not squeeze this
       below the 5-across geometry or the grid folds to 4/4/2. */
    min-width: 14rem;
    display: flex; flex-direction: column; align-items: center;
    margin-left: .4rem;
    /* Boxed: the algorithms are one instrument, and a panel says so —
       the same recessed ground the value groups sit on. */
    background: var(--tint); border: 1px solid var(--line); border-radius: 10px;
    padding: .3rem .42rem .4rem;
}
html[data-dark] .tf--taste { background: var(--deep-soft); }
.tf--taste .tf__cap { align-self: center; }
#t-taste { display: flex; flex-wrap: wrap; justify-content: center; gap: 3px; max-width: 12.9rem; }
@media (max-width: 1100px) { .tf--taste { flex: 1 1 100%; margin-left: 0; } }
.tasteb {
    appearance: none; -webkit-appearance: none;
    min-height: 26px; width: 2.4rem; padding: 0;
    text-align: center;
    background: var(--paper); border: 1px solid var(--line); border-radius: 5px;
    font: inherit; font-size: .55rem; font-weight: 800; letter-spacing: .08em;
    color: var(--ink-55); cursor: pointer;
    transition: color .18s var(--ease), border-color .18s var(--ease), background .18s var(--ease);
    touch-action: manipulation; -webkit-tap-highlight-color: transparent; user-select: none;
}
.tasteb:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* RAW engaged wears the INK — the one "colour" that says no algorithm. */
.tasteb--raw[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.tasteb--e[aria-pressed="true"]   { background: #15803D; border-color: #15803D; color: #fff; }
.tasteb--fpo[aria-pressed="true"] { background: #7C3AED; border-color: #7C3AED; color: #fff; }
.tasteb--ebm[aria-pressed="true"] { background: #DC2626; border-color: #DC2626; color: #fff; }
.tasteb--trn[aria-pressed="true"] { background: #CA8A04; border-color: #CA8A04; color: #fff; }
html[data-dark] .tasteb--e[aria-pressed="true"]   { background: #4ADE80; border-color: #4ADE80; color: #101318; }
html[data-dark] .tasteb--fpo[aria-pressed="true"] { background: #A78BFA; border-color: #A78BFA; color: #101318; }
html[data-dark] .tasteb--ebm[aria-pressed="true"] { background: #F87171; border-color: #F87171; color: #101318; }
html[data-dark] .tasteb--trn[aria-pressed="true"] { background: #FACC15; border-color: #FACC15; color: #101318; }
.tasteb--sp[aria-pressed="true"]  { background: #0EA5E9; border-color: #0EA5E9; color: #fff; }
.tasteb--nw[aria-pressed="true"]  { background: #DB2777; border-color: #DB2777; color: #fff; }
.tasteb--ind[aria-pressed="true"] { background: #52525B; border-color: #52525B; color: #fff; }
.tasteb--dw[aria-pressed="true"]  { background: #3730A3; border-color: #3730A3; color: #fff; }
html[data-dark] .tasteb--sp[aria-pressed="true"]  { background: #7DD3FC; border-color: #7DD3FC; color: #101318; }
html[data-dark] .tasteb--nw[aria-pressed="true"]  { background: #F9A8D4; border-color: #F9A8D4; color: #101318; }
html[data-dark] .tasteb--ind[aria-pressed="true"] { background: #A1A1AA; border-color: #A1A1AA; color: #101318; }
html[data-dark] .tasteb--dw[aria-pressed="true"]  { background: #A5B4FC; border-color: #A5B4FC; color: #101318; }
@media (pointer: coarse) { .tasteb { min-height: 30px; } }

/* =============================================================================
   THE RADIO — twenty tiny station keys, the car-radio contract: press plays
   what a key holds, a long press keeps the roll you are on. Lit when full,
   a numeral when empty; the accent dot is the "something is here".
   ========================================================================== */
/* The .tbreak before this row is a zero-height flex item, so the container's
   row gap is paid TWICE on the way here (row -> gap -> break -> gap -> radio,
   ~13px). The negative margin gives one gap back and then a little more —
   the radio is the transport's last line and reads as part of the panel, not
   a separate section. */
.tf--radio { flex: 1 1 100%; margin-top: -.55rem; }
/* THE RECORD LABEL — the current song and its number, large, in the lyric
   row's spare width. Orbitron, accent, right-set: the panel says what is on
   the platter the way a deck does. */
/* THE RECORD LABEL, STACKED: the song large, its number underneath in its
   own quieter size — and HARD-CONTAINED: the cell hides overflow and both
   lines ellipsize, so nothing can ever run past the panel's edge again. */
.tf--now {
    flex: 1 1 8rem; min-width: 0; align-self: center; margin-left: auto;
    overflow: hidden; text-align: right;
}
.now-name {
    display: block;
    font-family: 'Orbitron', ui-monospace, Menlo, monospace;
    font-weight: 600; font-size: 1.15rem; letter-spacing: .09em; line-height: 1.15;
    color: var(--accent); text-transform: uppercase;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    font-variant-numeric: tabular-nums;
}
.now-no {
    display: block; margin-top: .12rem; font-style: normal;
    font-family: 'Orbitron', ui-monospace, Menlo, monospace;
    font-weight: 600; font-size: .58rem; letter-spacing: .22em;
    color: var(--ink-55); text-transform: uppercase;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    font-variant-numeric: tabular-nums;
}
#t-radio { display: flex; flex-wrap: wrap; gap: 3px; }
.radiob {
    appearance: none; -webkit-appearance: none;
    width: 24px; height: 22px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--paper); border: 1px solid var(--line); border-radius: 6px;
    font: inherit; font-size: .52rem; font-weight: 800;
    font-variant-numeric: tabular-nums; color: var(--ink-55);
    cursor: pointer; user-select: none; -webkit-user-select: none;
    touch-action: manipulation; -webkit-tap-highlight-color: transparent;
    transition: color .16s var(--ease), border-color .16s var(--ease),
                background .16s var(--ease), transform .16s var(--ease);
}
.radiob:hover { color: var(--ink); border-color: var(--ink-55); }
.radiob:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.radiob.is-kept { border-color: var(--accent); color: var(--accent); }
.radiob.is-arming { transform: scale(.88); background: var(--accent); border-color: var(--accent); color: #fff; }
html[data-dark] .radiob.is-arming { color: #101318; }
@media (pointer: coarse) { .radiob { width: 30px; height: 28px; } }

/* =============================================================================
   THE SCREEN — a hardware readout in the feel row's spare width. Always the
   deep ground, both editions: an LCD is an object, not a surface. Line one
   is what is SOUNDING — bar·beat, tempo, key, style, algorithm, mix — and
   line two is the last thing that happened, in Orbitron like the serial.
   ========================================================================== */
/* The screen grows with the row's spare width, and a long message used to
   let it take as much of the bar as it liked — the cap holds it to a
   readable phrase and the ellipsis does the rest. */
.tf--screen { flex: 1 1 15rem; min-width: 11rem; max-width: 22rem; align-self: stretch; }
/* Tokens, not literals: on paper the screen is a paper instrument — accent
   ink on the recessed ground — and on the moon it sits on the deep, both
   straight from the sheet's own palette. */
.lcd {
    height: 100%; min-height: 44px;
    display: flex; flex-direction: column; justify-content: center; gap: .18rem;
    background: var(--tint); border: 1px solid var(--line); border-radius: 9px;
    padding: .35rem .65rem; overflow: hidden;
}
html[data-dark] .lcd { background: var(--deep-soft); }
.lcd b {
    font-family: 'Orbitron', ui-monospace, Menlo, monospace;
    font-weight: 600; font-size: .58rem; letter-spacing: .1em;
    color: var(--accent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    font-variant-numeric: tabular-nums;
}
.lcd span {
    font-family: 'Orbitron', ui-monospace, Menlo, monospace;
    font-weight: 600; font-size: .5rem; letter-spacing: .09em;
    color: var(--ink-55); text-transform: uppercase;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
@media (max-width: 760px) { .tf--screen { flex: 1 1 100%; max-width: none; } }

/* The top row's readouts speak the screen's language: the engaged mix and
   the engaged algorithm are LIVE VALUES, so they wear the same Orbitron and
   the same accent as the screen — tokens, so paper and moon each ink their
   own. */
.tf--pats .tf__cap b,
.tf--taste .tf__cap b {
    font-family: 'Orbitron', ui-monospace, Menlo, monospace;
    font-weight: 600; letter-spacing: .09em; color: var(--accent);
    font-variant-numeric: tabular-nums; text-transform: uppercase;
}

/* =============================================================================
   THE TINY EQ — four dancing bars beside the lockup, alive only while the
   transport runs. The width is RESERVED whether or not it shows, so nothing
   in the bar ever shifts; the bars idle flat and invisible, and the screen's
   own tick flips .is-live.
   ========================================================================== */
.nav-eq {
    display: inline-flex; align-items: flex-end; gap: 2px;
    width: 18px; height: 12px; margin-left: .28rem; margin-right: .06rem; flex: 0 0 auto;
    opacity: 0; transition: opacity .25s var(--ease);
}
.nav-eq b {
    width: 3px; height: 25%; border-radius: 1px;
    background: var(--accent);
    animation: navEq 0.92s ease-in-out infinite;
    animation-play-state: paused;
}
.nav-eq b:nth-child(2) { animation-delay: -.24s; animation-duration: .78s; }
.nav-eq b:nth-child(3) { animation-delay: -.48s; animation-duration: 1.04s; }
.nav-eq b:nth-child(4) { animation-delay: -.12s; animation-duration: .86s; }
.nav-eq.is-live { opacity: 1; }
.nav-eq.is-live b { animation-play-state: running; }
@keyframes navEq {
    0%, 100% { height: 25%; }
    35%      { height: 100%; }
    65%      { height: 45%; }
}
@media (prefers-reduced-motion: reduce) { .nav-eq b { animation: none; height: 60%; } }
html[data-motion="off"] .nav-eq b { animation: none; height: 60%; }

/* The lockup holds ONE line, tight against its chip and EQ, and below a
   laptop's width the sentence yields entirely so Roll song never wraps. */
.nav__mark span { white-space: nowrap; }
.nav__mark, .nav__mark a { gap: .45rem; }
/* The build chip's sitewide tuck is calc(.34rem − .8rem) — arithmetic
   against the house .8rem mark gap. This bar tightens that gap to .45rem,
   which left the chip PRESSED against the wordmark with no air at all. Same
   arithmetic, this bar's own gap: an even .34rem step like every other page. */
.nav .nav__mark .labs-ver { margin-left: calc(.34rem - .45rem); }
.nav__mark span { padding-left: .45rem; }
@media (max-width: 1240px) { .nav__mark span { display: none; } }

/* The screen's fixed cells: the clock and the tempo own constant widths, so
   digit swaps happen inside them and the line never shifts. */
#t-lcd-live { display: flex; align-items: baseline; gap: 0; }
.lcd__slot { flex: 0 0 auto; overflow: hidden; white-space: nowrap; }
.lcd__slot--bar  { width: 3.9rem; }
.lcd__slot--bpm  { width: 4.8rem; }
.lcd__slot--rest { flex: 1 1 auto; min-width: 0; text-overflow: ellipsis; }

/* The history pair: two slivers stacked beside Stop — back a roll, forward
   a roll. Quiet until they have somewhere to go. */
.t-hist { display: inline-flex; flex-direction: column; gap: 2px; align-self: center; }
.t-hist__b {
    appearance: none; -webkit-appearance: none;
    width: 20px; height: 15px; padding: 0; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--paper); border: 1px solid var(--line); border-radius: 5px;
    font: inherit; font-size: .7rem; color: var(--ink-55); cursor: pointer;
    transition: color .16s var(--ease), border-color .16s var(--ease);
    touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.t-hist__b:hover { color: var(--ink); border-color: var(--ink-55); }
.t-hist__b[disabled] { opacity: .35; cursor: default; }

/* The DICE key — not a tenth algorithm, a MODE: while lit, every roll
   draws its own. Wears the accent, the colour no algorithm owns. */
.tasteb--dice svg { display: block; margin: 0 auto; }
.tasteb--dice[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }
html[data-dark] .tasteb--dice[aria-pressed="true"] {
    /* Spelled out at full weight like every coloured key — the dark sheet
       carries a generic pressed-tasteb rule that outguns the short form. */
    background: var(--accent); border-color: var(--accent); color: #101318;
}

/* THE LYRICS WINDOW — the sing-along sheet at reading scale. */
.lymodal {
    position: fixed; inset: 0; z-index: 240;
    display: flex; align-items: center; justify-content: center;
    padding: 1.2rem;
    background: rgba(15, 17, 21, .45);
    -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
}
.lymodal[hidden] { display: none; }
.lymodal__card {
    width: min(34rem, 100%); max-height: min(78vh, 40rem);
    display: flex; flex-direction: column;
    background: var(--paper); border: 1px solid var(--line); border-radius: 14px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, .28);
    overflow: hidden;
}
.lymodal__head {
    display: flex; align-items: center; justify-content: space-between; gap: .6rem;
    padding: .55rem .8rem; border-bottom: 1px solid var(--line); background: var(--tint);
}
.lymodal__t {
    font-size: .78rem; font-weight: 800; letter-spacing: .14em; text-transform: uppercase;
    color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lymodal__acts { display: inline-flex; gap: .3rem; flex: none; }

/* ------------------------------------------------------- the keyboard map --
   Keycaps drawn with the house tokens; the two note rows are laid out as the
   physical rows they are, staggered like the keyboard itself. */
.kbm { overflow-y: auto; padding: .9rem 1.1rem 1.2rem; display: grid; gap: 1rem; }
.kbm h3 {
    font-size: .62rem; font-weight: 700; letter-spacing: .14em;
    text-transform: uppercase; color: var(--ink-55); margin: 0 0 .45rem;
}
.kbm p { margin: 0 0 .5rem; font-size: .78rem; color: var(--ink-55); line-height: 1.5; }
.kbm dl { margin: 0; display: grid; gap: .34rem; }
.kbm dl > div { display: flex; align-items: baseline; gap: .6rem; }
.kbm dt { flex: 0 0 8.2rem; text-align: right; }
.kbm dd { margin: 0; font-size: .78rem; color: var(--text, var(--ink)); }
.kbm kbd {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 1.35rem; height: 1.35rem; padding: 0 .3rem;
    font: 600 .62rem/1 ui-monospace, Menlo, monospace;
    color: var(--ink); background: var(--tint);
    border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px;
}
html[data-dark] .kbm kbd { background: var(--deep-soft); }
.kbm__rows { display: grid; gap: .3rem; margin: .2rem 0 .5rem; }
.kbm__row { display: flex; flex-wrap: wrap; gap: .22rem; }
.kbm__row + .kbm__row { padding-left: .7rem; }
.kbm__note { font-size: .72rem; }

/* The modal sheet must stay ITS OWN scroller (highlight() scrolls by
   offsetTop) — position relative, internal overflow, nothing else moves. */
.ly-sheet--m {
    position: relative; flex: 1 1 auto; overflow: auto; max-height: none;
    padding: .9rem 1.1rem 1.2rem; font-size: 1rem; line-height: 1.6;
}

/* =============================================================================
   LABS234 — THE DJ, THE MAIN STACK, THE RECORD KEYS, AND THE PHONE HELD
   SIDEWAYS. At the end of the file on purpose: several of these exist to win
   the cascade's argument, and the end is where the last word is said.
   ========================================================================== */

/* Roll and Play, with their memory in a sliver row underneath: the history
   arrows and the four-pip die, out of the first-order row at last. */
.t-mainstack { display: inline-flex; flex-direction: column; gap: 3px; align-self: center; }
.t-mainrow   { display: inline-flex; align-items: center; gap: .35rem; }
.t-underrow  { display: inline-flex; align-items: center; gap: 4px; justify-content: center; }
.t-underrow .t-hist__b { width: 26px; }
.t-hist__b--four svg { display: block; }

/* THE DJ — beside Stop, where the four-pip die sat. Green is the running
   colour everywhere in the house (--free), so green is what "on" is; the
   pulse itself is driven by js/dj.js from the audible clock, not by CSS,
   because CSS cannot know the tempo. */
.djb {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    height: 2.25rem; min-width: 3.2rem; padding: 0 .6rem;
    background: var(--paper); border: 1px solid var(--line); border-radius: 9px;
    color: var(--ink); cursor: pointer;
    font-family: inherit; font-size: .74rem; font-weight: 800; letter-spacing: .09em;
    transition: color .16s var(--ease), border-color .16s var(--ease),
                background-color .16s var(--ease);
    touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}
.djb:hover { border-color: var(--ink-55, currentColor); }
.djb.is-on {
    background: var(--free, #15803D); border-color: var(--free, #15803D);
    color: #FFFFFF;
}
html[data-dark] .djb.is-on {
    background: var(--free, #4ADE80); border-color: var(--free, #4ADE80);
    color: #0C1116;
}
/* The bar it is in, worn as a tiny corner count. */
.djb__bar {
    position: absolute; top: -6px; right: -7px;
    min-width: 15px; height: 15px; padding: 0 3px;
    border-radius: 8px; text-align: center;
    background: var(--ink); color: var(--paper);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 9px; font-weight: 700; line-height: 15px; letter-spacing: 0;
    box-shadow: 0 0 0 2px var(--paper);
}
.djb.is-on .djb__bar { background: var(--free, #15803D); color: #fff; }
html[data-dark] .djb.is-on .djb__bar { background: var(--free, #4ADE80); color: #0C1116; }
.djb__bar[hidden] { display: none; }

/* WHAT THE RECORD BUTTON WRITES — three tiny keys under the serial field. */
.t-serialbox { flex-wrap: wrap; }
.t-recopts {
    flex: 1 1 100%; display: flex; align-items: center; gap: .6rem;
    justify-content: flex-end; padding-top: 3px;
}
.recopt {
    display: inline-flex; align-items: center; gap: 3px; cursor: pointer;
    font-size: .56rem; font-weight: 700; letter-spacing: .07em;
    color: var(--ink-55, #777); user-select: none;
}
.recopt input {
    width: 11px; height: 11px; margin: 0; accent-color: var(--accent);
    cursor: pointer;
}
.recopt:hover { color: var(--ink); }
.recopt input:checked + span { color: var(--accent); }

/* -----------------------------------------------------------------------------
   THE PHONE HELD SIDEWAYS. Every phone accommodation above keys off width
   ≤760, and a landscape phone is 800-930px across — it missed all of them
   and got the desktop bar: sticky, 300px tall, most of the viewport. The
   file's own TOUCH preamble states the principle; this block finally follows
   it: a COARSE pointer on a SHORT viewport is a phone, whatever the width.
   -------------------------------------------------------------------------- */
@media (pointer: coarse) and (max-height: 520px) {
    .transport { position: static; }
    .transport .transport__in { width: 100%; }
    .transport .tgrp--head {
        flex-wrap: wrap; row-gap: .4rem; justify-content: flex-start;
    }
    .transport .tgrp--head .tf--pats { flex: 1 1 100%; min-width: 0; margin-left: 0; }
    .transport #t-pats { grid-template-columns: repeat(14, minmax(0, 1fr)); gap: 3px; }
    /* The coarse-pointer 30px MINIMUM WIDTH on the keys is what forced the
       key row past the frame's edge out here; height keeps the touch target,
       width learns to share. */
    .transport #t-pats .patb { min-width: 0; }
    .transport .tf--taste { flex: 1 1 auto; }
    .transport .tgrp--vals { flex-wrap: wrap; }
    .transport .t-files { margin-left: 0; }
    .transport .t-serialbox { margin-left: 0; }
}

/* -----------------------------------------------------------------------------
   PORTRAIT, RE-SEATED. space-between on a wrapping row put each line's
   leftovers at opposite edges — the "spread apart" feel; and one fader per
   row made the feel panel a tower. Left-packed lines with even gaps, the
   faders two to a row, the tiny keys snug beside them.
   -------------------------------------------------------------------------- */
@media (max-width: 760px) {
    .transport .tgrp--head { justify-content: flex-start; gap: .4rem .45rem; }
    .transport .t-mainstack { flex: 1 1 100%; }
    .transport .t-mainrow { width: 100%; }
    .transport .t-underrow { width: 100%; }
    .transport .djb { flex: 1 1 auto; min-height: 2.6rem; }
    .transport .t-files { margin-left: 0; }
    .transport .tgrp--feel { gap: .35rem .5rem; }
    .transport .tgrp--feel .tf--swing { flex: 1 1 42%; min-width: 8.2rem; }
    .transport .tgrp--feel .tf--tiny { flex: 0 1 auto; min-width: 0; }
}

/* THE CABINET — instrument keys above the memory row, at the memory's own
   scale. Lit means the song is using it; filled means the selected track is.
   currentColor in the shadow so the glow is the accent of whichever edition
   is on. */
.tf--cab #t-cab { display: flex; flex-wrap: wrap; gap: 3px; }
.cabb.is-in  { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 7px currentColor; }
.cabb.is-now { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: 0 0 8px var(--accent); }
html[data-dark] .cabb.is-now { color: #101318; }
.cabb--r { border-style: dashed; color: var(--ink); }

/* =============================================================================
   LABS235 — THE SLIVER DJ, THE FAMILY CABINET, THE ENVELOPES, THE PILLS,
   THE FOLD, AND A COMPACT PHONE. Later than the labs234 block on purpose:
   several of these rules exist to overrule it.
   ========================================================================== */

/* The DJ, at sliver scale in the under-row — same key, smaller seat. */
.t-underrow .djb {
    height: 20px; min-width: 46px; padding: 0 10px;
    font-size: .56rem; letter-spacing: .1em; border-radius: 7px;
}
.t-underrow .djb .djb__bar { top: -7px; right: -6px; }
.t-underrow .t-hist__b { color: var(--ink); font-size: .8rem; width: 26px; height: 20px; }
.t-underrow .t-hist__b[disabled] { opacity: .45; }
#t-stop[hidden] { display: none !important; }

/* THE FAMILIES. Each cabinet key carries its family's hue as --cabc and
   wears it ONLY when lit — an idle cabinet stays as quiet as the memory
   row under it. The gap before each family is the filing system. */
.cabb { --cabc: 210; }
.cabb.grp-a { margin-left: 9px; }
.cabb.is-in {
    color: hsl(var(--cabc) 62% 34%);
    border-color: hsl(var(--cabc) 52% 46%);
    box-shadow: 0 0 7px hsl(var(--cabc) 70% 46% / .45);
}
.cabb.is-now {
    background: hsl(var(--cabc) 60% 38%);
    border-color: hsl(var(--cabc) 60% 38%);
    color: #fff;
    box-shadow: 0 0 8px hsl(var(--cabc) 70% 46% / .6);
}
html[data-dark] .cabb.is-in {
    color: hsl(var(--cabc) 85% 68%);
    border-color: hsl(var(--cabc) 70% 58%);
}
html[data-dark] .cabb.is-now {
    background: hsl(var(--cabc) 75% 64%);
    border-color: hsl(var(--cabc) 75% 64%);
    color: #101318;
}
.tf--cab { margin-bottom: .45rem; }

/* WHAT THE RECORD BUTTON WRITES, as pills. The native boxes read as a form;
   these read as switches, which is what they are. */
.t-recopts { justify-content: flex-start; gap: .35rem; }
.recopt {
    position: relative; border: 1px solid var(--line); border-radius: 999px;
    padding: 3px 10px; font-size: .55rem;
}
.recopt input {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; margin: 0; cursor: pointer;
}
.recopt input:checked + span { color: inherit; }
.recopt:has(input:checked) { background: var(--accent); border-color: var(--accent); color: #fff; }
html[data-dark] .recopt:has(input:checked) { color: #101318; }

/* THE ENVELOPES — eight mini faders in Find's old third-column seat. */
.tf--env { min-width: 0; }
.envg { display: flex; flex-direction: column; gap: 4px; }
.envg__row { display: flex; gap: 7px; }
.envg__c { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.envg__c > span {
    font-size: .48rem; font-weight: 700; letter-spacing: .08em;
    color: var(--ink-55);
}
.envg__c input { width: 100%; height: 14px; margin: 0; accent-color: var(--accent); }
.envg__c.is-off { opacity: .3; }

/* FIND, IN THE MENU — the control finder under the menu's own search. */
.menu__search--find { margin-top: 6px; }
.menu__findmark { font-weight: 800; opacity: .5; padding-inline: 4px; }
.menu__search--find .find { position: relative; flex: 1 1 auto; min-width: 0; }
.menu__search--find .find__out {
    position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 6;
    max-height: 40vh; overflow: auto;
}

/* THE FOLD — the chevron in the frame's corner; a strip and back. */
.transport .transport__in { position: relative; }
.tminb {
    position: absolute; top: 5px; right: 7px; z-index: 3;
    width: 22px; height: 17px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--paper); border: 1px solid var(--line); border-radius: 6px;
    color: var(--ink-55); cursor: pointer;
}
.tminb:hover { color: var(--ink); border-color: var(--ink-55); }
.tminb svg { transition: transform .18s var(--ease); transform: rotate(180deg); }
.transport.is-min .tminb svg { transform: rotate(0deg); }
.transport.is-min .transport__in > *:not(.tname):not(.tgrp--head):not(.tminb) { display: none !important; }
.transport.is-min .tgrp--head > *:not(.t-mainstack) { display: none !important; }
.transport.is-min .transport__in { padding-block: .35rem; }
.transport.is-min .tname { margin-bottom: 0; }

/* SUPER COMPACT PHONES — portrait first, then the sideways gate again. */
@media (max-width: 760px) {
    .transport .transport__in { padding: .4rem .45rem; gap: .3rem .35rem; }
    .transport .tgrp--head { gap: .3rem .35rem; }
    .transport .tgrp--vals { --tf-h: 2.5rem; padding: .25rem .35rem; gap: .3rem .35rem; }
    .transport .btn--play, .transport .dieb--main { min-height: 2.35rem; }
    .transport .sqb { min-height: 2.35rem; }
    .transport .t-underrow .djb { min-height: 0; height: 22px; flex: 0 1 auto; }
    .transport #t-pats .patb { min-height: 28px; }
    .transport .tasteb { min-height: 28px; }
    .transport .radiob { width: 26px; height: 24px; }
    .transport .tf--radio { margin-top: -.3rem; }
    .transport .tf--cab { margin-bottom: .2rem; }
    .transport .tf__cap { font-size: .52rem; }
}
@media (pointer: coarse) and (max-height: 520px) {
    .transport .transport__in { padding: .35rem .45rem; gap: .28rem .32rem; }
    .transport .tgrp--vals { --tf-h: 2.4rem; }
    .transport .radiob { width: 26px; height: 24px; }
    .transport #t-pats .patb { min-height: 26px; }
    .transport .tf__cap { font-size: .5rem; }
}

/* THE THREE ON ONE LINE (labs235b). Roll, Play and the DJ share the top
   row at full height; the memory slivers live underneath — and they read
   like controls now, not like watermarks: full ink, a real border, and the
   four-pip die at the same weight as the arrows. */
.t-mainrow .djb { height: 2.25rem; min-width: 3.4rem; font-size: .74rem; }
.t-underrow { justify-content: flex-start; gap: 5px; }
.t-underrow .t-hist__b {
    width: 32px; height: 22px;
    color: var(--ink); border-color: var(--ink-55);
    font-size: .95rem; opacity: .92;
}
.t-underrow .t-hist__b:hover { opacity: 1; border-color: var(--ink); }
.t-underrow .t-hist__b[disabled] { opacity: .4; }

/* =============================================================================
   LABS236 — the serial stack, the three shelves, the spread slivers.
   ========================================================================== */

/* The serial field back to ONE line — labs234's wrap made the box two lines
   tall and the absolute × ↺ ↵ centred against the pair of them, which read
   as buttons fallen out of the field. The record pills live in a stack
   UNDER the box now, not inside it. */
.t-serialbox { flex-wrap: nowrap; }
.t-serialstack { display: inline-flex; flex-direction: column; gap: 3px; align-self: center; }
.t-serialstack .t-recopts { flex: 0 0 auto; padding-top: 0; }

/* The three slivers spread evenly under Roll, Play and the DJ. */
.t-underrow { width: 100%; justify-content: space-between; padding-inline: 2px; }

/* The die sits on the same baseline as R. */
.cabb--r, .cabb--dice { line-height: 0; }
.cabb--dice svg { display: block; }

/* THE THREE SHELVES. Each row is a flex of family titles and keys, and the
   keys share the row's full width — no orphan third row of three. */
#t-cab { display: flex; flex-direction: column; gap: 4px; }
.cab__row { display: flex; align-items: center; gap: 3px; }
.cab__row .cabb { flex: 1 1 0; min-width: 0; width: auto; height: 22px; }
.cab__row .cabb--r, .cab__row .cabb--dice { flex: 0 0 26px; }
.cabt {
    flex: 0 0 auto; padding: 0 4px 0 7px;
    font-size: .44rem; font-weight: 800; letter-spacing: .1em;
    text-transform: uppercase; color: var(--ink-55);
}
.cab__row .cabt:first-child { padding-left: 0; }
.cabb.grp-a { margin-left: 0; }
@media (pointer: coarse) {
    .cab__row .cabb { height: 26px; }
}

/* The envelope caption costs no line: it floats in the top-right corner,
   which is the one empty spot in the grid (each cell's label sits at its
   own left edge). */
.tf--env { position: relative; }
.tf--env .tf__cap { position: absolute; top: -1px; right: 2px; margin: 0; font-size: .46rem; }
.tf--env .envg { margin-top: 2px; }

/* A shelf that cannot hold its keys folds instead of jutting out of the
   frame: below ~900px the three rows wrap internally — the families keep
   their titles, the keys keep a playable size, and the document keeps its
   width. */
@media (max-width: 900px) {
    .cab__row { flex-wrap: wrap; }
    .cab__row .cabb { flex: 1 1 24px; max-width: 44px; }
}

/* =============================================================================
   LABS237 — the under-row as five equal pills, and the screen a stretch.
   ========================================================================== */
.t-underrow { justify-content: space-between; gap: 6px; }
.t-underrow .t-hist__b {
    flex: 1 1 0; min-width: 0; width: auto; height: 24px;
    display: inline-flex; align-items: center; justify-content: center; gap: 4px;
    border-radius: 999px; border-color: var(--line);
    color: var(--ink); font-size: .9rem; opacity: 1;
    transition: background .15s var(--ease), color .15s var(--ease),
                border-color .15s var(--ease), transform .12s var(--ease);
}
.t-underrow .t-hist__b:hover {
    border-color: var(--free, #15803D); color: var(--free, #15803D);
}
/* The press answers in the house's running green — the same word the DJ
   and the live dot speak. */
.t-underrow .t-hist__b:active {
    background: var(--free, #15803D); border-color: var(--free, #15803D);
    color: #fff; transform: scale(.95);
}
html[data-dark] .t-underrow .t-hist__b:active {
    background: var(--free, #4ADE80); border-color: var(--free, #4ADE80);
    color: #101318;
}
.t-underrow .t-hist__b[disabled] { opacity: .4; }
.t-hist__b--fs { font-size: .6rem; font-weight: 800; letter-spacing: .02em; }
.t-hist__b--fs svg { opacity: .65; }
.t-hist__b--fs:active svg, .t-hist__b--fs:hover svg { opacity: 1; }

/* The screen frame runs to the panel's right edge — it was capped at 22rem
   and left a hole beside it, truncating its own readout to buy nothing. */
.tf--screen { max-width: none; }

/* Captions at the envelope's size everywhere in the lyric row — MODE was
   wearing a full line for a four-letter word, and its neighbours with it. */
.tgrp--lyrics .tf__cap { font-size: .46rem; line-height: 1; margin: 0 0 2px; }
.tgrp--lyrics { row-gap: .25rem; }

/* Rap and Sing, out of their box: bare pills standing on the row's own
   ground, feet on the same line as the keys beside them. */
#lyt-mode { background: none; border: 0; padding: 0; box-shadow: none; gap: 4px; align-self: flex-end; }
#lyt-mode .seg__b { border: 1px solid var(--line); border-radius: 999px; }

/* The history arrows say when they are LOADED: an arrow with somewhere to
   go is bold and wears the frame's red; an empty one stays a whisper. The
   press still answers in green. */
.t-underrow .t-hist__b { font-weight: 800; }
#t-back:not([disabled]), #t-forth:not([disabled]) {
    color: var(--accent); border-color: var(--accent); font-size: 1rem;
}

/* =============================================================================
   LABS239 — the locks as a 2-wide, 3-high stack, and an LCD that wraps.
   ========================================================================== */
/* Five padlocks, filed: DRUMS MELODY / VOICES RACK / STYLE. The caption is
   gone — a padlock named DRUMS needs no essay. */
.lockrow, .tgrp--feel .lockrow {
    display: grid; grid-template-columns: repeat(2, minmax(0, auto));
    gap: .22rem .25rem; min-height: 0; align-items: stretch;
}
.lockrow .lockb { justify-content: flex-start; }

/* The screen never truncates its own readout again: the tail wraps onto a
   second row inside the box instead of ellipsing against its neighbour. */
#t-lcd-live { flex-wrap: wrap; row-gap: 1px; }
.lcd__slot--rest {
    white-space: normal; overflow: visible; text-overflow: clip;
    line-height: 1.3; min-width: 8rem;
}
.lcd { height: auto; }

/* The screen, bounded: it wraps now, so it no longer needs the whole row —
   a readout wider than a sentence is a banner, not a screen. */
.tf--screen { max-width: 24rem; }

/* The locks fold to a chip — the panel's own move at padlock scale. */
.tf--locks { position: relative; padding-right: 16px; }
.tminb--locks { position: absolute; top: 0; right: 0; width: 14px; height: 12px; border-radius: 4px; }
.tminb--locks svg { transform: rotate(180deg); }
.tf--locks.is-min .tminb--locks svg { transform: rotate(0deg); }
.tf--locks.is-min .lockrow { display: none; }
.lockmin {
    display: inline-flex; align-items: center; gap: 3px;
    font-size: .52rem; font-weight: 800; letter-spacing: .06em;
    color: var(--ink-55); border: 1px solid var(--line); border-radius: 999px;
    padding: 3px 8px; align-self: center;
}
.lockmin[hidden] { display: none; }

/* Folded locks hand their width to the screen — the length integrates
   itself with what the row can spare. */
.tgrp--feel:has(.tf--locks.is-min) .tf--screen { max-width: 34rem; }

/* Smart height: the padlocks shrink until three of their rows stand no
   taller than the faders beside them, and the screen sizes to its words —
   the row is as short as its tallest necessary member, nothing more. */
.tgrp--feel .lockrow { gap: 2px 3px; }
.tgrp--feel .lockrow .lockb { padding: .12rem .3rem; font-size: .48rem; gap: 2px; }
.tgrp--feel .lockrow .lockb svg { width: 8px; height: 8px; }
.lcd { min-height: 0; }
.tgrp--feel { align-items: center; }

/* Phone: the algorithm keys stand at the left margin like everything else
   in the panel — a centred block in a left-aligned console reads adrift. */
@media (max-width: 760px) {
    .transport .tf--taste,
    .transport .tf--taste .tf__cap { align-items: flex-start; text-align: left; align-self: stretch; }
    .transport #t-taste { justify-content: flex-start; }
}

/* =============================================================================
   LABS240a — the screen to the edge, the minis to the row's height.
   ========================================================================== */
/* The readout runs to the same right edge as every panel above it — the
   24rem cap left a hole beside it; the second-row wrap keeps the height
   honest, so the cap bought nothing. */
.tf--screen { max-width: none; }

/* The two miniature screens stand as tall as the row that holds them: the
   envelopes beside them set the height, the canvases fill it, and viz.js
   reads its size every frame so the picture simply grows. */
.tstrip { align-items: stretch; }
.tstrip .tsc { height: auto; min-height: 2.35rem; align-self: stretch; }
.tstrip .tsc__c { align-self: stretch; height: auto; }

/* The three record pills share the serial field's whole width — three
   buttons matching the furniture above them, not three crumbs under it. */
.t-serialstack .t-recopts { display: flex; width: 100%; gap: 4px; }
.t-serialstack .recopt { flex: 1 1 0; justify-content: center; text-align: center; }

/* The pills inherit the field's own 4px indent, so both edges align. */
.t-serialstack .t-recopts { margin-left: 4px; width: calc(100% - 4px); }

/* The first band after the control panel starts close to it — the house
   band padding is right for prose, twice too generous for the seam between
   the console and its shelf. */
#shelf.band { padding-top: clamp(20px, 2.6vw, 36px); }

/* The page breathes at half the site's default: two seams read as "too
   much gap" in a row, and a studio wants its sections at arm's length,
   not across the room. One rule instead of a fix per seam. */
.band { padding-block: clamp(30px, 4vw, 56px); }
.band--tight { padding-block: clamp(22px, 3vw, 40px); }

/* THE CONTACT FORM — the house contract, dressed for this page. */
.cform { display: flex; flex-direction: column; gap: .6rem; max-width: 40rem; }
.cform__row { display: flex; gap: .6rem; }
.cform__row .field { flex: 1 1 0; min-width: 0; }
.cform textarea.field { min-height: 6.5rem; resize: vertical; }
.cform .btn { align-self: flex-start; }
.form__note { font-size: .74rem; color: var(--ink-55); margin: 0; }
@media (max-width: 560px) { .cform__row { flex-direction: column; } }

/* ROLL stands centred in its own button — min-width was parking the label
   left and banking the slack on the right. */
.dieb { justify-content: center; }

/* The minis, re-measured: freeing the canvas let its intrinsic 150px drive
   the whole strip sky-high. Pinned to the envelope side's own height
   instead — matched, not liberated. */
.tstrip .tsc { height: 5.1rem; min-height: 0; align-self: auto; }
.tstrip .tsc__c { height: 100%; align-self: auto; }
.tstrip { align-items: center; }

/* Folded locks: the screen keeps the same right edge as open — the old
   34rem "reward" is now a cap that un-justifies it. */
.tgrp--feel:has(.tf--locks.is-min) .tf--screen { max-width: none; }

/* Matched by construction, not by constant: the row stretches every cell to
   the envelope side's height, and the canvas — whose intrinsic 150px was
   the culprit — is told height 0 and STRETCHED back up, so it can follow
   the row but never drive it. */
.tstrip { align-items: stretch; }
.tstrip .tsc { height: auto; min-height: 3.2rem; align-self: stretch; }
.tstrip .tsc__c { height: 0; min-height: 100%; align-self: stretch; }

/* THE JUMP ROW — three doors where the slash mark idled. */
.menu__jump { display: flex; gap: 6px; margin: 8px 0 0; }
.menu__jumpb {
    flex: 1 1 0; display: inline-flex; align-items: center; justify-content: center;
    padding: 7px 8px; border: 1px solid var(--line); border-radius: 999px;
    font-size: .62rem; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
    color: var(--ink); text-decoration: none; white-space: nowrap;
}
.menu__jumpb:hover { border-color: var(--accent); color: var(--accent); }
.menu__findmark { display: none; }

/* The jump chips at their own size — pills, not planks — with the little
   arrow that says "opens beside you, the studio stays". */
.menu__jump { justify-content: flex-start; gap: 7px; }
.menu__jumpb { flex: 0 1 auto; padding: 5px 12px; font-size: .58rem; }
.menu__jumpb::after { content: "\2197"; font-size: .62rem; margin-left: 5px; opacity: .55; }
.menu__jumpb:hover::after { opacity: 1; }

/* The three doors, centred in the panel. */
.menu__jump { justify-content: center; }

/* The form's answer line — the page's own voice, no navigation. */
.form__status { min-height: 1.1em; margin: 0; font-size: .8rem; font-weight: 600; color: var(--accent); }

/* THE CONTACT FORM — the main page's form, verbatim: same grid, same
   status voice, same colours for its two answers. */
.form { display: grid; gap: .8rem; margin-top: 1.4rem; max-width: 460px; }
.form__row { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }
@media (max-width: 560px) { .form__row { grid-template-columns: 1fr; } }
.form .btn { justify-content: center; }
.form textarea.field { min-height: 6.5rem; resize: vertical; }
.form__status { font-size: .82rem; margin: 0; min-height: 1.2em; color: var(--ink); }
.form__status[data-state="ok"]  { color: #0F7A52; }
.form__status[data-state="err"] { color: #B3261E; }
html[data-dark] .form__status[data-state="ok"]  { color: #6EE7B7; }
html[data-dark] .form__status[data-state="err"] { color: #FCA5A5; }

/* The shelf's STYLE picker at chip height — it stood a head taller than
   the pills and the deal key beside it. */
.console__head .tfield .field { height: 2.15rem; min-height: 0; padding-block: 0; font-size: .85rem; }

/* =============================================================================
   LABS252 — BASS MODE

   A sixth pill in the under-row, and the row was built to take it: the five
   were `flex: 1 1 0` with `min-width: 0` already, so a sixth divides the same
   width rather than pushing anything off the end.

   LIT IN THE ACCENT, not in the running green. Green is what a PRESS answers
   in — every pill in this row flashes it on :active — and bass mode is a state
   rather than a press, so it wears the same colour the T switch and the mix
   keys wear when they are engaged. A toggle that lit in the press colour would
   look like a button somebody was holding down.

   AND IT FILLS ITS GROUND, which is not a decoration. Lit in accent TEXT on
   the panel it shares with its neighbours, it was the same two colours as the
   history arrows: `#t-back:not([disabled])` sets exactly `color: var(--accent);
   border-color: var(--accent)` a couple of hundred lines up, and both measured
   rgb(194,65,12) on rgb(194,65,12). After one roll three of the six pills in
   that row were orange outlines and only one of them meant ENGAGED — the panel
   behind it is 2.4% alpha, which is not a cue, and the glyph going from .7 to 1
   opacity is not one either. So the ground fills, the way :active already fills
   with the running green: a state reads as a filled pill, a press reads as a
   flash, and a loaded arrow stays an outline.
   ========================================================================== */
.t-hist__b--bass svg { display: block; opacity: .7; }
.t-hist__b--bass:hover svg { opacity: 1; }
.t-underrow .t-hist__b--bass[aria-pressed="true"] {
    color: var(--paper, #fff); border-color: var(--accent); background: var(--accent);
}
html[data-dark] .t-underrow .t-hist__b--bass[aria-pressed="true"] { color: #101318; }
.t-underrow .t-hist__b--bass[aria-pressed="true"] svg { opacity: 1; }
/* The hover rule one row up paints the free green over the fill; engaged, the
   pill keeps its own colour and only lifts. */
.t-underrow .t-hist__b--bass[aria-pressed="true"]:hover {
    color: var(--paper, #fff); border-color: var(--accent); background: var(--accent);
}
html[data-dark] .t-underrow .t-hist__b--bass[aria-pressed="true"]:hover { color: #101318; }

/* ------------------------------------------ THE SAME TRAP, ONE ROW UP ------
   `.t-bassr` was written against a measured failure: a `white-space: nowrap`
   line whose max-content feeds its parent's width, and therefore the whole
   document's. The two SENTENCE captions in the panel are the same shape and
   were never given the same guard — `.tf__cap` is nowrap by design, and these
   two carry a sentence rather than a word: the cabinet's "…instruments in
   eight families · lit is in the song · press re-voices the track · R resets"
   and the radio's "forty-six slots · press plays · hold keeps". Measured at a
   360 px layout viewport: the under-row and its measurement line contributed
   nothing, and the document still came out 607 px wide, with the cabinet
   caption's 515 px `<b>` the single offender — a horizontal scrollbar under
   every fixed element on the page, from a caption.

   Same contract, same reason: a DEFINITE zero contributes nothing to the
   parent's intrinsic size, and the percentage minimum is applied after that
   size is settled. The caption then fills the panel and ellipses, which is
   what a caption should do. The whole sentence stays in the DOM for anyone
   reading it with anything other than eyes. */
.tf--radio .tf__cap {
    display: block; width: 0; min-width: 100%;
    overflow: hidden; text-overflow: ellipsis;
}

/* WHAT IT FOUND, on its own line under the row.

   `width: 0` WITH `min-width: 100%` IS THE WHOLE OF ITS LAYOUT CONTRACT, and
   the pair is not interchangeable with the obvious `max-width: 100%`.

   Its parent is a column flex box, so this line's MAX-CONTENT width feeds the
   parent's width — and the line is `white-space: nowrap` by design, because a
   measurement that wrapped to three lines would take three lines of a sticky
   bar. Written the obvious way, one sentence of measurement made the transport
   640px wide and the document 1788px wide inside an 1100px window, which puts
   a horizontal scrollbar under every fixed element on the page. `max-width:
   100%` does not help: the 100% resolves against a parent whose width this
   element is what decides.

   A DEFINITE width of zero contributes nothing to the parent's intrinsic size,
   and a PERCENTAGE minimum is ignored while that size is being worked out and
   applied afterwards — so the parent is sized by the buttons above, and the
   line then fills exactly that and truncates. Measured: 1788px back to 1402,
   which is what the page is without this element at all. Same lesson as
   `minmax(0, 1fr)` in grid, in the shape flexbox takes it. */
.t-bassr {
    display: block; width: 0; min-width: 100%;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: .5rem; line-height: 1.3; letter-spacing: .01em;
    color: var(--ink-55, var(--ink)); opacity: .85;
    font-variant-numeric: tabular-nums;
}
.t-bassr[hidden] { display: none; }

/* ------------------------------------------------ THE UNDER-ROW'S WIDTH ----
   THE ROW ABOVE DECIDES HOW WIDE THE TRANSPORT IS. Roll, Play and DJ set it;
   the sliver row underneath divides that width up, however many keys are in it.

   Its keys already carry `flex: 1 1 0; min-width: 0`, so they share what they
   are given — but the row was still contributing its own max-content to the
   stack, which is an inline-flex column and therefore as wide as its widest
   child. Add a sixth key and the row grew, and the whole transport stretched
   out under the two big buttons instead of staying under them.

   `width: 0` takes the row out of that max-content calculation entirely, and
   `min-width: 100%` then fills whatever width the row above settled on. The
   result is the one the eye wants: a new key makes every key a little
   narrower, and the transport does not move. */
.t-underrow { width: 0; min-width: 100%; }
