/**
 * Operator UI.
 *
 * Deliberately flat: no shadows on moving parts, no blur, no WebGL, no
 * animation that runs when nothing changed. On the festival machine this may
 * share a GPU with TouchDesigner rendering a building, and the operator screen
 * must never be the thing that costs a frame.
 *
 * Colour is used as information, not decoration - a lane's state must be
 * readable from arm's length on a dark stage.
 */

:root {
    --bg: #0d0f12;
    --bg-panel: #15181d;
    --bg-raised: #1c2027;
    --bg-hover: #242932;
    --line: #2b313b;
    --line-bright: #3a424f;

    --text: #e6e9ef;
    --text-dim: #8b94a3;
    --text-faint: #5b6472;

    --active: #35d07f;
    --active-dim: #1d6b45;
    /* Fader fill. Brighter than --active-dim so gain reads at arm's length on
       a dark stage, but cooler and flatter than the meter's --active so the
       two green bars beside each other stay tellable apart. */
    --fader-fill: #2f8f5e;
    --queued: #f0b429;
    --exiting: #e2564a;
    --accent: #5b9dff;

    --lane-gap: 6px;
    --radius: 5px;

    --strip-height: 235px;
    --clips-width: 210px;

    /* Fader + meter metrics in one place; the strip row above is sized to
       match, so the control grows in travel as well as thickness. */
    --fader-width: 39px;
    --fader-track: 9px;
    --fader-thumb-width: 36px;
    --fader-thumb-height: 17px;
    --meter-width: 16px;
    --topbar-height: 62px;
}

* { box-sizing: border-box; }

html, body {
    margin: 0;
    height: 100%;
    overflow: hidden;
    background: var(--bg);
    color: var(--text);
    font: 13px/1.4 "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    -webkit-font-smoothing: antialiased;
    user-select: none;
}

button, select, input { font: inherit; color: inherit; }
button { cursor: pointer; border: none; background: none; }
[hidden] { display: none !important; }

/* --- boot ---------------------------------------------------------------- */

.boot {
    position: fixed;
    inset: 0;
    display: grid;
    place-items: center;
    background: var(--bg);
    z-index: 50;
}

.boot-panel {
    width: 340px;
    padding: 28px;
    background: var(--bg-panel);
    border: 1px solid var(--line);
    border-radius: 8px;
    text-align: center;
}

.boot-panel h1 { margin: 0; font-size: 22px; font-weight: 600; letter-spacing: -0.01em; }
.boot-logo {
    display: block;
    width: 240px;
    max-width: 100%;
    height: auto;
    margin: 0 auto 16px;
}

/* Shown only after a visitor leaves a room, to explain the empty picker. */
.boot-notice {
    margin: 14px 0 4px;
    padding: 11px 12px;
    border: 1px solid var(--line-bright);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius);
    background: var(--bg-raised);
    color: var(--text-dim);
    font-size: 12px;
    line-height: 1.5;
    text-align: left;
}
.boot-notice strong { display: block; margin-bottom: 3px; color: var(--text); }
.boot-sub { margin: 4px 0 24px; color: var(--text-faint); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; }

.picker-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 6px;
    font-size: 11px;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.picker-count { font-variant-numeric: tabular-nums; }
.picker-count.full { color: var(--active); }

/* Scrolls rather than growing: the list is however many packs are staged, and
   the panel must stay on screen on a tablet. */
.pack-list {
    max-height: 300px;
    overflow-y: auto;
    text-align: left;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-raised);
    padding: 4px;
    scrollbar-width: thin;
    scrollbar-color: var(--line-bright) transparent;
}

.pack-row {
    display: flex;
    align-items: center;
    gap: 9px;
    width: 100%;
    padding: 8px 9px;
    margin-bottom: 2px;
    border: 1px solid transparent;
    border-left: 3px solid var(--line-bright);
    border-radius: 3px;
    background: transparent;
    text-align: left;
    color: var(--text-dim);
}
.pack-row:hover { background: var(--bg-hover); color: var(--text); }

/* The order badge is the point of the design: it shows WHICH pack anchors the
   session tempo and key, not merely that two are selected. */
.pack-order {
    flex: none;
    width: 18px; height: 18px;
    display: grid; place-items: center;
    border: 1px solid var(--line-bright);
    border-radius: 3px;
    font-size: 10px; font-weight: 700;
    color: transparent;
}
.pack-row.on { border-left-color: var(--active); color: var(--text); background: rgba(53, 208, 127, 0.08); }
.pack-row.on .pack-order { background: var(--active); border-color: var(--active); color: #06121f; }
.pack-row.anchor .pack-order { background: var(--accent); border-color: var(--accent); }

.pack-meta { flex: 1; min-width: 0; }
.pack-name { display: block; font-weight: 600; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pack-sub { display: block; font-size: 10px; color: var(--text-faint); font-variant-numeric: tabular-nums; }
.pack-row.disabled { opacity: 0.4; cursor: not-allowed; }
.pack-row.disabled:hover { background: transparent; color: var(--text-dim); }

.picker-hint { margin: 10px 0 14px; font-size: 11px; color: var(--text-faint); text-align: left; }

.boot-actions { display: flex; gap: 8px; }
.boot-button {
    flex: 1;
    padding: 11px;
    background: var(--accent);
    color: #06121f;
    font-weight: 600;
    border-radius: var(--radius);
}
.boot-button:disabled { background: var(--bg-raised); color: var(--text-faint); cursor: not-allowed; }

/* A lane's clips are grouped by pack when more than one is loaded, so a
   merged column reads as labelled blocks rather than an interleaved jumble. */
.clip-pack-head {
    /* Negative side margins (with the padding added back) let the label bleed
       over the list gutters, so a clip scrolling past cannot show slivers
       beside it. */
    margin: 0 -3px;
    padding: 6px 11px 3px;
    font-size: 9px;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--text-faint);
    position: sticky;
    top: -3px;
    z-index: 2;
    background: var(--bg-panel);
}

/* A clip passing under a sticky label would otherwise be guillotined mid-row.
   Fade it out instead, so it reads as scrolled-under rather than cut off. */
.clip-pack-head::after {
    content: ;
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: 14px;
    background: linear-gradient(var(--bg-panel), transparent);
    pointer-events: none;
}

.boot-status { margin-top: 14px; min-height: 32px; font-size: 12px; color: var(--text-dim); }
.boot-status.error { color: var(--exiting); }

/* --- shared-session entry ----------------------------------------------- */

.join-panel { text-align: left; }
.join-panel h1 { text-align: center; }
.join-panel .boot-sub, .join-room { text-align: center; }
.join-panel .boot-sub { margin-bottom: 6px; }
.join-room { margin: 6px 0 22px; color: var(--text-dim); }
.join-field { display: grid; gap: 6px; margin: 0 0 15px; color: var(--text-dim); font-size: 11px; }
.join-field input {
    width: 100%;
    padding: 11px 12px;
    border: 1px solid var(--line-bright);
    border-radius: var(--radius);
    outline: none;
    background: var(--bg-raised);
    color: var(--text);
    font-size: 15px;
    user-select: text;
}
.join-field input:focus { border-color: var(--accent); }

.collab-hold {
    position: fixed;
    inset: 0;
    display: grid;
    place-items: center;
    padding: 20px;
    background: rgba(13, 15, 18, 0.94);
    z-index: 70;
}
.collab-hold-card { max-width: 380px; text-align: center; }
.collab-hold-card h2 { margin: 14px 0 7px; font-size: 20px; }
.collab-hold-card p { margin: 0; color: var(--text-dim); }
.hold-pulse {
    display: inline-block;
    width: 11px;
    height: 11px;
    border-radius: 50%;
    background: var(--queued);
    animation: hold-pulse 1.5s ease-in-out infinite;
}
@keyframes hold-pulse { 50% { opacity: 0.35; transform: scale(0.8); } }

.host-panel {
    grid-column: 2;
    grid-row: 1 / -1;
    min-width: 0;
    margin: var(--lane-gap) var(--lane-gap) var(--lane-gap) 0;
    overflow-y: auto;
    overflow-x: hidden;
    border: 1px solid var(--line-bright);
    border-radius: 8px;
    background: var(--bg-panel);
    box-shadow: -8px 0 24px rgba(0, 0, 0, 0.22);
}
.host-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 14px;
    border-bottom: 1px solid var(--line);
}
.host-panel-head > div { min-width: 0; }
.host-panel-head strong, .host-panel-head span { display: block; }
.host-panel-head span { margin-top: 2px; color: var(--text-faint); font-size: 10px; }
#hostRoomLabel { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.host-request-badge {
    flex: none;
    padding: 5px 7px;
    border-radius: 999px;
    color: #1a1204 !important;
    background: var(--queued);
    font-weight: 700;
    animation: request-pulse 1.15s ease-in-out infinite;
}
.host-panel.has-requests { border-color: var(--queued); }
@keyframes request-pulse { 50% { box-shadow: 0 0 0 5px rgba(240, 180, 41, 0.14); } }
.host-panel-tools { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 11px 14px; }
.join-policy { color: var(--text-dim); font-size: 11px; }
.host-panel section { padding: 5px 14px 10px; }
.host-panel h3 { margin: 5px 0 7px; color: var(--text-faint); font-size: 10px; letter-spacing: .07em; text-transform: uppercase; }
.host-member-list { display: grid; gap: 5px; }
/* Two rows since the jazzicon arrived: identity on top, authority beneath.
   One line could not hold art, a full name, a role select and Kick. */
.host-member {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 5px;
    padding: 6px 7px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-raised);
    overflow: hidden;
}
.host-member-identity { display: flex; align-items: center; gap: 7px; min-width: 0; }
.host-member-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.host-member-controls { display: flex; align-items: center; gap: 6px; }
.host-member-controls select { flex: 1; min-width: 0; }
.host-member select { flex: none; max-width: 76px; padding: 4px; border: 1px solid var(--line); border-radius: 3px; background: var(--bg-panel); font-size: 10px; }
.host-mini-button { padding: 5px 7px; border: 1px solid var(--line); border-radius: 3px; color: var(--text-dim); font-size: 10px; }
.host-mini-button:hover { border-color: var(--line-bright); color: var(--text); }
.host-role-label { color: var(--active); font-size: 10px; }
.host-empty { margin: 4px 0; color: var(--text-faint); font-size: 11px; }
.host-danger-row { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 4px 14px 14px; }
.host-end, .host-kick-all { padding: 8px 4px; border: 1px solid var(--exiting); border-radius: var(--radius); color: var(--exiting); }
.host-end:hover { color: #fff; background: var(--exiting); }
.host-kick-all { border-color: var(--queued); color: var(--queued); }
.host-kick-all:hover:not(:disabled) { color: #1a1204; background: var(--queued); }
.host-kick-all:disabled { opacity: .35; }

.role-readout, .role-pack-readout { display: none; }

/* Shared-session roles progressively reveal only the controls each visitor
   can actually use. Dataset changes apply immediately when the Host promotes
   or demotes somebody; the player never needs to reload. */
body[data-collab-role="player"] :is(#playButton, #stopAllButton, #tempoSlider, #tempoPending, #keySelect, .lock-toggle, .master-wrap, #packsButton),
body[data-collab-role="viewer"] :is(#playButton, #stopAllButton, #tempoSlider, #tempoPending, #keySelect, .lock-toggle, #themeSelect, .master-wrap, #packsButton),
body[data-collab-role="producer"] #packsButton {
    display: none;
}
body[data-collab-role="player"] :is(#keyValue, #packReadout),
body[data-collab-role="viewer"] :is(#keyValue, #themeValue, #packReadout),
body[data-collab-role="producer"] #packReadout {
    display: inline-block;
}
body.collab-paused .clip { opacity: 0.46; }
button:disabled, select:disabled, input:disabled { cursor: not-allowed; }

/* Jazzicon identity art. Same seed, same icon, everywhere in the product. */
.member-avatar {
    display: block;
    flex: none;
    border-radius: 50%;
    overflow: hidden;
    background: var(--bg-raised);
}
.member-avatar svg { display: block; width: 100%; height: 100%; }

.collab-identity {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
}
.collab-own-avatar {
    display: inline-flex;
    flex: none;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    overflow: hidden;
}
/* The name the visitor typed on the way in. Capped and clipped: it is free
   text, and an unbounded one would push the rest of the row off the screen. */
.collab-own-name {
    min-width: 0;
    max-width: 110px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Matches .tempo-value: this is a name, not a field label, and it should
       carry the same weight as the other values in the bar. */
    font-size: 15px;
    font-weight: 600;
    color: var(--text-dim);
}

.collab-room-badge {
    padding: 3px 8px;
    border: 1px solid var(--line-bright);
    border-radius: 999px;
    background: var(--bg-raised);
    color: var(--text-dim);
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 160px;
}

.collab-role-badge {
    padding: 4px 7px;
    border: 1px solid var(--line-bright);
    border-radius: 999px;
    color: var(--text-dim);
    font-size: 9px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
}
.collab-role-badge.role-player { border-color: var(--active); color: var(--active); }
.collab-role-badge.role-producer { border-color: var(--queued); color: var(--queued); }
.collab-role-badge.changed { animation: role-badge-pulse .7s ease-in-out 5; }
@keyframes role-badge-pulse { 50% { transform: scale(1.1); box-shadow: 0 0 0 5px rgba(79, 189, 133, .12); } }

.role-notice {
    position: fixed;
    top: calc(var(--topbar-height) + 18px);
    left: 50%;
    z-index: 90;
    display: grid;
    gap: 4px;
    width: min(420px, calc(100vw - 28px));
    padding: 14px 18px;
    transform: translateX(-50%);
    border: 1px solid var(--active);
    border-radius: 8px;
    background: rgba(25, 30, 37, .96);
    box-shadow: 0 16px 40px rgba(0, 0, 0, .42);
    text-align: center;
}
.role-notice[hidden] { display: none; }
.role-notice strong { color: var(--active); font-size: 14px; }
.role-notice span { color: var(--text-dim); font-size: 11px; line-height: 1.45; }
.role-notice.role-viewer { border-color: var(--line-bright); }
.role-notice.role-viewer strong { color: var(--text); }
.role-notice.role-producer { border-color: var(--queued); }
.role-notice.role-producer strong { color: var(--queued); }
.role-notice.show { animation: role-notice-in .2s ease-out; }
@keyframes role-notice-in { from { opacity: 0; transform: translate(-50%, -8px); } }

/* --- topbar -------------------------------------------------------------- */

.topbar {
    display: flex;
    align-items: center;
    /* 14px, not 22: with six groups the gaps alone were 110px of the bar. */
    gap: 12px;
    height: var(--topbar-height);
    padding: 0 16px;
    background: var(--bg-panel);
    border-bottom: 1px solid var(--line);
}

.topbar-group { display: flex; align-items: center; gap: 10px; }
.topbar-group.right { margin-left: auto; }

.role-readout {
    min-width: 28px;
    color: var(--text);
    font-size: 15px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.role-pack-readout {
    max-width: 190px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.leave-session { padding: 6px 10px; border-color: var(--exiting); color: var(--exiting); }
.leave-session:hover { color: #fff; background: var(--exiting); }

.transport-button {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    /* Fixed: "▶ Play" and "■ Stop" render 2.6px apart, and the button sits at
       the far left of the bar - so every transport start nudged every control
       to its right. */
    width: 88px;
    padding: 8px 0;
    background: var(--bg-raised);
    border: 1px solid var(--line-bright);
    border-radius: var(--radius);
    font-weight: 600;
}
.transport-button .glyph { font-size: 11px; color: var(--active); }
.transport-button.playing { background: var(--active-dim); border-color: var(--active); }
.transport-button.playing .glyph { color: var(--text); }

.ghost-button {
    padding: 8px 12px;
    background: transparent;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    color: var(--text-dim);
}
.ghost-button:hover { background: var(--bg-hover); color: var(--text); }

.phrase-group { gap: 12px; }

.phrase-readout {
    font-variant-numeric: tabular-nums;
    font-size: 19px;
    font-weight: 600;
    min-width: 52px;
}
.phrase-sep { color: var(--text-faint); margin: 0 1px; }
.phrase-beat { color: var(--text-dim); }

/* Eight ticks = eight bars. The fill crossing the last tick is the cue that a
   queued clip is about to land. */
.phrase-meter {
    position: relative;
    width: 110px;
    height: 8px;
    background: var(--bg-raised);
    border-radius: 4px;
    overflow: hidden;
}
.phrase-fill { height: 100%; width: 0; background: var(--accent); }
.phrase-ticks { position: absolute; inset: 0; display: flex; }
.phrase-ticks i { flex: 1; border-right: 1px solid var(--bg); }
.phrase-ticks i:last-child { border-right: none; }

.countdown {
    font-variant-numeric: tabular-nums;
    font-size: 12px;
    color: var(--text-dim);
    min-width: 40px;
}
.countdown.imminent { color: var(--queued); font-weight: 600; }

.field { display: flex; align-items: center; gap: 8px; }
.field-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.06em; }

.tempo-control { display: flex; align-items: center; gap: 9px; }
.tempo-control input[type="range"] { width: 92px; accent-color: var(--accent); }
.tempo-value { font-variant-numeric: tabular-nums; font-size: 15px; font-weight: 600; min-width: 30px; }

/* The chip always occupies its space and only toggles visibility. Appearing
   and disappearing with display:none shoved every control to its right
   sideways, which is unacceptable on a bar the operator is aiming at. */
.pending-chip {
    width: 44px;
    text-align: center;
    padding: 3px 0;
    background: rgba(240, 180, 41, 0.14);
    border: 1px solid var(--queued);
    border-radius: 3px;
    color: var(--queued);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
}
.pending-chip.invisible { visibility: hidden; }

/* The theme select carries its own pending state - it already shows the
   incoming name, so it only needs to say "not yet". */
.theme-select.pending {
    border-color: var(--queued);
    color: var(--queued);
    animation: pulse 0.9s ease-in-out infinite;
}

.theme-select {
    padding: 6px 8px;
    background: var(--bg-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    font-weight: 600;
}

.key-select {
    padding: 6px 8px;
    background: var(--bg-raised);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    font-weight: 600;
}

.toggle { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-dim); }
.toggle input { accent-color: var(--accent); }

/* Key lock as an icon toggle. The words "Key lock" cost ~55px on a bar that
   had none to spare; the padlock plus its lit state carries the same meaning
   and the title attribute carries the rest. */
.lock-toggle { position: relative; display: inline-grid; place-items: center; }
.lock-toggle input {
    position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer;
}
.lock-toggle span {
    display: grid; place-items: center;
    width: 30px; height: 30px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-raised);
    font-size: 13px;
    color: var(--text-faint);
}
.lock-toggle input:checked + span {
    background: var(--active-dim);
    border-color: var(--active);
    color: #fff;
}
.lock-toggle input:focus-visible + span { outline: 1px solid var(--accent); outline-offset: 2px; }

/* Queued for the next boundary - same amber language as a queued clip. The
   animation lives on the label: a native select does not animate dependably,
   and a Viewer has no select at all. */
.field.pending .field-label {
    color: var(--queued);
    animation: pulse 0.9s ease-in-out infinite;
}
.key-select.pending { border-color: var(--queued); color: var(--queued); }
.role-readout.pending {
    color: var(--queued);
    animation: pulse 0.9s ease-in-out infinite;
}

.lock-toggle.pending span {
    border-color: var(--queued);
    color: var(--queued);
    animation: pulse 0.9s ease-in-out infinite;
}

/* An inert control should look inert. */
.key-select:disabled {
    opacity: 0.4;
    cursor: not-allowed;
    color: var(--text-faint);
}

/* --- master output ------------------------------------------------------- */

.master-wrap { position: relative; }
/* Keep the label and its value on one line - a cramped top bar was wrapping
   them, which changes the bar's height as well as the button's width. */
#masterButton { white-space: nowrap; }

/* Reserve room for three digits so the button does not resize as the value
   crosses 100 -> 99 -> 9. `ch` is the width of one figure, and tabular-nums
   makes every digit that same width, so 3ch is exactly three digits. */
.master-value {
    display: inline-block;
    /* 3.25ch, not 3ch: a three-digit value renders a fraction wider than three
       `ch` units, which left the button 0.1px wider at 100 than at 99. The
       extra quarter pads every case onto the same box. */
    min-width: 3.25ch;
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: var(--text);
    margin-left: 4px;
}
.master-value.reducing { color: var(--queued); }

.master-popup {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    width: 230px;
    padding: 14px;
    background: var(--bg-panel);
    border: 1px solid var(--line-bright);
    border-radius: var(--radius);
    z-index: 40;
}

.master-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 8px;
}
.master-label { font-size: 10px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-dim); }
/* Same fixed-width treatment inside the popup. The limiter readout needs more
   room: it swings between "0.0 dB" and "-12.3 dB". */
.master-readout {
    display: inline-block;
    min-width: 3.25ch;
    text-align: right;
    font-size: 13px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.master-readout.wide { min-width: 8ch; }

#masterFader {
    width: 100%;
    margin: 2px 0 12px;
    accent-color: var(--active);
}

.master-meter {
    flex: 1;
    height: 8px;
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 4px;
    overflow: hidden;
}
.master-meter i {
    display: block;
    height: 100%;
    width: 0;
    background: linear-gradient(to right, var(--active) 0%, var(--active) 70%, var(--queued) 88%, var(--exiting) 100%);
}

/* Amber whenever the limiter is actually working, so it reads as "engaged",
   not as a fault. */
.master-readout.reducing { color: var(--queued); }

.master-note { margin: 4px 0 10px; font-size: 10px; line-height: 1.5; color: var(--text-faint); }
.master-reset { width: 100%; padding: 6px; font-size: 11px; }

/* Mode is derived, never chosen - so it reads as a status word, not a control.
   Fixed width so the bar does not shift as it changes. */
.mode-label {
    display: inline-block;
    /* Short codes, not full words: reserving 11 characters for "PERFORMANCE"
       cost ~55px on a bar with none to spare. Colour carries most of the
       meaning anyway, and the full word is in the title.
       A FIXED width, not min-width: these are letters, so `ch` (a digit's
       width) does not bound them - "PERF" renders ~3px wider than "IDLE" at
       the same character count, and the bar shifted every time the transport
       started. */
    width: 52px;
    text-align: center;
    padding: 4px 8px;
    font-size: 10px;
    letter-spacing: 0.09em;
    border: 1px solid var(--line);
    border-radius: 3px;
    background: var(--bg-raised);
    color: var(--text-faint);
}
.mode-label.PERFORMANCE { color: var(--active); border-color: var(--active-dim); }
.mode-label.LOADING { color: var(--queued); border-color: var(--queued); }

.status { display: flex; align-items: center; gap: 14px; }
.status-pack { font-size: 12px; color: var(--text-dim); letter-spacing: 0.04em; }

.bridge-wrap { position: relative; }
.bridge-light {
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-raised);
}
.bridge-light:hover { background: var(--bg-hover); }
.bridge-light i { width: 8px; height: 8px; border-radius: 50%; background: var(--exiting); }
.bridge-light.up i { background: var(--active); }

.bridge-popup {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    width: 250px;
    padding: 14px;
    background: var(--bg-panel);
    border: 1px solid var(--line-bright);
    border-radius: var(--radius);
    z-index: 40;
}
.bridge-state { font-size: 12px; font-weight: 600; color: var(--exiting); }
.bridge-state.up { color: var(--active); }
.bridge-addr { font-size: 12px; font-variant-numeric: tabular-nums; color: var(--text); }

/* --- session ------------------------------------------------------------- */

.session {
    display: grid;
    /* minmax(0, ...) not a bare 1fr: `1fr` means minmax(auto, 1fr), so a tall
       lane column could impose its content height as a minimum and squeeze the
       fixed strip row - collapsing .strip-body, and the fader inside it, to
       zero. The lanes scroll; they must never size this row. */
    grid-template-rows: minmax(0, 1fr) var(--strip-height);
    grid-template-columns: minmax(0, 1fr);
    height: calc(100% - var(--topbar-height));
}

.session { grid-template-columns: minmax(0, 1fr) var(--clips-width); }
body[data-collab-role="host"] .session {
    grid-template-columns: minmax(0, 8fr) var(--clips-width) minmax(190px, 1fr);
}
body[data-collab-role="host"] .host-panel { grid-column: 3; }

/* --- clips ---------------------------------------------------------------
   Saved snapshots of a performance, to the right of the lanes and left of the
   Host panel. Spans both rows: it belongs beside the whole instrument, not
   beside the lanes or the mixer alone. */
.clips-panel {
    grid-column: 2;
    grid-row: 1 / -1;
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-height: 0;
    margin: var(--lane-gap) var(--lane-gap) var(--lane-gap) 0;
    padding: 10px;
    background: var(--bg-panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}
body:is([data-collab-role="viewer"], [data-collab-role="player"]) .clips-panel { display: none; }
body:is([data-collab-role="viewer"], [data-collab-role="player"]) .session {
    grid-template-columns: minmax(0, 1fr);
}

.clips-head { display: flex; align-items: center; justify-content: space-between; }
.clips-head strong { font-size: 11px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
.clips-save { display: flex; gap: 6px; }
.clips-save input {
    flex: 1;
    min-width: 0;
    padding: 6px 8px;
    border: 1px solid var(--line-bright);
    border-radius: 3px;
    background: var(--bg-raised);
    color: var(--text);
    font-size: 12px;
}
.clips-save input:focus { outline: none; border-color: var(--accent); }
.clips-save button { flex: none; padding: 6px 10px; font-size: 11px; }
.clips-list {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
    gap: 4px;
    scrollbar-width: thin;
    scrollbar-color: var(--line-bright) transparent;
}
.clips-empty { margin: 0; font-size: 11px; line-height: 1.5; color: var(--text-faint); }
.clip-card { display: flex; gap: 4px; align-items: stretch; }
.clip-recall {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 7px 8px;
    text-align: left;
    background: var(--bg-raised);
    border: 1px solid transparent;
    border-left: 3px solid var(--line-bright);
    border-radius: 3px;
    color: var(--text-dim);
}
.clip-recall:hover { background: var(--bg-hover); color: var(--text); border-left-color: var(--accent); }

/* Waiting for the loop to flip. Same yellow, same pulse as a queued sample -
   the wait means the same thing here, so it should look the same. */
.clip-card.pending .clip-recall {
    border-left-color: var(--queued);
    background: rgba(240, 180, 41, 0.10);
    animation: pulse 0.9s ease-in-out infinite;
}
.clip-card.pending .clip-card-name { color: var(--queued); }

/* Landed, and the session still describes it. Only the bar changes: the card
   is not a control that is "on", it is a place the performance currently is. */
.clip-card.current .clip-recall { border-left-color: var(--active); }
.clip-card.current .clip-card-name { color: var(--active); }
.clip-card-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 12px;
    font-weight: 600;
    color: var(--text);
}
.clip-card-meta { font-size: 10px; color: var(--text-faint); font-variant-numeric: tabular-nums; }
.clip-delete {
    flex: none;
    width: 22px;
    background: var(--bg-raised);
    border: 1px solid var(--line);
    border-radius: 3px;
    color: var(--text-faint);
    font-size: 13px;
    line-height: 1;
}
.clip-delete:hover { color: #fff; background: var(--exiting); border-color: var(--exiting); }

/* Deleting belongs to the Host. A Producer sees the Clips column and can play
   from it, but cannot throw anything away; the card takes the full width where
   the button was rather than leaving a gap. */
body[data-collab-role="producer"] .clip-delete,
body[data-collab-role="player"] .clip-delete,
body[data-collab-role="viewer"] .clip-delete {
    display: none;
}
.clips-status { min-height: 26px; font-size: 10px; line-height: 1.4; color: var(--text-dim); }
.clips-status.error { color: var(--exiting); }

.lanes, .strips {
    display: grid;
    grid-template-columns: repeat(8, minmax(0, 1fr));
    gap: var(--lane-gap);
    padding: 0 var(--lane-gap);
    grid-column: 1;
}

.lanes { padding-top: var(--lane-gap); min-height: 0; }

.lane { display: flex; flex-direction: column; min-height: 0; min-width: 0; }

.lane-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 6px 8px;
    background: var(--bg-panel);
    border: 1px solid var(--line);
    border-bottom: none;
    border-radius: var(--radius) var(--radius) 0 0;
}
.lane-name { font-size: 11px; font-weight: 700; letter-spacing: 0.09em; }
.lane-count { font-size: 10px; color: var(--text-faint); font-variant-numeric: tabular-nums; }

/* The clip list is the only scrolling region: lane sizes differ per pack, so
   the column scrolls rather than the page. */
.clips {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    background: var(--bg-panel);
    border: 1px solid var(--line);
    border-top: none;
    padding: 3px;
    scrollbar-width: thin;
    scrollbar-color: var(--line-bright) transparent;
}
/* Keeps any scrolling the browser does itself - focus, keyboard, anchors -
   clear of the sticky pack label, the same way _revealClip does by hand. */
.clips { scroll-padding-top: 56px; }

.clips::-webkit-scrollbar { width: 8px; }
.clips::-webkit-scrollbar-thumb { background: var(--line-bright); border-radius: 4px; }
.clips::-webkit-scrollbar-track { background: transparent; }

.clip {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    padding: 7px 8px;
    margin-bottom: 2px;
    background: var(--bg-raised);
    border: 1px solid transparent;
    border-left: 3px solid var(--line-bright);
    border-radius: 3px;
    text-align: left;
    color: var(--text-dim);
    font-size: 12px;
    overflow: hidden;
}
.clip:hover { background: var(--bg-hover); color: var(--text); }

.clip-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clip-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--line-bright); flex: none; }

.clip.active {
    background: var(--active-dim);
    border-left-color: var(--active);
    color: #fff;
    font-weight: 600;
}
.clip.active .clip-dot { background: var(--active); }

/* Queued has to read instantly from across a stage - the performer may be
   waiting up to 8 bars and needs to know the machine heard them. */
.clip.queued {
    border-left-color: var(--queued);
    color: var(--queued);
    background: rgba(240, 180, 41, 0.10);
}
.clip.queued .clip-dot { background: var(--queued); animation: pulse 0.9s ease-in-out infinite; }

.clip.exiting { border-left-color: var(--exiting); color: var(--exiting); }
.clip.exiting .clip-dot { background: var(--exiting); animation: pulse 0.9s ease-in-out infinite; }

@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }

/* --- channel strips ------------------------------------------------------ */

.strips {
    padding-top: var(--lane-gap);
    padding-bottom: var(--lane-gap);
    /* The row is a fixed height; without this a tall child would spill past
       the bottom of the page instead of being contained. */
    min-height: 0;
    overflow: hidden;
}

.strip {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 10px;
    background: var(--bg-panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    min-width: 0;
    /* Grid items default to min-height:auto, which refuses to shrink below
       content. The vertical range input reports a large intrinsic height, so
       without these the strip grew past its row and ran off screen. */
    min-height: 0;
    height: 100%;
    overflow: hidden;
}

.strip-top { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.strip-name { font-size: 11px; font-weight: 700; letter-spacing: 0.09em; }

.strip-state {
    font-size: 9px;
    letter-spacing: 0.08em;
    padding: 2px 5px;
    border-radius: 3px;
    background: var(--bg-raised);
    color: var(--text-faint);
}
.strip-state.ACTIVE { background: var(--active-dim); color: #fff; }
.strip-state.QUEUED { background: rgba(240, 180, 41, 0.18); color: var(--queued); }
.strip-state.EXITING { background: rgba(226, 86, 74, 0.18); color: var(--exiting); }

.strip-playing {
    font-size: 11px;
    color: var(--text-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-height: 15px;
}

/* Fader and meter sit side by side so level and gain read as one control. */
.strip-body { display: flex; gap: 9px; flex: 1 1 0; min-height: 0; align-items: stretch; }

.fader-wrap { flex: 1; display: flex; min-height: 0; justify-content: center; }

/* Vertical range input.
 *
 * `-webkit-appearance: slider-vertical` was REMOVED in Chrome 121 - keeping it
 * alongside writing-mode rendered a bare track with no thumb until a drag
 * forced a repaint. Orientation now comes from writing-mode alone, and the
 * track/thumb are drawn explicitly so the result does not depend on whatever
 * the browser's native vertical slider happens to look like. */
.fader {
    -webkit-appearance: none;
    appearance: none;
    writing-mode: vertical-lr;
    direction: rtl;
    width: var(--fader-width);
    height: 100%;
    min-height: 0;
    margin: 0;
    background: transparent;
    cursor: ns-resize;
}
/* The filled portion below the thumb is the current gain. Chrome draws no
 * such fill for a range input, so it is a gradient with a hard stop at
 * --fill, which the view updates whenever the value changes. Gradient
 * keywords are physical, so `to top` fills upward regardless of the
 * vertical writing mode. */
.fader::-webkit-slider-runnable-track {
    width: var(--fader-track);
    height: 100%;
    border: 1px solid var(--line);
    border-radius: 3px;
    background: linear-gradient(
        to top,
        var(--fader-fill) 0 var(--fill, 0%),
        var(--bg) var(--fill, 0%) 100%
    );
}
.fader:hover::-webkit-slider-runnable-track {
    background: linear-gradient(
        to top,
        var(--active) 0 var(--fill, 0%),
        var(--bg-raised) var(--fill, 0%) 100%
    );
}
.fader::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: var(--fader-thumb-width);
    height: var(--fader-thumb-height);
    box-sizing: border-box;
    /* WebKit does not centre a thumb wider than its track; Firefox does, so
       this offset stays scoped to the -webkit- pseudo-element. */
    margin-left: calc((var(--fader-track) - var(--fader-thumb-width)) / 2);
    background: var(--text-dim);
    border: 1px solid var(--line-bright);
    border-radius: 2px;
}
.fader:hover::-webkit-slider-thumb { background: var(--text); }
.fader:active::-webkit-slider-thumb { background: var(--active); border-color: var(--active); }
/* Gecko ignores every -webkit- pseudo-element above and would otherwise render
   a default platform slider. These mirror the WebKit rules so the control reads
   the same in Firefox. */
.fader::-moz-range-track {
    width: var(--fader-track);
    height: 100%;
    border: 1px solid var(--line);
    border-radius: 3px;
    background: linear-gradient(
        to top,
        var(--fader-fill) 0 var(--fill, 0%),
        var(--bg) var(--fill, 0%) 100%
    );
}
.fader:hover::-moz-range-track {
    background: linear-gradient(
        to top,
        var(--active) 0 var(--fill, 0%),
        var(--bg-raised) var(--fill, 0%) 100%
    );
}
/* The fill is painted by the track gradient above, so Firefox's own progress
   element must not paint a second one over it. */
.fader::-moz-range-progress { background: transparent; }
.fader::-moz-range-thumb {
    width: var(--fader-thumb-width);
    height: var(--fader-thumb-height);
    box-sizing: border-box;
    background: var(--text-dim);
    border: 1px solid var(--line-bright);
    border-radius: 2px;
}
.fader:hover::-moz-range-thumb { background: var(--text); }
.fader:active::-moz-range-thumb { background: var(--active); border-color: var(--active); }

.fader:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }

.meter {
    width: var(--meter-width);
    background: var(--bg);
    border: 1px solid var(--line);
    border-radius: 2px;
    position: relative;
    overflow: hidden;
    flex: none;
}
.meter-fill {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: var(--level, 0);
    background: linear-gradient(to top, var(--active) 0%, var(--active) 62%, var(--queued) 84%, var(--exiting) 100%);
}

.strip-foot { display: flex; gap: 6px; }
.strip-foot button {
    flex: 1 1 0;
    /* Without this a flex item will not shrink below its text, so SOLO
       overflowed the strip and was clipped rather than narrowing. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    padding: 5px 2px;
    font-size: 10px;
    letter-spacing: 0.06em;
    background: var(--bg-raised);
    border: 1px solid var(--line);
    border-radius: 3px;
    color: var(--text-faint);
}
.strip-foot button:hover { background: var(--bg-hover); color: var(--text); }
.strip-foot .mute.on { background: var(--exiting); border-color: var(--exiting); color: #fff; font-weight: 600; }
.strip-foot .solo.on { background: var(--queued); border-color: var(--queued); color: #1a1204; font-weight: 600; }

/* Another lane holds the solo, so this lane's mix controls are inert: they
   would describe a mix nobody is hearing. The name and meter stay legible. */
.strip.solo-locked .strip-body,
.strip.solo-locked .strip-foot {
    pointer-events: none;
    opacity: 0.4;
}

/* Silenced only because another lane is soloed - dimmed so it is obvious why
   the lane went quiet without hunting for which button is lit. */
.strip.silenced { opacity: 0.45; }
.strip.silenced .meter-fill { background: var(--line-bright); }

/* --- toast --------------------------------------------------------------- */

.toast {
    position: fixed;
    bottom: 18px;
    left: 50%;
    transform: translateX(-50%);
    padding: 9px 16px;
    background: var(--bg-raised);
    border: 1px solid var(--line-bright);
    border-radius: var(--radius);
    font-size: 12px;
    z-index: 60;
}
.toast.error { border-color: var(--exiting); color: var(--exiting); }

/* Narrow screens: an iPad in portrait still needs all eight lanes visible, so
   the columns compress rather than wrap. Below tablet width the strips shrink
   first, since clip selection matters more than fader precision. */
/* Clips are a desktop tool. Below this there is not room for eight lanes, the
   mixer and a third column without all three becoming unusable, so the panel
   goes and the grid closes up behind it. */
@media (max-width: 900px) {
    .clips-panel { display: none; }
    .session { grid-template-columns: minmax(0, 1fr); }
    body[data-collab-role="host"] .session {
        grid-template-columns: minmax(0, 8fr) minmax(190px, 1fr);
    }
    body[data-collab-role="host"] .host-panel { grid-column: 2; }
}

@media (max-width: 1100px) {
    :root { --strip-height: 195px; --lane-gap: 4px; }
    .topbar { gap: 14px; }
    .phrase-meter { width: 130px; }
    .tempo-control input[type="range"] { width: 90px; }
}

@media (max-width: 600px) {
    .host-panel-tools { align-items: stretch; flex-direction: column; }

    /* Square MUTE/SOLO buttons stacked in a ~34px column eat about 70px of the
       strip, so the row is taller here than the 1100px value above - otherwise
       the fader would give up the difference. */
    :root { --strip-height: 215px; }

    /* Each strip sits under its own lane, so the mixer keeps the lanes' eight
       equal columns rather than scrolling on its own. A ~42px column cannot
       hold a fader beside a meter beside two buttons, so everything that was
       side by side stacks: fader, then a horizontal meter, then the buttons. */
    .strips {
        grid-template-columns: repeat(8, minmax(0, 1fr));
        overflow: hidden;
    }
    .strip {
        padding: 5px 3px;
        gap: 3px;
        /* The fader is the full column wide here, and its thumb has to stay
           inside that. */
        --fader-width: 100%;
        --fader-thumb-width: 30px;
        --fader-thumb-height: 16px;
    }
    /* Both are unreadable at this width, and the lane above already shows
       which clip is up and what state it is in. */
    .strip-state,
    .strip-playing { display: none; }
    .strip-top { justify-content: center; }
    .strip-name {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: 9px;
        letter-spacing: 0;
    }
    .strip-body { flex-direction: column; gap: 3px; align-items: stretch; }
    .fader-wrap { flex: 1; min-height: 0; }
    .meter {
        /* Between the lane name and the fader: the DOM has the meter after the
           fader for the desktop side-by-side layout, so reorder rather than
           reshuffle the markup. */
        order: -1;
        width: 100%;
        height: 7px;
        flex: none;
    }
    .meter-fill {
        top: 0;
        right: auto;
        width: var(--level, 0);
        height: auto;
        background: linear-gradient(
            to right,
            var(--active) 0%, var(--active) 62%,
            var(--queued) 84%, var(--exiting) 100%
        );
    }
    .strip-foot { flex-direction: column; gap: 3px; }
    .strip-foot button {
        /* flex: 1 shares width in a row but height in a column, which let these
           collapse. Square, sized by the column width; the fader takes the
           remaining slack. */
        flex: none;
        aspect-ratio: 1;
        padding: 0;
        font-size: 9px;
        letter-spacing: 0;
    }

    /* Eight lane heads across a phone leave ~42px each, which the name alone
       overflows - pushing the count past the viewport and making the whole
       page scroll sideways. The sample count is the expendable half, so it
       goes and the name keeps its width. */
    .lane-head { padding: 5px 4px; gap: 0; justify-content: center; }
    .lane-count { display: none; }
    .lane-name {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: 10px;
        letter-spacing: 0.01em;
    }
    .collab-room-badge { max-width: 40vw; }
    /* Phones get a compact status deck; the desktop toolbar needs ~1280px.
       `display: contents` lets the status items take grid cells individually
       while the desktop DOM grouping stays intact.

       Five columns throughout, so one row can carry identity art, a role, the
       room, the bridge light and Leave without pushing anything off the edge. */

    /* --- Viewer and Player: three rows -------------------------------- */
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) {
        --topbar-height: 138px;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .topbar {
        display: grid;
        /* Six even shares, not auto tracks: the name beside the icon needs two
           of them, and an auto track sized to its widest row refuses to shrink
           - which pushed a Player's wider theme select off the screen. */
        grid-template-columns: repeat(6, minmax(0, 1fr));
        grid-template-rows: repeat(3, 38px);
        gap: 4px 6px;
        padding: 8px 10px;
        align-items: center;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .topbar > .topbar-group,
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .status > *,
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .field,
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .tempo-control {
        min-width: 0;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) #themeSelect,
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) #keySelect {
        width: 100%;
        min-width: 0;
    }
    /* Transport belongs to the Host, so a visitor's first row is the clock. */
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .topbar > .topbar-group:first-child {
        display: none;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .phrase-group {
        grid-column: 1 / -1;
        grid-row: 1;
        gap: 8px;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .topbar > .topbar-group:nth-child(3) {
        grid-column: 1 / 3;
        grid-row: 2;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .topbar > .topbar-group:nth-child(4) {
        grid-column: 3 / 4;
        grid-row: 2;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .topbar > .topbar-group:nth-child(5) {
        grid-column: 4 / 6;
        grid-row: 2;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .topbar > .topbar-group.right,
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .status {
        display: contents;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) #packReadout {
        display: block;
        grid-column: 6 / -1;
        grid-row: 2;
        justify-self: end;
        max-width: 100%;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) #modeLabel {
        display: none;
    }
    /* Row three is who and where: identity, role, room, link, and the way out. */
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) #collabIdentity {
        grid-column: 1 / 3;
        grid-row: 3;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .leave-session {
        padding: 4px 8px;
        font-size: 10px;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) #collabRoleBadge {
        grid-column: 3 / 4;
        grid-row: 3;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) #collabRoomBadge {
        grid-column: 4 / 5;
        grid-row: 3;
        max-width: 100%;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .bridge-wrap {
        grid-column: 5 / 6;
        grid-row: 3;
        justify-self: center;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .leave-session {
        grid-column: 6 / -1;
        grid-row: 3;
        justify-self: end;
    }
    body:is([data-collab-role="viewer"], [data-collab-role="player"]) .collab-own-name {
        max-width: 100%;
    }

    /* --- Producer, and a solo session, which has the same controls ----- */
    body:is([data-collab-role="producer"], :not([data-collab-role])) {
        --topbar-height: 180px;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .topbar {
        display: grid;
        /* Even fractions, not auto tracks. An auto track is sized by the widest
           item in its column across every row, and with this many rows they ate
           the whole width - starving the last column to 0px and dropping Leave
           on top of the room label. Equal shares are predictable instead. */
        grid-template-columns: repeat(5, minmax(0, 1fr));
        grid-template-rows: repeat(4, 38px);
        gap: 4px 8px;
        padding: 8px 10px;
        align-items: center;
    }
    /* Everything in the deck has to be able to shrink. A grid item will not go
       below its content width without min-width: 0, and a fixed-width slider or
       a nowrap button spills into the next column instead of narrowing. */
    body:is([data-collab-role="producer"], :not([data-collab-role])) .topbar > .topbar-group,
    body:is([data-collab-role="producer"], :not([data-collab-role])) .status > *,
    body:is([data-collab-role="producer"], :not([data-collab-role])) .field,
    body:is([data-collab-role="producer"], :not([data-collab-role])) .tempo-control {
        min-width: 0;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .tempo-control input[type="range"] {
        flex: 1 1 0;
        width: auto;
        min-width: 0;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) #themeSelect {
        width: 100%;
        min-width: 0;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) #masterButton,
    body:is([data-collab-role="producer"], :not([data-collab-role])) #packReadout,
    body:is([data-collab-role="producer"], :not([data-collab-role])) #packsButton {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    /* The select already names the theme; the readout beside it is redundant
       here and it is the widest thing competing for row four. */
    body:is([data-collab-role="producer"], :not([data-collab-role])) #themeValue {
        display: none;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) #masterButton {
        padding: 4px 7px;
        font-size: 10px;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .topbar > .topbar-group:first-child {
        grid-column: 1 / 3;
        grid-row: 1;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .phrase-group {
        grid-column: 3 / -1;
        grid-row: 1;
        gap: 8px;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .topbar > .topbar-group:nth-child(3) {
        grid-column: 1 / 3;
        grid-row: 2;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .topbar > .topbar-group:nth-child(4) {
        grid-column: 3 / -1;
        grid-row: 2;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .topbar > .topbar-group.right,
    body:is([data-collab-role="producer"], :not([data-collab-role])) .status {
        display: contents;
    }
    /* Row three: what is loaded, how loud, who you are, what the app is doing.
       packsButton and packReadout share a cell - only one shows per role. */
    /* The pack gives up a column to the name beside the icon: the loaded packs
       are also named across the lane headers below, while a person's name has
       nowhere else to appear. Both ellipsise rather than push the row wider. */
    body:is([data-collab-role="producer"], :not([data-collab-role])) #packsButton,
    body:is([data-collab-role="producer"], :not([data-collab-role])) #packReadout {
        grid-column: 1 / 2;
        grid-row: 3;
        justify-self: stretch;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .master-wrap {
        grid-column: 2 / 3;
        grid-row: 3;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) #collabIdentity {
        grid-column: 3 / 5;
        grid-row: 3;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .collab-own-name {
        max-width: 100%;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) #collabRoleBadge {
        grid-column: 5 / -1;
        grid-row: 3;
        justify-self: end;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) #modeLabel {
        grid-column: 5;
        grid-row: 4;
        justify-self: end;
        display: none;
    }
    /* Row four: link health, look, room, and Leave last. */
    body:is([data-collab-role="producer"], :not([data-collab-role])) .bridge-wrap {
        grid-column: 1;
        grid-row: 4;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .topbar > .topbar-group:nth-child(5) {
        grid-column: 2 / 4;
        grid-row: 4;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) #collabRoomBadge {
        grid-column: 4;
        grid-row: 4;
        max-width: 100%;
    }
    body:is([data-collab-role="producer"], :not([data-collab-role])) .leave-session {
        grid-column: 5;
        grid-row: 4;
        justify-self: end;
    }

    /* Both popups hang off their button with `position: absolute; right: 0`,
       which assumes the button is near the right edge. In the deck it is not -
       the bridge light sits in column one - so a 250px panel anchored to its
       right edge started at -214px and was mostly off screen. Pinned to the
       viewport instead: full width under the bar, wherever its button lives. */
    .master-popup,
    .bridge-popup {
        position: fixed;
        top: calc(var(--topbar-height) + 4px);
        left: 10px;
        right: 10px;
        width: auto;
    }

    /* The segmented phrase meter is the point of the clock row: it reads at a
       glance in a way the bar.beat text does not. It was hidden on phones only
       because the old fixed 130px would not fit - flexed, it does. */
    .phrase-meter {
        display: block;
        flex: 1 1 auto;
        width: auto;
        min-width: 60px;
    }
}

/* APC mini: outline the samples and Clips currently on the controller's grid.
   The left border is left alone - it carries the playing/queued state. */
.clip.apc-window {
    border-top-color: rgba(91, 157, 255, 0.28);
    border-right-color: rgba(91, 157, 255, 0.28);
    border-bottom-color: rgba(91, 157, 255, 0.28);
}
.clip-card.apc-window { outline: 1px solid rgba(91, 157, 255, 0.28); outline-offset: 1px; border-radius: 3px; }

/* Visitor link buttons: stacked full-width under Require approval. The Host
   panel is too narrow to hold both labels side by side. */
.host-panel-tools { flex-wrap: wrap; }
.host-link-buttons { display: grid; flex: 1 1 100%; gap: 6px; }

/* Join QR code popup. The code itself stays black on white whatever the app
   theme: phone cameras read dark-on-light far more reliably. */
.join-qr {
    position: fixed;
    inset: 0;
    z-index: 120;
    display: grid;
    place-items: center;
    padding: 16px;
    background: rgba(5, 7, 10, .72);
}
.join-qr-dialog {
    display: grid;
    gap: 14px;
    justify-items: center;
    width: min(420px, 100%);
    padding: 20px;
    border: 1px solid var(--line-bright);
    border-radius: 8px;
    background: var(--bg-panel);
    box-shadow: 0 16px 40px rgba(0, 0, 0, .42);
    text-align: center;
}
.join-qr-head { display: grid; gap: 3px; }
.join-qr-head strong { font-size: 16px; letter-spacing: .02em; }
.join-qr-head span { color: var(--text-dim); font-size: 12px; }
.join-qr-code { width: min(320px, 100%); aspect-ratio: 1; max-width: 100%; }
.join-qr-code svg { display: block; width: 100%; height: 100%; border-radius: 4px; }
.join-qr-url {
    margin: 0;
    max-width: 100%;
    color: var(--text-dim);
    font-size: 12px;
    overflow-wrap: anywhere;
    user-select: all;
}

/* Hosted (remote profile) builds: themes and the TouchDesigner bridge belong to
   the art installation only. The code stays for the local build; the controls
   are hidden here. See main.mjs (INSTALLATION). */
body.hosted .theme-group,
body.hosted .bridge-wrap {
    display: none;
}

/* Sign-in screen for hosted builds (auth/accountAuth.mjs). Reuses the boot panel look. */
.sign-in-gate { z-index: 1000; }
.sign-in-gate .boot-actions { justify-content: center; }
.sign-in-button { display: inline-block; text-decoration: none; text-align: center; }

/* ProSeq projects: start screen and the save-status pill (project/, ui/projectScreen.mjs). */
.projects-gate { z-index: 60; }
.project-templates { display: grid; gap: 6px; margin-bottom: 14px; }
.project-template .pack-name { color: var(--text); }
.project-status-line.error { color: var(--danger, #ff6b6b); }
.project-status {
    display: inline-flex; align-items: center; gap: 8px; max-width: 280px;
    padding: 4px 10px; border-radius: 999px; border: 1px solid var(--border, #2a2f37);
    background: transparent; color: var(--text-dim); font: inherit; font-size: 12px; cursor: pointer;
}
.project-status .project-title { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.project-status .project-state::before { content: '\25CF'; margin-right: 4px; }
.project-status[data-state="saved"] .project-state::before { color: var(--active, #35d07f); }
.project-status[data-state="dirty"] .project-state::before,
.project-status[data-state="saving"] .project-state::before { color: var(--accent, #f0b429); }
.project-status[data-state="offline"] .project-state::before,
.project-status[data-state="failed"] .project-state::before,
.project-status[data-state="conflict"] .project-state::before { color: var(--danger, #ff6b6b); }

/* Project naming (ui/projectScreen.mjs, main.mjs renameOpenProject). */
.project-name-input {
    width: 100%; box-sizing: border-box; margin-bottom: 8px; padding: 8px 10px;
    border-radius: 6px; border: 1px solid var(--border, #2a2f37);
    background: var(--bg-raised, #161a20); color: var(--text); font: inherit;
}
.project-row { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.project-row .pack-meta { flex: 1; min-width: 0; }
.project-rename { font-size: 11px; padding: 2px 8px; }
.project-status .project-title { text-decoration: underline dotted; cursor: text; }
