/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| APP.CSS — WaveFactory SPA shell
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
*/

/* Font stack imported from the vfisa project. */
@font-face {
    font-family: 'TTNorms';
    src: url('../fonts/TTNormsProNormal.woff2') format('woff2');
    font-weight: 100 500;
    font-display: block;
}
@font-face {
    font-family: 'TTNorms';
    src: url('../fonts/TTNormsProMedium.woff2') format('woff2');
    font-weight: 501 600;
    font-display: block;
}
@font-face {
    font-family: 'TTNorms';
    src: url('../fonts/TTNormsProBold.woff2') format('woff2');
    font-weight: 700 900;
    font-display: block;
}
@font-face {
    font-family: 'MavenPro';
    src: url('../fonts/MavenProMedium.woff2') format('woff2');
    font-display: block;
}
@font-face {
    font-family: 'RobotoMono';
    src: url('../fonts/RobotoMonoMedium.woff2') format('woff2');
    font-display: block;
}

:root {
    --fnt-std: 'TTNorms', -apple-system, Arial, sans-serif;
    --fnt-btn: 'TTNorms', -apple-system, Arial, sans-serif;
    --fnt-hdg: 'MavenPro', -apple-system, Arial, sans-serif;
    --fnt-fix: 'RobotoMono', -apple-system, Arial, sans-serif;

    /* Height of the fixed bottom toast/status rail. Used in two places:
       body.padding-bottom (reserves viewport space so the rail never
       overlaps editor chrome) and .toast-rail.height (the rail itself).
       Bumping this value moves both in lockstep. */
    --toast-rail-h: 28px;

    /* Cursors imported from the original waveform project. */
    --cursor-default:      url('/cursors/default.svg') 11 6, default;
    --cursor-pointer:      url('/cursors/pointer.svg') 6 5, pointer;
    --cursor-pointer-down: url('/cursors/pointer-down.svg') 6 5, pointer;
    --cursor-add-left:     url('/cursors/add-left.svg'), pointer;
    --cursor-add-right:    url('/cursors/add-right.svg'), pointer;

    --bg:          #16181d;
    --panel:       #1d2027;
    --panel-2:     #232730;
    --panel-3:     #2a2f3a;
    --line:        #2f3441;
    --line-soft:   #262a33;
    --text:        #e7e9ef;
    --text-dim:    #aab0bd;
    --muted:       #757b88;
    --accent:      #5aa0ff;
    --accent-2:    #7fbaff;
    --accent-soft: rgba(90,160,255,0.18);
    --danger:      #cb4f4d;
    --select:      #a855f7;   /* selection rings (tracks, clips, bin rows) — keep in sync with the canvas hexes in app.js */

    /* Transport-bar icon tints — each category gets its own colour so the
       button groups read at a glance. */
    --icon-playback: #4ade80;
    --icon-history:  #facc15;
    --icon-edit:     #c084fc;
    --icon-presets:  #fb923c;
    --icon-effects:  #38bdf8;
    --ok:          #4caf50;
    --silence:     #1a1d24;
    --silence-2:   #20242c;
    --ruler:       #1b1e25;
    --ruler-tick:  #444958;
    --ruler-text:  #8189a0;
    --playhead:    #ff5552;
    --topbar-h:    52px;
    --bins-h:      260px;

    /* Studio design tokens — ported so auth / fields / inputs / labels / selects
       share studio's look. Keep names matching studio so future ports stay in sync. */
    --blk-0:   hsl(230, 15%, 32%);
    --blk-1:   hsl(230, 15%, 26%);
    --blk-2:   hsl(230, 15%, 22%);
    --blk-3:   hsl(230, 15%, 15.5%);
    --blk-4:   hsl(230, 15%, 12.5%);
    --wht-1:   hsl(30, 35%, 88%);
    --wht-2:   hsl(30, 20%, 75%);
    --wht-3:   hsl(30, 5%, 60%);
    --blu-1:   hsl(215, 100%, 70%);
    --red-1:   hsl(0, 60%, 55%);

    --btn-hgt: 40px;
    --rad-1:   4px;
    --rad-3:   11px;
    --siz-0:   13px;
    --siz-1:   14px;
    --siz-2:   17px;
}

* { box-sizing: border-box; cursor: var(--cursor-default); }
a, button, [role="tab"] { cursor: var(--cursor-pointer); }
a:active, button:active, [role="tab"]:active { cursor: var(--cursor-pointer-down); }

/* Scrollbars — tinted to the editor palette so they recede into the panels
   instead of using the OS default white/light theme. Applied globally so the
   timelines column, bin bodies, and modal scroll regions all match. */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--panel-3) transparent;
}

::-webkit-scrollbar         { width: 8px; height: 8px; }
::-webkit-scrollbar-track   { background: transparent; }
::-webkit-scrollbar-thumb   { background: var(--panel-3); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--line); }
::-webkit-scrollbar-corner  { background: transparent; }

/* All UI text is non-selectable — drag gestures (clips, fade nodes, ruler
   scrubs) were leaving accidental text highlights across labels and
   toolbars. Editable surfaces opt back in just below. */
body {
    user-select: none;
    -webkit-user-select: none;
}

input, textarea, [contenteditable="true"] {
    user-select: text;
    -webkit-user-select: text;
}

html, body {
    margin: 0;
    padding: 0;
    height: 100%;
    background: var(--bg);
    color: var(--text);
    font: 400 14px/1.4 var(--fnt-std);
    letter-spacing: 0.01ch;
    overflow: hidden;
}

body {
    display: flex;
    flex-direction: column;
    /* Reserve viewport space at the bottom equal to the fixed toast-rail
       height (.toast-rail is position: fixed; bottom: 0). Without this
       padding the rail visually overlaps whatever happens to land at the
       editor's bottom edge — usually the transport bar, sometimes the
       bins row — so the user can lose part of those surfaces every time
       a toast is showing AND the layout is tight. The padding shrinks
       the flex column's content area instead, so the editor (and its
       transport bar at the bottom of the column) sits cleanly above the
       rail no matter what. --toast-rail-h is the single knob for the
       reservation height; .toast-rail uses the same variable for its
       own `height` so the two stay in lockstep. */
    padding-bottom: var(--toast-rail-h);
}

/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| HEADER
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
*/
header {
    /* 3-column grid: left (title), centre (project actions), right (user
       settings). 1fr / auto / 1fr keeps the centre group geometrically
       centred no matter how wide the title or the right-hand controls are. */
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 16px;
    /* Matches .bin-tabs (12px all sides) so the page header and the bin's
       tab strip read as a paired pair of toolbars across the layout. */
    padding: 12px;
    background: var(--panel);
    border-bottom: 1.5px solid var(--line);
    flex: 0 0 auto;
}

.header-info {
    min-width: 0;
    overflow: hidden;
    /* Logo + project name on the same row, vertically centered. The
       project-name truncates with ellipsis if it would overflow. */
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 12px;
}

.brand-logo {
    width: 130px;
    height: auto;
    display: block;
}

.header-actions {
    display: flex;
    align-items: center;
    /* +50% — was 10px */
    gap: 15px;
    justify-self: center;
}

.header-auth {
    display: flex;
    align-items: center;
    gap: 10px;
    justify-self: end;
}

.auth-user-label {
    color: var(--text-dim);
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    user-select: none;
}

.auth-user-label:empty { display: none; }

h1, h2, h3 {
    font-family: var(--fnt-hdg);
}

h1 {
    margin: 0;
    font-size: 18px;
    letter-spacing: 0.2px;
    display: flex;
    align-items: baseline;
    gap: 10px;
    min-width: 0;
    color: var(--text);
}

.project-name {
    /* Inline next to the logo (.header-info is a row flex). Ellipsis-truncates
       when the project name is longer than the available space. */
    color: var(--text-dim);
    font-size: 13px;
    font-weight: 400;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.project-name:empty { display: none; }

/* Holding Shift swaps the human-readable name for the project's opaque id —
   handy when copy-pasting a project reference. Uses the body.shift-held flag
   already maintained by the Party-tab reveal logic. .pn-id is monospace and
   slightly dimmer so the format-shift is unmistakable. */
.project-name .pn-id { display: none; font-family: "SF Mono", ui-monospace, Menlo, monospace; letter-spacing: 0.5px; }
body.shift-held .project-name .pn-name { display: none; }
body.shift-held .project-name .pn-id   { display: inline; }

.controls {
    display: flex;
    flex-wrap: nowrap;
    gap: 14px;
    align-items: center;
    justify-content: flex-end;
    margin: 0;
}

.btn-group {
    display: inline-flex;
    align-items: center;
    width: fit-content;
    /* Buttons in the same category sit edge-to-edge with no gap — the
       per-button corner-radius rules below strip the radii from the
       touching sides so the cluster reads as one segmented control
       with rounded ends only. */
    gap: 0;
    background: transparent;
    padding: 0;
    border-radius: 8px;
}

/* Horizontal btn-groups (default): only the first child keeps its
   left-side radii, only the last keeps its right-side radii. Middle
   children flatten on both horizontal sides. Applies recursively via
   the `> :is(button, .btn)` direct-child selector so nested wrappers
   (rare) don't accidentally pull the rule. */
.btn-group > :is(button, .btn):not(:first-child) {
    border-top-left-radius:    0;
    border-bottom-left-radius: 0;
}

.btn-group > :is(button, .btn):not(:last-child) {
    border-top-right-radius:    0;
    border-bottom-right-radius: 0;
}

/* Vertical btn-groups (side-toolbar stacks them in a column) swap the
   axis: first child keeps top corners, last keeps bottom. The more-
   specific `.side-toolbar .btn-group` selector wins the cascade over
   the horizontal rule above. */
.side-toolbar .btn-group > :is(button, .btn):not(:first-child) {
    border-top-left-radius:    0;
    border-top-right-radius:   0;
    /* Restore the horizontal-axis radii cleared by the parent rule —
       in a column stack only the vertical edges touch a sibling.
       Matches the doubled base button radius (8px). */
    border-bottom-left-radius: 8px;
}

.side-toolbar .btn-group > :is(button, .btn):not(:last-child) {
    border-bottom-left-radius:  0;
    border-bottom-right-radius: 0;
    border-top-right-radius:    8px;
}

button img.icon {
    width: 18px;
    height: 18px;
    display: block;
    pointer-events: none;
}

/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| CARD (toolbar + content) — used by upload.php
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
*/
.card {
    flex: 1 1 auto;
    display: flex;
    flex-direction: row;
    align-items: stretch;
    min-height: 0;
    overflow: hidden;
}

/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| EDITOR — index.php layout
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| Vertical stack of full-width timelines + transport bar, then a horizontal
| bins-row whose left edge holds the side toolbar (bin-icon tabs + project
| actions) and right side holds the bin bodies. The side toolbar's top edge
| aligns with the top of the bins, not the top of the editor — so timelines
| and transport extend fully to the left.
*/
.editor {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
}

/* Horizontal slot inside the editor that holds the scrollable timelines
   column on the left and the master peak limiter strip on the right.
   Sized to fit its content (the #timelines column's max-height:
   fit-content + the row's own padding) so the row's bottom matches the
   playhead line and the meter strip doesn't dangle below the lanes. */
.timelines-row {
    flex: 0 0 auto;
    min-height: 0;
    display: flex;
    flex-direction: row;
}

.peak-limiter {
    flex: 0 0 48px;
    background: #10131a;
    position: relative;
    box-sizing: border-box;
    border-left: 1px solid var(--line);    /* separates the meter strip from the timelines column on the left */
}

/* Out-of-flow so the canvas's HTML width/height attrs (set by JS during peak
   sizing) don't act as intrinsic dimensions that pin .peak-limiter — and by
   extension .timelines-row — at the previous tallest height. With absolute
   positioning, the row's cross-axis is driven by #timelines alone, so
   deleting a lane collapses the row to match. */
#peak-limiter-canvas {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

/* The timelines column above is capped at its intrinsic content size
   (see #timelines max-height), so the bins-row grows to fill every pixel
   of remaining viewport. A 320px floor keeps the bins usable on short
   viewports where there's no slack left to grow into. */
.bins-row {
    flex: 1 1 0;
    min-height: 320px;
    display: flex;
    flex-direction: row;
    overflow: hidden;
}

/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| LAYOUT TOGGLES
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| Two independent body flags set by the header's layout buttons:
|   .bins-hidden      — hide the bins row, let the timeline fill the editor
|   .waveform-compact — halve each lane's canvas height
| Default (neither flag): bins visible with a 2-channel timeline cap so the
| bins below get vertical room for drag-and-drop importing, and full-height
| waveform lanes.
*/

/* Default — bins visible, cap the timeline at 2 channels (instead of the
   default 3) so the bins row underneath has predictable space. The cap
   disappears the moment .bins-hidden takes over. */
body:not(.bins-hidden) #timelines {
    --max-channels: 2;
    max-height: calc(
        28px + 1px
        + var(--max-channels) * (1rem + var(--lane-h))
        + 1rem
    );
}

/* Bins hidden — drop the bins row entirely, let the timelines row stretch
   to fill the editor's vertical space, and remove the per-channel max-
   height cap so the lane stack can pack at the top. Lanes stay content-
   sized (see .timeline-lane above); the column's overflow-y handles taller
   stacks. The transport bar stays put in the .editor flex column so it
   lands visually at the bottom of the editor surface. */
body.bins-hidden .bins-row {
    display: none;
}

body.bins-hidden #timelines {
    max-height: none;
}

body.bins-hidden .timelines-row {
    flex: 1 1 auto;
}

/* Compact waveform — halve the canvas baseline so each lane reads at
   half height. The waveform's bipolar shape squashes into the smaller
   space (the simplest "top half visible" interpretation); --lane-h
   inherits the new --canvas-h since the calc uses var(--canvas-h). */
body.waveform-compact #timelines {
    --canvas-h: 50px;
}

.timelines-row > #timelines {
    flex: 1 1 0;
    min-width: 0;
    min-height: 0;
    /* No height cap — lanes are allowed to overflow vertically. The column's
       own overflow-y: auto kicks in when the content exceeds the available
       viewport space, so the user can scroll to reach lower lanes without
       the column eating into the bins area indefinitely. */
}

.bins-row > .bins {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
}

.side-toolbar {
    display: flex;
    flex-direction: column;
    /* +50% — was 14px. Between btn-groups so categories read as distinct
       clusters; the transport bar shares this rhythm. */
    gap: 21px;
    flex-shrink: 0;
    align-self: stretch;
    padding: 12px;
    background: var(--panel);
    border-right: 1.5px solid var(--line);
    z-index: 11;
}

.side-toolbar .btn-group {
    flex-direction: column;
    align-items: center;
    /* Edge-to-edge in side-toolbar too; the .side-toolbar-specific
       radii overrides above handle the corner flattening per-axis. */
    gap: 0;
}

.card-content {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
    display: grid;
    /* timelines sized to content (2 channels max), transport bar auto, bins fills the rest. */
    grid-template-rows: auto auto 1fr;
    overflow: hidden;
}

/* Upload shell has only the bins as a child of .card-content — collapse the
   three-row grid so the bins stretch to fill the available height. */
body.upload-page .card-content { grid-template-rows: 1fr; }

/* Upload-page Clips bin: stack cards as full-width rows instead of the editor's
   grid of tiles. A row layout reads as a list of "uploaded files" which matches
   what the Creator is doing on this page. */
body.upload-page .bin-body[data-bin="clips"].active {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

body.upload-page .clip-card {
    flex-direction: row;
    align-items: center;
    gap: 14px;
    padding: 10px 14px;
}

body.upload-page .clip-card .name {
    flex: 1 1 auto;
    min-width: 0;
}

body.upload-page .clip-card .meta {
    flex: 0 0 auto;
    /* Push past the absolute-positioned delete button so the duration text
       isn't covered when the delete affordance fades in on hover. */
    margin-right: 26px;
}

body.upload-page .clip-card .clip-card-delete {
    top: 50%;
    transform: translateY(-50%);
}

/* Processing / failed visual state on the Creator's bin rows. Same row layout
   but dimmed + a status word in place of the duration, so the Creator can see
   at a glance which files still need worker attention before handing the
   project off to the Client / Editor. */
/* Pending / failed placeholder states for clip cards in BOTH the editor and
   the upload shell. A clip is shown as soon as its CLIPS row lands so the
   user sees the import happened, but it's dimmed + tagged Processing… (or
   Failed) and dragging is disabled (renderClipsBin attaches aria-disabled
   and intercepts dragstart) until the transcode worker flips previewStatus
   to 'ready'. The card un-dims automatically on the next previewStatusPoll
   tick. */
.clip-card.processing             { opacity: 0.55; cursor: progress; }
.clip-card.processing .thumb      { opacity: 0.35; }
.clip-card.processing .meta       { color: var(--wht-3); }
.clip-card.failed                 { opacity: 0.75; }
.clip-card.failed .meta           { color: var(--red-1, var(--danger)); }

/* Align the "Clips" heading's text with the clip-card name text underneath.
   Both stacks add up to the same 24px inset (bin padding 10 + card padding 14,
   matched by bin-tabs padding 10 + bin-tab padding 14). */
body.upload-page .bin-tabs { padding-left: 10px; }

/* Project list shown in the Open-project modal. A borderless table — rows are
   clickable, the .current row uses colour (not a border) to mark the project
   the editor / upload shell is already pointing at. */
.project-list {
    max-height: 50vh;
    overflow-y: auto;
}

.project-table {
    width: 100%;
    border-collapse: collapse;
    border: none;
    font-family: var(--fnt-std);
    font-size: var(--siz-1);
    color: var(--wht-1);
}

.project-table td {
    border: none;
    padding: 10px 14px;
    cursor: var(--cursor-pointer);
}

.project-table tr.project-row:hover td { background: hsl(230, 15%, 22%); }

.project-table tr.project-row.current td       { color: var(--accent-2); }
.project-table tr.project-row.current .name    { font-weight: 600; }

.project-table td.name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 0; /* with width: 100% + table-layout: auto, lets the cell flex while still ellipsising */
    width: 100%;
}

.project-table td.meta {
    color: var(--wht-3);
    font-size: 12px;
    text-align: right;
    white-space: nowrap;
}

.project-list .project-empty {
    padding: 20px;
    color: var(--wht-3);
    text-align: center;
}

.transport-bar {
    position: relative;
    /* Never shrink: the editor is a flex column, and without an explicit
       `flex: 0 0 auto` the default `flex: 0 1 auto` lets the browser
       compress the transport row when the editor runs short on vertical
       space (lots of channels + tall bins-row + a small viewport). Pin
       it to its intrinsic height so every transport button stays fully
       visible — the bins-row absorbs the squeeze instead (it has its
       own min-height floor + internal overflow). */
    flex: 0 0 auto;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    /* 21 px matches the .bar-side gap so cross-side spacing (bar-left ↔
       bar-right) reads identically to within-side spacing (Channel ↔
       Playback ↔ Snap ↔ …). Every button-category gap looks the same. */
    gap: 21px;
    padding: 12px;
    background: var(--panel);
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
}

.bar-side {
    display: flex;
    align-items: center;
    gap: 21px;
}

/* Layout: bar-left + bar-right flow naturally with equal category gaps;
   the time-readout gets pushed to the far right via margin-left:auto so
   the edit buttons sit just to the left of the readout, not flush
   against the right edge. */
.transport-bar .time-readout {
    margin-left: auto;
}

.transport-bar .btn-group {
    /* Edge-to-edge; the base .btn-group radii overrides handle the
       corner flattening for adjacent buttons. */
    gap: 0;
}

.transport-bar .time-readout {
    /* Sits at the right edge of the transport bar, after bar-right.
       No elastic growth — its natural intrinsic width is what shows. */
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: 'RobotoMono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
    font-size: 18px;
    color: var(--text);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.04em;
}

.transport-bar .time-readout .time-sep {
    /* Thin vertical divider between the playhead and total-duration readouts. */
    width: 1.5px;
    align-self: stretch;
    background: rgba(255, 255, 255, 0.18);
    border-radius: 1px;
}

.transport-bar .duration-editable {
    cursor: var(--cursor-pointer);
    padding: 0 4px;
    border-radius: 4px;
    outline: none;
    transition: background 0.12s;
}

.transport-bar .duration-editable:hover {
    background: rgba(255, 255, 255, 0.08);
}

.transport-bar .duration-editable[contenteditable="true"] {
    background: rgba(90, 160, 255, 0.15);
    user-select: text;
    cursor: text;
}


.btn, button {
    appearance: none;
    /* Transparent border at rest — keeps the 1.5px of layout space so
       buttons don't jump when the active / pressed state colours the
       border in. button[aria-pressed="true"] and other "active" rules
       (e.g. .bin-tab.active, lane-mute-btn[aria-pressed="true"]) set
       the border-color to make the outline visible. */
    border: 1.5px solid transparent;
    background: rgba(255,255,255,0.06);
    color: var(--text);
    padding: 0 12px;
    height: 38px;
    border-radius: 8px;
    font-family: var(--fnt-btn);
    font-size: 13px;
    transition: background 120ms ease, transform 80ms ease, opacity 120ms ease, border-color 120ms ease;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    white-space: nowrap;
    line-height: 1;
}

/* Every icon-only button is square (width == height). Catches plain <button>
   elements that hold just an <img class="icon"> — covers the side-toolbar,
   the transport bar, and any other markup where the .icon-btn class wasn't
   added explicitly. */
button:has(> img.icon) {
    width: 38px;
    padding: 0;
}

.btn:hover, button:hover {
    background: rgba(255,255,255,0.1);
}

.btn:active, button:active {
    transform: scale(0.98);
}

/* Strip the browser's default focus ring after a mouse click — looks like
   a stuck outline lingering on the pressed button. :focus-visible is
   left intact so keyboard navigation still gets a visible focus
   indicator. */
.btn:focus:not(:focus-visible),
button:focus:not(:focus-visible) {
    outline: none;
}

/* Disabled buttons keep their border outline; the parent `opacity: 0.5`
   dims background, border, and icon together so they read as one unit. */
.btn:disabled,
.btn:disabled:hover,
button:disabled,
button:disabled:hover {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Pressed-state: every ON button gets the same blue accent outline + a soft
   blue tint, regardless of the button's own --icon-tint. Per-button colour
   coding (lane mute/solo/FX/etc.) can still opt back in by overriding
   border-color on its own aria-pressed rule further down. */
button[aria-pressed="true"] {
    background: color-mix(in srgb, var(--accent) 18%, transparent);
    border-color: var(--accent);
    color: var(--text);
}

/* Loading state — toggled by withLoading() around any async click handler that
   fires an /api call. The icon is hidden and a 1.5px-stroke spinner takes its
   place until the request settles. */
button.is-loading {
    pointer-events: none;
    position: relative;
}

button.is-loading > .icon { visibility: hidden; }

button.is-loading::after {
    content: '';
    position: absolute;
    inset: 0;
    margin: auto;
    width: 16px;
    height: 16px;
    /* Match the Lucide icon stroke colour (#e7ecf3) so the spinner reads as the
       same mark as the icon it's replacing, regardless of button background. */
    border: 1.5px solid #e7ecf3;
    border-top-color: transparent;
    border-radius: 50%;
    animation: btn-spin 0.7s linear infinite;
}

@keyframes btn-spin { to { transform: rotate(360deg); } }

/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| TRANSPORT-BAR ICON TINTS
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| The transport-bar buttons paint their icon as a CSS mask over a category-
| coloured ::before layer, hiding the underlying <img>. This keeps the SVG
| markup untouched while letting each category (playback / history / edit)
| carry its own colour. Each button only needs to declare --icon-src and
| --icon-tint; the shared rule does the rest.
*/
:is(#add-channel-btn,
    #play-btn, #rewind-btn, #forward-btn, #fit-btn, #goto-playhead-btn, #snap-btn,
    #quietmap-btn,
    #slice-btn, #merge-btn, #delete-btn,
    #bins-toggle-btn, #compact-toggle-btn,
    #new-btn, #open-btn, #settings-btn, #archive-btn, #project-delete-btn) {
    position: relative;
}

:is(#add-channel-btn,
    #play-btn, #rewind-btn, #forward-btn, #fit-btn, #goto-playhead-btn, #snap-btn,
    #quietmap-btn,
    #slice-btn, #merge-btn, #delete-btn,
    #bins-toggle-btn, #compact-toggle-btn,
    #new-btn, #open-btn, #settings-btn, #archive-btn, #project-delete-btn)::before {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-color: var(--icon-tint);
    -webkit-mask: var(--icon-src) center / 18px 18px no-repeat;
            mask: var(--icon-src) center / 18px 18px no-repeat;
}

:is(#add-channel-btn,
    #play-btn, #rewind-btn, #forward-btn, #fit-btn, #goto-playhead-btn, #snap-btn,
    #quietmap-btn,
    #slice-btn, #merge-btn, #delete-btn,
    #bins-toggle-btn, #compact-toggle-btn,
    #new-btn, #open-btn, #settings-btn, #archive-btn, #project-delete-btn) > img.icon {
    /* Hide the raw image — the ::before mask provides the visible icon. */
    visibility: hidden;
}

/* The loading-state spinner replaces every icon source, so also suppress the
   mask layer while a button is in flight. */
button.is-loading::before { visibility: hidden; }

/* Non-effect transport buttons share a single neutral tint — only effect
   buttons (fades + volume change) carry a colour, and theirs matches the
   colour the effect renders with on the per-lane effect strip. */
#add-channel-btn { --icon-src: url('../icons/list-plus.svg'); --icon-tint: #fff; }
#play-btn    { --icon-src: url('../icons/play.svg');         --icon-tint: #fff; }
#rewind-btn  { --icon-src: url('../icons/skip-back.svg');    --icon-tint: #fff; }
#forward-btn { --icon-src: url('../icons/skip-forward.svg'); --icon-tint: #fff; }
#fit-btn     { --icon-src: url('../icons/fullscreen.svg');             --icon-tint: #fff; }
#goto-playhead-btn { --icon-src: url('../icons/between-vertical-start.svg'); --icon-tint: #fff; }
#snap-btn    { --icon-src: url('../icons/magnet.svg');                 --icon-tint: #fff; }
#quietmap-btn { --icon-src: url('../icons/flame.svg');                  --icon-tint: #fff; }

#slice-btn  { --icon-src: url('../icons/scissors.svg');             --icon-tint: #fff; }
#merge-btn  { --icon-src: url('../icons/squares-unite.svg');        --icon-tint: #fff; }
#delete-btn { --icon-src: url('../icons/trash-2.svg');              --icon-tint: #fff; }

/* Header — all icons share the neutral text tint, matching the transport
   bar. Button grouping is conveyed by the .btn-group containers and
   spacing alone, not colour. */
#new-btn      { --icon-src: url('../icons/file.svg');         --icon-tint: #fff; }
#open-btn     { --icon-src: url('../icons/folder-open.svg');  --icon-tint: #fff; }
#settings-btn { --icon-src: url('../icons/settings.svg');     --icon-tint: #fff; }

#archive-btn        { --icon-src: url('../icons/folder-archive.svg'); --icon-tint: #fff; }
#project-delete-btn { --icon-src: url('../icons/trash-2.svg');        --icon-tint: #fff; }

#bins-toggle-btn    { --icon-src: url('../icons/panel-bottom-dashed.svg');       --icon-tint: #fff; }
#compact-toggle-btn { --icon-src: url('../icons/list-chevrons-down-up.svg');     --icon-tint: #fff; }

/* Inside the bin-tabs row the SSH toggle owns the right-edge push so it sits
   immediately to the left of the Import button instead of clinging to the bin
   tabs. .bin-import-btn also has margin-left:auto, which would split the free
   space between the two — overriding it here lets the SSH button absorb all
   leftover space and Import slot in flush against it. Sized to match the
   .bin-import-btn padding so both read as a single coherent action pair. */
.bin-tabs #bridge-toggle-btn { margin-left: auto; padding: 0 14px; font-size: 13px; letter-spacing: 0.5px; }
.bin-tabs .bin-import-btn    { margin-left: 6px; }

.btn.primary {
    background: var(--accent);
    border-color: var(--accent);
    color: #0f1419;
}

.btn.primary:hover {
    background: var(--accent-2);
    border-color: var(--accent-2);
}

.btn.icon-btn,
.icon-btn {
    width: 38px;
    padding: 0;
    font-size: 14px;
}

.time-readout {
    font-family: var(--fnt-fix);
    font-size: 12px;
    color: var(--text);
    padding: 0 8px 0 12px;
    min-width: 84px;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

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

.zoom-control input[type=range] {
    width: 100px;
    accent-color: var(--accent);
}

.zoom-label { user-select: none; }

/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| TIMELINES
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
*/
.timelines {
    background: var(--bg);
    overflow-x: hidden;
    overflow-y: auto;
    /* No top padding so the ruler sits flush with the page header above.
       No right padding so lane content runs flush against the peak meter's
       left border. No left padding so each channel lane (and the ruler)
       runs flush to the column's left edge. */
    padding: 0 0 1rem 0;
    display: flex;
    flex-direction: column;
    gap: 1rem;
    position: relative;
    border-left: 1px solid var(--line);
    /* Width of the per-lane channel-label column on the left of each lane.
       The ruler's padding-left and the playhead/range overlays both factor
       this in so the time axis stays aligned with the waveform canvases. */
    /* Wide enough to fit the channel meta as TWO rows under the name:
         Row 1: MUTE | SOLO | gear        — routing controls
         Row 2: FX | DRC | NOR | NR       — signal-processing toggles
       At the existing button sizing (10 px monospace bold, 6 px side
       padding) the single button row is M / S / FX / wand — 4 ms-btns
       (~24 px each) + 3×4 px gaps ≈ 108 px. Add 2×12 px header inset =
       ~132 px minimum; rounded up so the channel-name label above still has
       a little breathing room before it ellipsises. */
    --label-col-w: 144px;
    /* Lane geometry — each .timeline-lane is content-sized to
       2×--lane-pad-y + --canvas-h. Bumping --canvas-h is the single
       knob for tightening or relaxing the audio waveform's vertical
       footprint. */
    --canvas-h:    101px; /* 10% reduction from the previous 112px floor */
    --lane-pad-y:  12px;
    --lane-h:      calc(2 * var(--lane-pad-y) + var(--canvas-h));
    /* Cap the timelines column to fit at most 3 lanes (plus the ruler,
       inter-item gaps, and the bottom padding) before vertical scroll
       kicks in. Extra lanes overflow into the existing overflow-y: auto. */
    --max-channels: 3;
    max-height: calc(
        28px                                                /* ruler canvas (RULER_HEIGHT in app.js) */
        + 1px                                               /* ruler bottom border */
        + var(--max-channels) * (1rem + var(--lane-h))      /* 1rem gap before each lane + the lane itself */
        + 1rem                                              /* bottom padding */
    );
}

#playhead-line {
    position: absolute;
    top: 0;
    /* Height is set in JS to #timelines.scrollHeight so the line spans
       every channel even when the column scrolls past the 3-channel cap.
       bottom: 0 would only reach the viewport bottom; absolute children
       of an overflow:auto container are clipped to the visible padding
       box, not the full scroll content. */
    width: 0;
    border-left: 1.5px solid #ff5552;
    pointer-events: none;
    z-index: 5;
}

/* Range selection overlay. Set by shift+click on the ruler — anchors at
   the current playhead and extends to the clicked second. Sits behind
   the playhead line but above the lane canvases so the tint reads as
   a banded selection across every channel. */
#timeline-range {
    position: absolute;
    top: 0;
    /* Same reason as #playhead-line — JS sets height to scrollHeight so
       the range tint spans every channel through the scrollable column. */
    display: none;
    background: rgba(74, 144, 226, 0.18);
    border-left:  1px solid rgba(74, 144, 226, 0.7);
    border-right: 1px solid rgba(74, 144, 226, 0.7);
    /* Interactive, unlike the playhead line — the band doubles as a drag
       handle for sliding the whole range along the timeline (width kept).
       The ruler is a later same-z sibling, so clicks on the ruler still
       win where the band crosses it. */
    pointer-events: auto;
    cursor: var(--cursor-pointer);
    z-index: 4;
}

#timeline-range:active { cursor: var(--cursor-pointer-down); }

/* Per-lane duration readout sitting at the centre of the range overlay's
   currently-visible portion. Same font + size as the ruler text so the
   two read as one typographic family. JS sets both `top` (per lane) and
   `left` (centre of visible overlay window); the translate(-50%, -50%)
   pulls the box to centre on those coordinates so the text is balanced
   around the chosen point in both axes. */
.range-duration-label {
    position: absolute;
    transform: translate(-50%, -50%);
    font: 10px RobotoMono, -apple-system, Arial, sans-serif;
    color: var(--text);
    white-space: nowrap;
    pointer-events: none;
}

.timeline-ruler {
    flex: 0 0 auto;
    cursor: pointer;
    background: #1b1e25;
    /* Spans flush edge-to-edge of the timelines column (which has no
       horizontal padding now). Top stays at 0 — #timelines has no top
       padding either, so the ruler sits flush with the page header above.
       Left padding reserves space for each lane's channel-label column so
       the ruler ticks align with the waveform canvases (which start after
       their label gutter). */
    margin: 0;
    padding: 0 0 0 var(--label-col-w);
    border-bottom: 1px solid var(--line);
    /* Stays pinned to the top of the scrolling #timelines column so the
       time markers remain visible when more than 3 lanes scroll. Z-index
       sits above the lane content but below #playhead-line (z-index 5)
       so the red line continues to read across the ruler band. */
    position: sticky;
    top: 0;
    z-index: 4;
}

.timeline-ruler canvas {
    display: block;
}

.timeline-ruler canvas {
    display: block;
}

.timeline-lane {
    position: relative;
    /* Content-sized lane — no fixed height. The lane is exactly tall
       enough to fit its tallest child (.lane-content = canvas-wrap =
       var(--canvas-h), plus the content column's own padding).
       Predictably equals var(--lane-h), which is still used for the
       importing-mode #timelines max-height calc.
       Vertical padding lives on .lane-content so the .lane-header (a
       row sibling with align-self: stretch) can fill the lane's full
       height edge-to-edge — no padding gap above / below the meta
       gutter. */
    flex: 0 0 auto;
    display: flex;
    flex-direction: row;
    align-items: stretch;
    padding: 0;
    /* Match the page header's panel fill so each channel row reads as a
       coherent surface distinct from the darker .timelines background. */
    background: var(--panel);
    /* no within-lane gap — effect lane sits flush against its audio canvas, matching the zero gap between the effect rows inside the effect lane */
}

/* Right-hand column inside each lane — stacks the effect lane and the
   audio canvas vertically while the channel-label sits in the left
   gutter. min-width: 0 lets the flex child shrink so the canvas inside
   honours the available width instead of overflowing. Vertical
   padding lives here (instead of on .timeline-lane) so the meta
   gutter can stretch the full lane height. */
.lane-content {
    flex: 1 1 auto;
    min-width: 0;
    padding: var(--lane-pad-y) 0;
    display: flex;
    flex-direction: column;
}

/* Channel name strip — sits to the LEFT of each lane's content column,
   showing the channel name as a bin-tab-style pill. Dblclick opens the
   channel modal. Horizontal padding gives breathing room between the
   pill and both the lane's left edge and the waveform canvas. min-width:
   0 lets the inner pill shrink under its max-width: 100% so long names
   ellipsise inside the column instead of pushing the canvas right.
   position: sticky + left: 0 pins the header to the left edge of the
   scrolling #timelines column so its right edge always sits at the
   timeline canvas boundary even if the column ever scrolls horizontally.
   The lane's --panel background sits underneath, but the header carries
   its own opaque fill so canvas content can never bleed under it. */
.lane-header {
    flex: 0 0 var(--label-col-w);
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    /* Stretch to fill the lane's cross-axis (full inner lane height) so
       justify-content: center can actually centre the name + M/S row
       vertically. Without align-self: stretch the header would be
       content-sized and sit at the top via the lane's align-items: flex-start. */
    align-self: stretch;
    justify-content: center;
    gap: 12px;
    padding: 0 12px;
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--panel);
}

/* Plain text label sitting inside the meta gutter — no pill chrome, no
   border, no fill. The <button> element is kept for the dblclick handler
   and keyboard semantics but is visually stripped to a text affordance.
   Text-only hover brightens the colour so the click target still gives
   feedback without the old hover-lift box. */
.lane-header .lane-name {
    flex: 0 0 auto;
    max-width: 100%;
    /* Reset the global button { } base (height 38px, radius 4px,
       padding 0 12px, border, background) so the label reads as plain
       text inside the meta gutter. */
    height: auto;
    border-radius: 0;
    padding: 0;
    background: none;
    color: var(--text-dim);
    border: 0;
    display: inline-flex;
    align-items: center;
    justify-content: flex-start;
    text-align: left;
    font: inherit;
    font-size: 12px;
    font-weight: 500;
    outline: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    cursor: pointer;
    transition: color 0.12s;
}

.lane-header .lane-name:hover {
    color: var(--text);
}

/* Column wrapper for the two button rows that sit under the channel
   name. The lane-header itself is a column with a 12 px gap, which is
   the right separation between the name and the button cluster — but
   too much between the two rows themselves. The stack gives the rows
   a tight 4 px internal gap while letting the outer 12 px gap separate
   the whole cluster from the name. Left-aligned via flex-start so the
   stack hugs the gutter's 12 px left inset, lining up flush with the
   channel-name label above. */
.lane-header .lane-ms-stack {
    display: flex;
    flex-direction: column;
    gap: 4px;
    align-self: flex-start;
}

/* Row that holds a single horizontal band of channel-meta buttons
   under the channel name. Two of these stack inside .lane-ms-stack —
   the routing row (MUTE / SOLO / gear) and the signal-processing row
   (FX / DRC / NOR / NR). Gap spaces the buttons; align-self: flex-start
   left-aligns each row inside the stack so both rows start at the same
   X as the channel-name label above — the routing row (shorter) ends
   early on the right rather than floating into the middle. */
.lane-header .lane-ms-row {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 4px;
    align-self: flex-start;
}

/* Per-channel M / S buttons that sit beneath the channel name in the
   meta gutter. Compact 20×20 pills — smaller than the transport bar's
   38px chrome so two of them fit comfortably in the gutter. They
   override the global button { } base (height 38px, radius 4px,
   padding) the same way .ms-btn does in the transport bar. */
.lane-header .lane-ms-btn {
    flex: 0 0 auto;
    /* Consistent horizontal padding around the label, with a min-width
       floor so single-glyph buttons (M / S) still read as square pills
       rather than shrink-wrapping the letter. Multi-glyph labels (FX,
       NR, NOR, DRC) grow to fit while keeping the same breathing room
       on each side. */
    min-width: 24px;
    height: 24px;
    padding: 0 6px;
    /* Transparent at rest — lane-mute-btn[aria-pressed="true"] /
       lane-solo-btn[aria-pressed="true"] / .lane-settings-btn override
       this when each control needs to mark itself active. */
    border: 1px solid transparent;
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.04);
    color: var(--text-dim);
    font-family: var(--fnt-fix);
    font-size: 10px;
    font-weight: 600;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: color 0.12s, border-color 0.12s, background 0.12s;
}

.lane-header .lane-ms-btn:hover {
    color: var(--text);
    background: rgba(255, 255, 255, 0.1);
}

/* Pressed states — different accent per role so a glance tells you
   whether a channel is muted vs. soloed: red for mute (silenced),
   yellow for solo (singled out). Background carries the same hue at
   low opacity so the pill reads as filled, not just outlined. */
.lane-header .lane-mute-btn[aria-pressed="true"] {
    background: rgba(239, 68, 68, 0.18);
    border-color: #ef4444;
    color: var(--text);
}

.lane-header .lane-solo-btn[aria-pressed="true"] {
    background: rgba(244, 197, 66, 0.18);
    border-color: #f4c542;
    color: var(--text);
}

/* Settings cog inside the M / S row — opens the channel-management
   modal. Uses the same CSS-mask pattern as the transport-bar icon
   buttons, scaled down to 12px to fit the 20×20 pill. */
.lane-header .lane-settings-btn {
    position: relative;
    --icon-src:  url('../icons/settings.svg');
    --icon-tint: #fff;
}

.lane-header .lane-settings-btn::before {
    content: '';
    position: absolute;
    inset: 0;
    background-color: var(--icon-tint);
    /* Mask sized to ~half the (now 24px) button so the cog has the same
       optical weight as the M / S glyphs alongside it. */
    -webkit-mask: var(--icon-src) center / 14px 14px no-repeat;
            mask: var(--icon-src) center / 14px 14px no-repeat;
    pointer-events: none;
}

.lane-header .lane-settings-btn:hover {
    --icon-tint: #fff;
}

/* Wand — opens the shared effects modal. Same CSS-mask pattern as the
   old cog so the icon weight stays consistent across lane meta. */
.lane-header .lane-fx-modal-btn {
    position: relative;
    --icon-src:  url('../icons/sparkles.svg');
    --icon-tint: #fff;
}

.lane-header .lane-fx-modal-btn::before {
    content: '';
    position: absolute;
    inset: 0;
    background-color: var(--icon-tint);
    -webkit-mask: var(--icon-src) center / 14px 14px no-repeat;
            mask: var(--icon-src) center / 14px 14px no-repeat;
    pointer-events: none;
}

.lane-header .lane-fx-modal-btn:hover {
    --icon-tint: #fff;
}

/* FX, NR — the two two-character labels in the meta row. Fixed at
   26 px wide so the row width is an exact integer (the monospace text
   would otherwise auto-fit to ~26.008 px, leaving sub-pixel margins
   when the row is centred). Pressed state uses a blue accent so a
   glance separates it visually from M (red) and S (yellow). */
.lane-header .lane-effects-btn,
.lane-header .lane-nr-btn {
    width: 26px;
}

.lane-header .lane-effects-btn[aria-pressed="true"] {
    background: rgba(90, 160, 255, 0.18);
    border-color: #5aa0ff;
    color: var(--text);
}

/* Channel-level Normalisation / Compression / Noise-reduction toggles
   — text-glyph N / C / NR buttons that sit alongside FX. All share the
   same purple pressed state so the three "channel processing" effects
   read as one cohesive cluster, visually distinct from M (red), S
   (yellow), and FX (blue). */
.lane-header .lane-norm-btn[aria-pressed="true"],
.lane-header .lane-comp-btn[aria-pressed="true"],
.lane-header .lane-nr-btn[aria-pressed="true"] {
    background: rgba(168, 85, 247, 0.18);
    border-color: #a855f7;
    color: var(--text);
}

.timeline-canvas-wrap {
    position: relative;
    /* Fixed audio-lane height — single source of truth lives on
       #timelines as --canvas-h so the 3-channel cap math stays in sync. */
    height: var(--canvas-h);
}

.timeline-canvas {
    display: block;
}

/* Clip-name labels (one per placement) rendered as DOM children of the
   .timeline-canvas-wrap. Positioned at the clip's left edge, just below
   the canvas, so the text reads as anchored to the clip. The wrap has
   no overflow rule (default visible), so long names flow down into the
   lane's bottom padding region without being clipped by the canvas
   backing store. pointer-events: none so the underlying canvas keeps
   receiving mouse interaction (selection, drag, hover). */
.placement-name {
    position: absolute;
    top: calc(100% - 8px);
    pointer-events: none;
    color: #8189a0;
    font: 500 11px TTNorms, -apple-system, Arial, sans-serif;
    line-height: 1;
    white-space: nowrap;
    user-select: none;
    /* Hidden by default — syncPlacementNames adds .visible while the
       placement is selected, so names only show for the working clips. */
    display: none;
}

.placement-name.visible { display: block; }

.timeline-canvas-wrap.drop-target {
    outline: none;
}

/* Ghost outline that follows the mouse during a bin → lane drag. Width spans the
   full clip duration at the current zoom, top/bottom inset matches the clip
   drawing area inside the lane. Pointer-events: none so it never steals events
   from the underlying canvas (drop target). */
.timeline-canvas-wrap .drop-ghost {
    position: absolute;
    top: 14px;
    bottom: 14px;
    left: 0;
    width: 0;
    display: none;
    pointer-events: none;
    background: var(--accent-soft);
    border: 1.5px dashed var(--accent);
    border-radius: 4px;
    box-sizing: border-box;
    z-index: 2;
}


/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| BINS
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
*/
.bins {
    background: var(--panel);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.bin-tabs {
    display: flex;
    align-items: center;
    gap: 6px;
    /* Matches .transport-bar so the bins heading and the transport bar
       read as paired containers around the timeline. */
    padding: 12px;
}



.bin-tab {
    background: rgba(255, 255, 255, 0.06);
    /* Transparent at rest — .bin-tab.active colours the border to mark
       the currently-selected bin. Same pattern as the generic .btn /
       button base above. */
    border: 1.5px solid transparent;
    border-radius: 8px;
    color: var(--text-dim);
    padding: 0 12px;
    height: 38px;
    font-size: 13px;
    font-weight: 500;
    transition: color 0.12s, border-color 0.12s, background 0.12s, transform 80ms ease;
}

.bin-tab:hover {
    color: var(--text);
    background: rgba(255, 255, 255, 0.1);
}

.bin-tab:active { transform: scale(0.98); }

.bin-tab.active {
    color: var(--accent-2);
    background: var(--accent-soft);
    border-color: var(--accent);
}

/* Party tab is an easter-egg launcher: hidden by default, revealed only
   while Shift is held. Clicking it opens the party-mode modal rather than
   switching the active bin. */
.bin-tab-party { display: none; }
body.shift-held .bin-tab-party { display: inline-flex; }

/* Import button sits on the right edge of the bin-tabs row — pushes itself
   over with auto-margin, sized to match the bin-tab pill height so the
   header reads as a coherent strip. Disabled state inherits the standard
   button dim from .btn:disabled. */
.bin-import-btn {
    margin-left: auto;
    height: 38px;
    padding: 0 14px;
    font-size: 13px;
}

.bin-bodies {
    flex: 1 1 auto;
    overflow: auto;
    /* The bin-bodies wrapper is a positioning + scroll context for the
       active body. Setting it to flex lets `.bin-body.active` (a flex
       child) stretch to fill the remaining height even when the body's
       content is short — so file-drop and clip-drop targets cover the
       full visible bin area, not just the card grid. */
    display: flex;
    flex-direction: column;
}

.bin-body {
    display: none;
    /* 12px symmetric padding matches the side toolbar so every bin shares one
       interior gutter. */
    padding: 12px;
    /* When active, flex: 1 below stretches it to the parent's full
       height. align-content puts the cards at the top instead of the
       grid stretching each row to consume the space. */
    flex: 1 1 auto;
    min-height: 0;
    align-content: start;
}

.bin-body.active { display: block; }

/*
| CLIPS BIN
*/
.bin-body[data-bin="clips"],
.bin-body[data-bin="notes"] {
    display: none;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 8px;
}

.bin-body[data-bin="clips"].active,
.bin-body[data-bin="notes"].active { display: grid; }

/* Note card variant of .clip-card — flat neutral-blue swatch (notes
   have no category in the Audacity TSV, so one colour fits all). */
.clip-card.note-card .note-thumb {
    /* No content inside; the colour itself is the visual. */
}

.clip-card.note-card .note-binding {
    font-size: 10px;
    color: var(--muted);
    font-family: var(--fnt-std);
    font-variant-numeric: normal;
    letter-spacing: 0;
    text-transform: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.clip-card {
    position: relative;
    background: var(--panel-2);
    border: 1px solid var(--line);
    border-radius: 6px;
    padding: 8px 10px 10px;
    cursor: var(--cursor-pointer);
    user-select: none;
    transition: border-color 0.12s, background 0.12s;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* Placeholder card that holds the bin's layout while an upload is in
   flight. The spinner uses the same animation as button.is-loading so
   the visual language is consistent. */
.clip-card.uploading {
    pointer-events: none;
    align-items: center;
    justify-content: center;
    min-height: 80px;
    color: var(--text-dim);
}

.clip-card.uploading::before {
    content: '';
    width: 22px;
    height: 22px;
    border: 1.5px solid #e7ecf3;
    border-top-color: transparent;
    border-radius: 50%;
    animation: btn-spin 0.7s linear infinite;
}

.clip-card.uploading .uploading-name {
    font-size: 12px;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Per-card delete affordance — hover-revealed so the card stays clean at rest
   and the delete is one click away when the user wants it. Shared between the
   editor's Clips bin and the Creator upload page. */
.clip-card .clip-card-delete {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 22px;
    height: 22px;
    padding: 0;
    border: none;
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.35);
    opacity: 0;
    transition: opacity 0.12s, background 0.12s;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: var(--cursor-pointer);
}

.clip-card:hover .clip-card-delete,
.clip-card .clip-card-delete:focus-visible { opacity: 1; }

.clip-card .clip-card-delete:hover { background: var(--danger); }

.clip-card .clip-card-delete .icon { width: 14px; height: 14px; }

.clip-card:hover {
    background: var(--panel-3);
    border-color: #3b4150;
}

.clip-card:active { cursor: var(--cursor-pointer-down); }

.clip-card.selected,
.clip-card.selected:hover {
    border-color: var(--select);
    box-shadow: 0 0 0 1px var(--select) inset;
}

.bin-row.selected {
    box-shadow: 0 0 0 1.5px var(--select) inset;
}

/* Selected (working) track — same purple inset ring the clip cards and bin
   rows use, painted on the channel-label pill so the whole lane reads as
   picked without tinting the waveform area. */
.timeline-lane.selected .lane-header {
    box-shadow: 0 0 0 1.5px var(--select) inset;
}

/* Matches .bin-tab typography so clip card names and the bin headings above
   them read as the same kind of UI affordance. */
.clip-card .name {
    font-size: 12px;
    font-weight: 500;
    color: var(--text-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.clip-card .meta {
    color: var(--muted);
    font-size: 11px;
    font-family: var(--fnt-fix);
    font-variant-numeric: tabular-nums;
}

.clip-card .thumb {
    width: 100%;
    height: 36px;
    background: var(--silence-2);
    border-radius: 4px;
    display: block;
}

/*
| EDL / HISTORY BIN
*/
.bin-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 6px 4px 10px;
    border-bottom: 1px solid var(--line-soft);
    margin-bottom: 8px;
    color: var(--text-dim);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.bin-toolbar .actions {
    display: flex;
    gap: 4px;
}

.bin-row {
    display: grid;
    grid-template-columns: 90px 1fr auto;
    gap: 10px;
    align-items: center;
    padding: 6px 10px;
    border-radius: 4px;
    background: var(--panel-2);
    font-size: 12px;
    margin-bottom: 3px;
}

.bin-row:hover {
    background: var(--panel-3);
}

.bin-row .when {
    color: var(--muted);
    font-family: var(--fnt-fix);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
}

.bin-row .label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text);
}

.bin-row .actions {
    display: flex;
    gap: 4px;
}

.bin-row.undone .label,
.bin-row.undone .when {
    color: var(--muted);
    text-decoration: line-through;
}

.history-group {
    color: var(--muted);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin: 12px 6px 4px;
}
.history-group:first-child { margin-top: 0; }

.empty {
    color: var(--muted);
    padding: 16px 4px;
}

/* Library bin rows. Grid: audition button | name (flex) | duration | add button. */
.bin-group-header {
    color: var(--muted);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin: 12px 6px 4px;
}
.bin-group-header:first-child { margin-top: 0; }

.library-row {
    grid-template-columns: 28px 1fr auto 28px;
}

.library-row .library-audition,
.library-row .library-add {
    width: 24px;
    height: 24px;
    border-radius: 0;
    border: 0;
    background: var(--panel-3);
    color: var(--text);
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.library-row .library-audition:hover,
.library-row .library-add:hover {
    background: var(--accent, var(--panel-3));
    color: #fff;
}

.library-row.playing .library-audition {
    background: var(--accent, #4a90e2);
    color: #fff;
}

.library-row .library-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text);
}

.library-row .library-meta {
    color: var(--muted);
    font-family: var(--fnt-fix);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
}

/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| TOASTS — stacked bottom-left popups, modeled on studio's snackbar.
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
*/
/* Status-bar style: fixed full-width row at the bottom of the window.
   Same height (28px), background, text colour and 1px top border as the
   .timeline-ruler so the bar reads as a sibling system surface. Only one
   toast is shown at a time (toast() calls rail.replaceChildren first), so
   the row contains a single line of text aligned left. Outer padding sits
   in addition to the lane-label inset so the text doesn't crowd the
   viewport edges on very wide windows. */
.toast-rail {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* Stays in lockstep with body's padding-bottom reservation so the
       rail occupies exactly the space the editor was kept out of —
       never under, never over. */
    height: var(--toast-rail-h);
    background: var(--ruler);
    border-top: 1px solid var(--line);
    display: flex;
    align-items: center;
    /* Horizontal padding matches .lane-header so the bottom status bar
       reads as a sibling of the channel-meta gutter — text starts at the
       same X as the channel label above it. */
    padding: 0 12px;
    z-index: 200;
    pointer-events: none;
    overflow: hidden;
}

.toast {
    pointer-events: auto;
    /* Default colour matches .lane-header .lane-name (the channel-label
       text in the meta gutter) so a neutral status message reads as the
       same UI element as the channel labels above. Severity overrides
       below recolour for HTTP 2xx / 3xx-4xx / 5xx responses. */
    color: var(--text-dim);
    font-family: var(--fnt-std);
    /* 12px / 500 — matches .lane-header .lane-name so the bottom status
       bar reads as part of the same UI vocabulary as the channel meta
       column labels on the left. */
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.025ch;
    line-height: var(--toast-rail-h);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    user-select: none;
    opacity: 0;
    transition: opacity 160ms ease;
}

.toast.show { opacity: 1; }

/* Severity is conveyed through the text colour now that the floating
   outline is gone — same accents the rest of the UI uses. */
.toast.error { color: var(--red-1); }
.toast.warn  { color: var(--ylw-1, var(--accent-2)); }
.toast.ok    { color: var(--grn-1, var(--accent)); }

/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| FILE-DROP OVERLAY
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
*/
/* Per-bin drop target — outlines the active bin's body while a file
   is being dragged in from the OS. Only the currently-visible bin
   highlights (the drag handler bails when body.dataset.bin !== activeBin),
   so files dragged over a non-matching tab silently ignore. */
.bin-body.drop-target {
    outline: 2px dashed var(--accent, #5aa0ff);
    outline-offset: -2px;
    background: rgba(90, 160, 255, 0.06);
    border-radius: 6px;
}

/* Legacy page-wide overlay — kept hidden so any stale markup doesn't
   paint. Drop targeting is now scoped to .bin-body.drop-target above. */
.drop-overlay { display: none; }

.drop-overlay-inner {
    background: var(--panel);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 10px;
    padding: 14px 22px;
    color: var(--text);
    font-size: 15px;
    font-weight: 500;
    box-shadow: 0 12px 36px rgba(0, 0, 0, 0.4);
}

/*
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
| MODALS — ported from studio's modal.css (.modal-card) so paddings, radii, and
| the header-divider stripe match. .auth-modal applies the tighter .auth-card
| overrides on top of the same base.
|–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
*/
.modal-backdrop {
    position: fixed;
    inset: 0;
    /* Semi-transparent scrim — keeps the workspace visible (timelines, lanes,
       toolbar) behind any open modal. */
    background: hsla(230, 15%, 12.5%, 0.7);
    backdrop-filter: blur(4px);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 32px;
    z-index: 50;
    opacity: 0;
    pointer-events: none;
    transition: opacity 180ms ease;
}

.modal-backdrop.open {
    opacity: 1;
    pointer-events: auto;
}

.modal {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 416px;
    max-height: calc(100vh - 64px);
    background-color: var(--blk-3);
    border-radius: var(--rad-1);
    overflow: visible;
    transform: translateY(10px);
    transition: transform 180ms ease, opacity 180ms ease;
}

.modal-backdrop.open .modal { transform: translateY(0); }

.modal > header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 16px;
    padding: 24px 32px 8px 32px;
    background: transparent;
    border-bottom: none;
}

.modal > header h2 {
    margin: 0;
    font-size: var(--siz-2);
    color: var(--wht-1);
}

/* `.close` inherits the standard .icon-btn look — same 38×38 square, hairline
   border, and dark surface as the side-toolbar buttons. No special-case styling. */

.modal > section,
.modal section.form {
    display: flex;
    flex-direction: column;
    gap: 24px;
    padding: 32px;
}

.modal section ~ section,
.modal section ~ footer {
    padding: 0 32px 32px 32px;
}

/* Project settings form lays its fields out two per row to keep the modal a
   reasonable height. Fields tagged `.field-wide` span the full row. */
#settings-modal section.form {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 24px;
}

#settings-modal section.form .field-wide {
    grid-column: 1 / -1;
}

.modal > footer {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 16px;
    padding: 32px;
    font-size: var(--siz-0);
    font-weight: 500;
    color: var(--wht-1);
}

/* Pushes following footer buttons to the right when a footer mixes leading
   destructive actions with trailing primary/cancel ones. */
.footer-spacer { flex: 1; }

/* Destructive footer button — red outline + red icon, fills red on hover. */
.icon-btn.danger {
    border-color: var(--danger);
    color: var(--danger);
}
.icon-btn.danger:hover {
    background: var(--danger);
    color: #fff;
}
.icon-btn.danger:hover .icon {
    filter: brightness(0) invert(1);
}

/* Hairline divider between modal sections, if any HR is present. The coloured
   header stripe was removed for visual consistency — header and body share the
   modal's background, no divider rendered between them. */
.modal hr {
    border: none;
    border-top: solid 1px var(--blk-2);
    width: 100%;
    margin: 0;
}

/* Studio's auth-card overrides — tighter padding than the base modal-card. */
.modal.auth-modal {
    max-width: 416px;
    border-radius: var(--rad-3);
    border: solid 1.5px var(--blk-4);
    box-shadow: 2px 12px 12px var(--blk-4);
}

.modal.auth-modal > header         { padding: 24px 24px 8px 24px; }
.modal.auth-modal > section,
.modal.auth-modal section.form     { padding: 8px 24px 24px 24px; gap: 24px; }
.modal.auth-modal > footer         { padding: 0 24px 24px 24px; justify-content: space-between; }
.modal.auth-modal section ~ footer { padding: 0 24px 24px 24px; }

.modal .body {
    padding: 32px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 24px;
    color: var(--wht-1);
}

/* Modal footer buttons share the default .icon-btn appearance — no primary
   highlight — to match the side-toolbar style. */

/* Every button icon paints pure white — overrides whatever stroke
   colour the source SVG ships with (Lucide files default to #e7ecf3, a
   light off-white). brightness(0) collapses the image to black, then
   invert(1) flips it to white in one pass. Applies project-wide so
   modal, toolbar, lane-meta, and bin icons all match. The CSS-mask
   buttons (.transport-bar, header) bypass <img> entirely and instead
   colour their ::before with --icon-tint, which is now also set to
   #fff. The danger-button hover rule below is unaffected because its
   filter applies on top of this base. */
button img.icon {
    filter: brightness(0) invert(1);
}

.auth-error {
    color: var(--red-1);
    font-size: var(--siz-0);
    min-height: calc(var(--siz-0) + 4px);
}

.link-btn {
    appearance: none;
    background: transparent;
    border: none;
    color: var(--blu-1);
    padding: 0;
    height: auto;
    font: inherit;
    cursor: var(--cursor-pointer);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.link-btn:hover  { background: transparent; color: var(--wht-1); }
.link-btn:active { transform: none; }

.auth-toggle {
    color: var(--wht-3);
    font-size: var(--siz-0);
    display: flex;
    gap: 6px;
    align-items: baseline;
}

/* Studio's input / select / label / field rules, ported verbatim. */
.modal .field { position: relative; display: flex; flex-direction: column; gap: 8px; }
.modal .field[hidden] { display: none !important; }

/* Volume-change in/out field: two timecode inputs sitting side by side
   in a single row so the [In, Out] window reads as one paired control. */
.modal .field.effect-modal-range {
    flex-direction: row;
    gap: 12px;
}

.modal .field.effect-modal-range label {
    flex: 1 1 0;
    min-width: 0;
}

.modal .field label {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    font-family: var(--fnt-std);
    font-size: var(--siz-0);
    font-weight: 500;
    color: var(--wht-3);
    user-select: none;
    -webkit-user-select: none;
}

.modal .field label > div { color: var(--wht-3); }

.modal .field [hidden] { display: none !important; }

/* Toggle field — labelled aria-pressed button on the left, inline
   settings icon-btn on the right. Used in the Clip modal for
   normalisation and compression. The check button reuses the app's
   standard toggle pattern (snap-btn, bins-toggle, lane M/S, …): a
   plain <button aria-pressed="…"> that the global
   button[aria-pressed="true"] rule paints with the accent tint when on. */
.modal .field.field-toggle {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.modal .field.field-toggle .check-btn {
    /* Fill the available row width so the label sits left-aligned the
       way it does for the other modal fields. Padding + height come
       from the base .btn rule. */
    flex: 1 1 auto;
    justify-content: flex-start;
    text-align: left;
    padding: 0 14px;
}

/* Two-column header above a modal range slider: field name on the left,
   live current-value readout right-aligned with the slider's right edge.
   The slider itself sits below at width 100% so the readout naturally
   lines up with the slider's track end. Only applies to fields that opt
   in via `.slider-label` — Fade in / Fade out use plain `<div>` headers
   because they're number inputs that show their own value inline. The
   value is muted (wht-3) so the field name reads as the primary label
   and the number doesn't visually compete with it. */
.modal .field .slider-label {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
}

.modal .field .slider-label .slider-value {
    color: var(--wht-3);
    font-variant-numeric: tabular-nums;
}

/* Checkbox rows in the effect-settings sub-modal (the Audacity-style
   compressor toggles). Fully custom checkbox — appearance:none turns the
   input into a plain square we paint ourselves (the native macOS widget
   renders rounded and ignores the palette), with the app's check.svg
   masked in white over an accent fill when ticked. The generic
   `.modal .field label` rule stacks children in a column and the
   `.modal input` skin stretches to full width; both are unwound here so
   the box sits left-aligned beside its caption. */
.modal .field .checkbox-label {
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
    align-items: center;
    gap: 10px;
    width: 100%;
    cursor: var(--cursor-pointer);
}

.modal .field .checkbox-label input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    position: relative;
    width: 15px;
    height: 15px;
    min-width: 15px;
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
    border: none;
    border-radius: 2px;
    outline: solid 1.5px var(--blk-1);
    background: hsl(230, 15%, 18.5%);
    cursor: var(--cursor-pointer);
}

.modal .field .checkbox-label input[type="checkbox"]:checked {
    outline-color: var(--accent);
    background: var(--accent);
}

.modal .field .checkbox-label input[type="checkbox"]:checked::after {
    content: '';
    position: absolute;
    inset: 1px;
    background: #fff;
    -webkit-mask: url('../icons/check.svg') center / contain no-repeat;
    mask: url('../icons/check.svg') center / contain no-repeat;
}

/* Range inputs in the modal — thin track + circular thumb, identical
   across every slider field (Volume, Pan, channel NR strength, etc.). The
   thumb + track are styled explicitly via the WebKit / Mozilla pseudo-
   elements so the browser has no native hover state to morph into;
   without this the default thumb visibly brightens / scales on hover,
   which reads as noise against an otherwise static modal. The base
   .modal input rule below paints text inputs with a boxy background +
   border — overriding it here keeps range inputs from inheriting that
   chrome (which made one slider look like a number field with a thumb
   stuck inside it). */
.modal input[type="range"] {
    width: 100%;
    height: 32px;
    padding: 0;
    margin: 0;
    outline: none;
    border: none;
    border-radius: 0;
    background: transparent;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    cursor: pointer;
}

/* Range tracks. The `--tick-pct` custom property is set per-slider by
   `initSliderTicks()` in app.js — one tick is drawn every <tick-pct>% via
   a 1px-wide vertical band in a repeating gradient over the base track
   colour. A trailing single-tick gradient closes the run with a tick at
   100% (the repeating gradient draws the leading edge of each tile, so
   the final position would otherwise be missed). Opacity kept low so
   the marks read as a subtle scale rather than chrome. */
.modal input[type="range"]::-webkit-slider-runnable-track {
    height: 4px;
    background-color: var(--blk-1);
    background-image:
        linear-gradient(to right, var(--wht-3) 0, var(--wht-3) 1px, transparent 1px),
        repeating-linear-gradient(
            to right,
            var(--wht-3) 0,
            var(--wht-3) 1px,
            transparent 1px,
            transparent var(--tick-pct, 100%)
        );
    background-repeat: no-repeat, repeat-x;
    background-position: right center, left center;
    background-size: 1px 8px, auto 8px;
    border-radius: 2px;
    border: none;
}

.modal input[type="range"]::-moz-range-track {
    height: 4px;
    background-color: var(--blk-1);
    background-image:
        linear-gradient(to right, var(--wht-3) 0, var(--wht-3) 1px, transparent 1px),
        repeating-linear-gradient(
            to right,
            var(--wht-3) 0,
            var(--wht-3) 1px,
            transparent 1px,
            transparent var(--tick-pct, 100%)
        );
    background-repeat: no-repeat, repeat-x;
    background-position: right center, left center;
    background-size: 1px 8px, auto 8px;
    border-radius: 2px;
    border: none;
}

.modal input[type="range"]::-webkit-slider-thumb {
    appearance: none;
    -webkit-appearance: none;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--accent);
    border: none;
    margin-top: -5px;     /* centre 14px thumb on 4px track */
    cursor: pointer;
}

.modal input[type="range"]::-moz-range-thumb {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--accent);
    border: none;
    cursor: pointer;
}

.modal input,
.modal select,
.modal textarea {
    display: flex;
    align-items: center;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    overflow: hidden;
    margin: 0;
    padding: 0 0.85rem;
    min-width: 0;
    width: 100%;
    height: var(--btn-hgt);
    outline: solid 1.5px var(--blk-1);
    border: none;
    border-radius: var(--rad-1);
    background: hsl(230, 15%, 18.5%);
    font-family: var(--fnt-std);
    font-size: var(--siz-1);
    font-weight: 500;
    text-overflow: ellipsis;
    letter-spacing: 0.0125ch;
    color: var(--wht-1);
    background-image: none;
}

.modal input, .modal select { white-space: nowrap; }

.modal textarea {
    field-sizing: content;
    padding: 0.85rem;
    line-height: 1.4;
    resize: none;
    height: auto;
}

.modal select { cursor: var(--cursor-pointer); }
.modal select::-ms-expand { display: none; }

.modal input::placeholder { color: var(--blk-0); }

.modal input:focus,
.modal select:focus,
.modal textarea:focus {
    outline: solid 1.5px var(--blu-1);
}

/* Range inputs don't need (or want) the focus outline that text inputs
   get — the thumb is its own visual indicator and a stray rectangle
   around the whole track on click reads as a bug, not a focus cue.
   Highly-specific override so the .modal input:focus rule above can't
   leak back in. */
.modal input[type="range"]:focus,
.modal input[type="range"]:focus-visible {
    outline: none;
}

.modal input[readonly] {
    color: var(--wht-3);
    cursor: var(--cursor-default);
}

/* Key-capture field used in user settings. Visually matches .modal input so the
   modal stays cohesive, but it's a button so clicks arm a one-shot keydown
   listener that reassigns the shortcut. Transitions/scale/hover are neutralized
   so it behaves like a static input rather than a pressable button. */
.modal .shortcut-input {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    appearance: none;
    margin: 0;
    padding: 0 0.85rem;
    width: 100%;
    height: var(--btn-hgt);
    outline: solid 1.5px var(--blk-1);
    border: none;
    border-radius: var(--rad-1);
    background: hsl(230, 15%, 18.5%);
    font-family: var(--fnt-std);
    font-size: var(--siz-1);
    font-weight: 500;
    color: var(--wht-1);
    cursor: var(--cursor-pointer);
    text-align: left;
    transition: none;
}

.modal .shortcut-input:hover  { background: hsl(230, 15%, 18.5%); }
.modal .shortcut-input:active { transform: none; }

.modal .shortcut-input:focus,
.modal .shortcut-input.recording {
    outline: solid 1.5px var(--blu-1);
    color: var(--wht-3);
}

.modal .field.required > label > div::after,
.modal .field > label.required > div::after {
    content: '•';
    position: relative;
    padding-left: 0.125rem;
    top: -0.5ch;
    color: var(--red-1);
    font-size: 0.625rem;
    opacity: 0.5;
}


/* OTP PIN cells — ported from studio. */
.pin-field { display: flex; justify-content: center; }
.pin-cells {
    position: relative;
    display: flex;
    gap: 0.5rem;
    cursor: text;
}
.pin-cells input[data-pin-input] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    font-size: 1rem;
    caret-color: transparent;
}
.pin-cell {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    height: 3.5rem;
    border-radius: var(--rad-1);
    outline: solid 1.5px var(--blk-1);
    background: hsl(230, 15%, 18.5%);
    font-family: var(--fnt-fix);
    font-size: var(--siz-3, 24px);
    color: var(--wht-1);
    transition: outline-color 0.15s, background-color 0.15s;
}
.pin-cell.filled { outline-color: var(--blk-0); }
.pin-cell.active { outline-color: var(--blu-1); }

.auth-subtitle {
    font-family: var(--fnt-std);
    font-size: var(--siz-1);
    font-weight: 500;
    color: var(--wht-3);
    margin: 0;
}

.auth-link {
    margin: 0;
    color: var(--wht-3);
    font-size: var(--siz-0);
}

/* Party-mode modal — full-screen takeover; the backdrop drops its
   32px padding so the modal hits every edge of the viewport. */
#party-modal { padding: 0; }

.modal.party-modal {
    width: 100vw;
    height: 100vh;
    max-width: none;
    max-height: none;
    border-radius: 0;
    position: relative;
    /* Default dark surface; the 35-second purple-stars phase swaps in a
       deep purple to mark the second act of the party. */
    background: var(--blk-3);
    transition: background-color 0.8s ease;
    overflow: hidden;
}

.modal.party-modal.purple {
    background: #4a1f8a;
}

.modal.party-modal.blue {
    background: #0e2a6b;
}

.modal.party-modal.orange {
    background: #000;
}

.modal.party-modal .close {
    position: absolute;
    top: 10px;
    right: 12px;
    z-index: 2;
}

.modal .party-body {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    overflow: hidden;
    position: relative;
    z-index: 1;
}

#party-svg-host svg {
    display: block;
    width: 100%;
    height: 100%;
    max-width: none;
    pointer-events: none;
    transform-origin: center center;
    animation: party-pulse 20s ease-in-out infinite;
}

/* Star layer — JS injects a handful of <div class="star"> elements
   inside this overlay when the purple phase begins. Each pulses with
   a staggered delay so the field reads as twinkling, not strobing. */
.star-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 1;
}

.star {
    position: absolute;
    width: 48px;
    height: 48px;
    margin: -24px 0 0 -24px;
    color: #ffd84d;
    transform: scale(0);
    opacity: 0;
    animation: star-zoom 2s ease-in-out infinite;
}

.star svg {
    width: 100%;
    height: 100%;
    display: block;
}

@keyframes star-zoom {
    0%, 100% { transform: scale(0); opacity: 0; }
    50%      { transform: scale(1); opacity: 1; }
}

/* Laser-light overlay — radial beams pinned to the centre of the modal,
   rotating as a group so the field reads as a club rig sweep. Each beam
   is an SVG with its own colour gradient; screen blend lifts the colours
   against the blue backdrop instead of stamping on top. */
.laser-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
    /* Sits below the party-body (z-index: 1) so the beams sweep behind
       the dancing cactus instead of cutting across it. */
    z-index: 0;
    overflow: hidden;
}

.laser-stage {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 0;
    height: 0;
    animation: laser-rotate 6s linear infinite;
}

.laser-beam {
    position: absolute;
    left: 0;
    top: -4px;
    width: 100vmax;
    height: 8px;
    transform-origin: 0 50%;
    mix-blend-mode: screen;
}

.laser-beam svg {
    width: 100%;
    height: 100%;
    display: block;
    filter: drop-shadow(0 0 6px currentColor);
}

@keyframes laser-rotate {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

@keyframes party-pulse {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(0.5); }
}

/* ─── shared effects modal: "Other effects" list ──────────────────────────────
   Lives at the bottom of the clip / channel effects modal. Each row is one
   effect — type label on the left, terse param summary in the middle, remove
   button on the right. The add chooser sits below the list. */

#clip-modal-other-effects {
    margin-top: 12px;
}

.effects-list {
    list-style: none;
    margin: 6px 0 8px;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.effects-list:empty {
    margin-bottom: 0;
}

/* toggle | label | summary | cog | delete. The summary takes the
   elastic 1fr slot so it right-aligns against the cog/delete pair;
   the label hugs its text on the left. Empty-cog rows (effects with
   no per-effect settings) still reserve a placeholder column so the
   delete button stays in the same vertical position across every row
   in the list. The toggle is the bypass switch — flipping it off
   marks the row `_disabled: true` in paramsJson rather than removing
   it, so params survive the bypass cycle. */
.effects-list-row {
    display: grid;
    /* [toggle | label (elastic) | cog | delete] — label absorbs the
       gap that the old summary column used to take. */
    grid-template-columns: max-content 1fr max-content max-content;
    gap: 8px;
    align-items: center;
    padding: 6px 0;
}

/* Dim the label when the row is bypassed so the list reads as
   "active vs. parked" at a glance, matching the toggle's off-state
   colour. */
.effects-list-label-disabled {
    color: var(--text-dim, #8189a0);
}

.effects-list-label {
    font-weight: 500;
    color: var(--text, #d8dce5);
    text-align: left;
}

.effects-list-remove {
    background: transparent;
    border: none;
    color: var(--text-dim, #8189a0);
    cursor: pointer;
    font-size: 18px;
    line-height: 1;
    padding: 2px 6px;
    border-radius: 3px;
}

.effects-list-remove:hover {
    color: var(--red-1, #ff6b6b);
    background: rgba(255, 107, 107, 0.08);
}

.effects-add-row {
    display: flex;
    gap: 6px;
    align-items: center;
}

.effects-add-row select {
    flex: 1;
    padding: 4px 6px;
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 3px;
    background: rgba(0, 0, 0, 0.2);
    color: var(--text, #d8dce5);
}

.effects-add-row button {
    padding: 4px 12px;
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 3px;
    background: rgba(255, 255, 255, 0.04);
    color: var(--text, #d8dce5);
    cursor: pointer;
}

.effects-add-row button:hover {
    background: rgba(255, 255, 255, 0.08);
}

/* The row is mid-DELETE: dim the whole thing and block any further
   clicks so the user sees the action took effect even before the
   server round-trip + refreshList rebuild lands. The `aria-busy`
   attribute is also set on the row for screen-reader feedback. */
.effects-list-row-removing {
    opacity: 0.45;
    pointer-events: none;
}

/* Reserves the cog-column slot for rows whose effect has no per-effect
   settings (e.g. normalisation). Without a fixed width, max-content
   collapses the column to 0 px and the delete button shifts left,
   breaking alignment with sibling rows. The width matches the rest of
   the app's icon-btn footprint so the delete column stays in line. */
.effects-list-spacer {
    width: 38px;
    height: 38px;
}

/* Per-instance bypass switch on each effects-list row. aria-pressed
   true = enabled, false = bypassed (the row stays in the DB but the
   audio chain skips it). Animation matches the lane FX toggle so the
   two surfaces feel like one control language. */

.effects-toggle {
    appearance: none;
    -webkit-appearance: none;
    width: 40px;
    height: 24px;
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 12px;
    background: rgba(0, 0, 0, 0.3);
    position: relative;
    cursor: pointer;
    padding: 0;
    flex-shrink: 0;
    /* Log-shaped ease: leaps off the rail then settles into the destination.
       The bezier's first control point near (0, 0.9) snaps motion + colour
       immediately, the second near (0.15, 1) glides the last few pixels. */
    transition: background-color 260ms cubic-bezier(0.05, 0.9, 0.15, 1),
                border-color     260ms cubic-bezier(0.05, 0.9, 0.15, 1);
}

.effects-toggle::after {
    content: '';
    position: absolute;
    top: 3px;
    left: 3px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--text-dim, #8189a0);
    transition: transform        260ms cubic-bezier(0.05, 0.9, 0.15, 1),
                background-color 260ms cubic-bezier(0.05, 0.9, 0.15, 1);
}

.effects-toggle[aria-pressed="true"] {
    background: var(--accent, #4a9eff);
    border-color: var(--accent, #4a9eff);
}

.effects-toggle[aria-pressed="true"]::after {
    /* Track content is 32 px wide. Knob 12 px + left:2 → off-state right edge
       at x=14, on-state should sit symmetrically with a 2 px right margin →
       right edge at x=30, so translateX = 30 − 14 = 16. */
    transform: translateX(16px);
    background: white;
}

/* .effects-list-settings inherits the global `button` styling — no custom
   override here. The button element in the row picks up the same border /
   background / padding / hover as every other button in the app. */

/* Older .effects-list-remove rule still defined above; no row uses it now,
   but kept in case future effect rows reintroduce a per-row delete button. */
