* {
  box-sizing: border-box;
}

/* A sideways trackpad swipe pans the canvases, and must never become the browser's back/forward:
   a canvas's wheel preventDefault only covers gestures that START over it, so the header, gutters
   and panel edges would still hand the swipe to history navigation. */
html,
body {
  overscroll-behavior-x: none;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 13px;
  background: var(--bg);
  color: var(--text);
}

/* The page grid. The sidebar has its own COLUMN (spanning the main row and the mix-strip row)
   rather than living inside main: in DJ mode the settings then run all the way down beside the
   strip, and the strip's symmetric region is simply grid column 1 - no margin arithmetic. */
.layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  /* header / an open replay / main / the mixer's drag handle / the mix strip / console. The two
     sized rows are properties rather than values because DJ mode moves the springy one between
     them as its regions fold (see djApplyRegionSizes); outside DJ mode they hold these defaults.
     The replay's row is empty (and so no height) while no replay is open. */
  grid-template-rows: auto auto var(--dj-row-main) auto var(--dj-row-strip) auto;
  height: 100vh;
}
.layout > header {
  grid-column: 1 / -1;
}
#replayBar {
  grid-column: 1 / -1;
  grid-row: 2;
}
main {
  grid-column: 1;
  grid-row: 3;
}
aside {
  grid-column: 2;
  grid-row: 3 / span 3; /* down past the handle and mix-strip rows */
}
#djStripResize {
  grid-column: 1;
  grid-row: 4;
}
#mixStrip,
#djStripRail {
  grid-column: 1;
  grid-row: 5;
}
#console {
  grid-column: 1 / -1;
  grid-row: 6;
}

/* ------------------------------------------------------------------ header */

header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 4%, var(--bg-raised)), var(--bg-raised));
  border-bottom: 1px solid var(--border);
}

/* The headphone knobs (see phonesRender): the desk's knob, with its label beside it rather than
   under, so the pair sits in the header's row without making it taller. Dimmed, but still
   turnable and learnable, while no cue pair is booted. */
.phones-ctl {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
}
.phones-ctl.off {
  opacity: 0.4;
}
.phones-ctl.hidden {
  display: none;
}
.phones-knob {
  display: flex;
  align-items: center;
  gap: 5px;
}
.phones-knob > span {
  font-size: 10px;
  color: var(--text-dim);
  white-space: nowrap;
}

.logo {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  background: linear-gradient(135deg, var(--accent), var(--warn));
  box-shadow: 0 0 12px var(--accent-strong);
}

header h1 {
  font-size: 15px;
  margin: 0 6px 0 0;
  font-weight: 700;
  letter-spacing: 0.03em;
}

/* Every control in the header is one height and one radius, with its contents centered - a button,
   a select and the docs link read as one row of the same thing, whatever each is made of. The
   icons are spans of their own so they can be sized as glyphs (a ▶ or ↻ at text size is a speck)
   without the label growing with them. */
header > button,
header > select,
header > a.hbtn,
.rec-wrap > button {
  height: 32px;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 0 14px;
  border-radius: 8px;
  font-size: 13px;
  line-height: 1;
  white-space: nowrap;
}

header .ico {
  font-size: 15px;
  line-height: 1;
  display: inline-block;
  transform: translateY(-0.5px); /* optical center: glyphs sit a hair low against lowercase text */
}

#recBtn .ico {
  font-size: 11px;
}

.spacer {
  flex: 1;
}

button,
select {
  background: var(--bg-panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 6px 14px;
  cursor: pointer;
  font-size: 13px;
  font-family: inherit;
  transition: background 0.12s, border-color 0.12s, transform 0.06s;
}

button:active {
  transform: translateY(1px);
}

/* Custom chevron so the arrow inset mirrors the text's left padding; long values (audio
   device names) ellipsize instead of running into it. */
select {
  appearance: none;
  -webkit-appearance: none;
  padding: 6px 28px 6px 10px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23888e99' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 10px 6px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

button:hover,
select:hover {
  background-color: var(--hover-bg); /* -color, not the shorthand: a select's chevron is a background-image */
  border-color: var(--border-strong);
}

/* A control that cannot be used says so wherever it is: dimmed, no hover, no press. A panel with
   its own idea of disabled (the deck heads, the roll's head, the menus) still overrides this. */
button:disabled,
select:disabled {
  opacity: 0.4;
  cursor: default;
}

button:disabled:hover,
select:disabled:hover {
  background-color: var(--bg-panel);
  border-color: var(--border);
}

button:disabled:active {
  transform: none;
}

/* Text-shaped inputs carry the theme everywhere - the stock white box with black text was
   what every unscoped field (a bpm box, the ramp seconds) fell back to. :where() keeps this at
   zero specificity, so a panel's own tighter rule (mono digits, a width) still wins. */
input:where(:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='color']):not([type='file'])) {
  padding: 3px 6px;
  font-size: 12px;
  font-family: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  outline: none;
  transition: border-color 0.12s;
}
input:where(:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='color']):not([type='file'])):focus {
  border-color: var(--accent);
}
input::placeholder {
  color: var(--text-dim);
}
/* Plain boxes for numbers: the stock spinner buttons are a few pixels wide and nobody steps a
   rate or a bpm by clicking them - you type it. */
input[type='number'] {
  -moz-appearance: textfield;
  appearance: textfield;
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

button.primary {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

button.primary:hover {
  background: var(--accent-strong);
}

/* Play/Stop and Update are a matched pair: same fixed width + centered "icon label" so they
   sit symmetrically and the Play/Stop toggle doesn't reflow the header when it changes text. */
#playBtn,
#updateBtn {
  min-width: 100px;
}

/* Play/Stop toggle: green while it says "▶ play", red once it flips to "■ stop". */
#playBtn {
  background: color-mix(in srgb, var(--ok) 16%, transparent);
  border-color: var(--ok);
  color: var(--ok);
}

#playBtn:hover {
  background: color-mix(in srgb, var(--ok) 28%, transparent);
}

#playBtn.is-playing,
#deckAPlay.is-playing,
#deckBPlay.is-playing {
  background: color-mix(in srgb, var(--err) 16%, transparent);
  border-color: var(--err);
  color: var(--err);
}

#playBtn.is-playing:hover,
#deckAPlay.is-playing:hover,
#deckBPlay.is-playing:hover {
  background: color-mix(in srgb, var(--err) 28%, transparent);
}

button.small {
  padding: 2px 8px;
  font-size: 11px;
  border-radius: 4px;
}

/* An anchor styled to sit among the header buttons (the docs link opens in a new tab). */
a.hbtn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--bg-panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 6px 12px;
  font-size: 13px;
  text-decoration: none;
  transition: background 0.12s, border-color 0.12s;
}

a.hbtn:hover {
  background: var(--hover-bg);
  border-color: var(--border-strong);
}

/* conf (configure) capture is armed for this track - knob moves in its plugin windows are being
   written into the code. */
button.small.conf-active {
  background: var(--accent, #d08770);
  border-color: var(--accent, #d08770);
  color: #1a1a1a;
}

/* Wide enough for the longest thing it says ("restarting engine…"), and centered inside that, so
   the engine going from checking to ready never resizes the pill - which used to shove every
   header control along with it. */
.status {
  min-width: 124px;
  text-align: center;
  font-size: 11px;
  padding: 3px 10px;
  border-radius: 99px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  color: var(--text-dim);
}

.status.ok {
  border-color: var(--ok-border);
  color: var(--ok);
}

.status.error {
  border-color: var(--err-border);
  color: var(--err);
}

/* -------------------------------------------------------------------- main */

main {
  display: flex;
  overflow: hidden;
}

/* The pane stacks its HEAD over the editor (mirroring deck B's pane), so the editor sizes by flex
   under it rather than by the old single-child fill. Always, not only in DJ mode: the head carries
   the pane's code | arrange switch now, and that is there whether or not there are two decks. */
#editorPane {
  position: relative; /* the save flash overlays it */
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
#editorPane .CodeMirror {
  height: auto;
  min-height: 0;
  flex: 1;
}

:root {
  --sidebar-w: 320px; /* one source for everything that must stop where the sidebar begins */
}
[data-sidebar-collapsed] {
  --sidebar-w: 40px;
}
aside {
  width: var(--sidebar-w);
  border-left: 1px solid var(--border);
  background: var(--bg-raised);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* sidebar tab bar (session | files) + minimize toggle */

.side-tabs {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 2px;
  padding: 8px 8px 0;
}

.side-tab {
  padding: 4px 9px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-radius: 7px;
  background: none;
  border: 1px solid transparent;
  color: var(--text-dim);
}

/* The minimize toggle must stay reachable even when the tabs crowd the row - never let it be
   the item that gets clipped by the sidebar's overflow: hidden. */
#sidebarToggle {
  flex-shrink: 0;
}

.side-tab.active {
  background: var(--bg-panel);
  border-color: var(--border);
  color: var(--accent);
}

.tab-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.tab-body.hidden {
  display: none;
}

/* The sidebar's own sections and buttons - the browser-only backup store, the desktop-only
   folders, the share button. The file has no global .hidden (an ID's display rule would outrank
   it, see #fxEdModal); this is the sidebar's, and it outranks `button.small`. */
#sidebar .hidden {
  display: none;
}

/* Minimized sidebar: a narrow rail with just the restore button.
   Keyed off <html>, not a class on the panel, because index.html stamps these attributes from
   localStorage before the first paint - client.js runs far too late to decide it without the
   panel visibly opening and then shutting on every reload. */
[data-sidebar-collapsed] aside .tab-body,
[data-sidebar-collapsed] aside .side-tab {
  display: none;
}

[data-sidebar-collapsed] aside .side-tabs {
  padding: 8px 0 0;
  justify-content: center;
}

/* The arrows belong to the same pre-paint state, so they are drawn from it rather than written in
   by script - otherwise a minimized panel still paints its toggle pointing the wrong way. */
#sidebarToggle::after {
  content: '»';
}

[data-sidebar-collapsed] #sidebarToggle::after {
  content: '«';
}

#consoleToggle::after {
  content: '▾';
}

[data-console-collapsed] #consoleToggle::after {
  content: '▴';
}

/* Sections are flex children that may be squeezed on short windows; overflow: hidden makes a
   squeezed section clip its content instead of painting over the section below. Anything that
   should survive squeezing scrolls inside its own .panel/.panel.list. */
aside section {
  display: flex;
  flex-direction: column;
  padding: 10px 12px 0;
  min-height: 0;
  overflow: hidden;
}

/* The settings tab is the exception to the rule above: it is a stack of fixed-size controls, not
   panels that can give ground, so there is nothing in it that squeezing can do but clip one section
   into the next. (A browser zoomed in is a short window - the viewport is the same screen in fewer
   CSS pixels - which is how this shows up at any window size.) It scrolls as a whole instead, and
   its sections keep the height their contents ask for. */
#settingsTab {
  overflow-x: hidden;
  overflow-y: auto;
  padding-bottom: 12px;
}

#settingsTab section {
  flex: 0 0 auto;
  overflow: visible;
}

/* session tab: tracks take whatever the macro bank doesn't need, scrolling inside the panel */
#trackSection {
  flex: 1;
}

#trackInfo {
  overflow-y: auto;
  min-height: 0;
}

/* the knob bank has a fixed footprint - never squeeze it */
#macrosSection {
  flex-shrink: 0;
  padding-bottom: 10px;
}

/* sounds tab: three scrolling lists sharing the height */
#paramsSection {
  flex: 3;
}

#pluginsSection {
  flex: 2;
}

#samplesSection {
  flex: 3;
  padding-bottom: 10px;
}

/* Macros - the knob bank. Each knob is a conic-gradient ring (--v is the 0..1 value, swept
   over 270° starting at the 7-o'clock position, like a hardware pot) around a raised center
   holding the knob number; the editable name sits underneath. */
.macro-bank {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px 6px;
  justify-items: center;
}

.macro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-width: 0;
}

.macro-knob {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: default;
  user-select: none;
  touch-action: none;
  background:
    radial-gradient(closest-side, var(--bg-raised) 70%, transparent 71%),
    conic-gradient(
      from 225deg,
      var(--accent) calc(var(--v, 0) * 270deg),
      var(--accent-soft) calc(var(--v, 0) * 270deg) 270deg,
      transparent 270deg
    );
}

.macro-knob:hover .macro-num {
  color: var(--text);
}

.macro-num {
  font-size: 13px;
  color: var(--text-dim);
  pointer-events: none;
}

.macro-name {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 10.5px;
  color: var(--text-dim);
  cursor: text;
}

.macro-name-input {
  width: 60px;
  font-size: 10.5px;
  font-family: inherit;
  background: var(--bg-raised);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 3px;
}

aside h2 {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  margin: 0 0 6px;
}

.count {
  color: var(--accent);
  font-weight: 400;
}

/* The plugin scan's running count and the plugin being probed, in the plugins heading. Unlike the
   other .dim labels this one has to be able to shrink: a plugin's filename is as long as its
   vendor felt like making it, and the heading is the width of the panel. */
#pluginScan {
  flex-shrink: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  text-transform: none;
}

.panel {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px;
  font-size: 12px;
  word-break: break-word;
  color: var(--text-dim);
  box-shadow: 0 1px 4px color-mix(in srgb, var(--shadow) 40%, transparent);
}

.panel.list {
  flex: 1;
  min-height: 60px;
  overflow-y: auto;
  padding: 4px;
}

#paramSearch,
#pluginSearch,
#sampleSearch,
#fileSearchInput {
  width: 100%;
  margin-bottom: 6px;
  padding: 6px 10px;
  font-size: 12px;
  font-family: inherit;
  color: var(--text);
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  outline: none;
}

#paramSearch:focus,
#pluginSearch:focus,
#sampleSearch:focus,
#fileSearchInput:focus {
  border-color: var(--accent);
}

/* ------------------------------------------------------------ files tab */

#filesSection {
  flex: 1;
  padding-bottom: 10px;
}

.file-save-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap; /* save / save as / export, in a sidebar that can be narrow */
}

.file-save-row button {
  margin-bottom: 6px;
  padding: 4px 12px;
  font-size: 12px;
}

.file-save-row button.small {
  padding: 4px 8px;
}

.file-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px;
  border-radius: 4px;
  cursor: pointer;
  color: var(--text);
  font-family: var(--mono);
  font-size: 11.5px;
}

.file-row:hover {
  background: var(--accent-soft);
}

/* The row the editor's buffer came out of: the pattern `save` writes to, or - while the buffer
   has no name yet - the session autosaving it. A bar down the side rather than a fill, so it
   still reads as the current one while some other row is hovered. */
.file-row.current {
  background: var(--accent-soft);
  box-shadow: inset 2px 0 0 var(--accent);
}

.file-row.current .file-label {
  color: var(--accent);
  font-weight: 600;
}

/* A save landed on this row. Outline rather than border so nothing reflows, and the green melts
   straight into the accent tint the current row already sits on - animating to `transparent`
   instead would dip the row to the panel background and then snap it back up to the accent. */
@keyframes saved-flash {
  from {
    outline-color: var(--ok);
    background-color: color-mix(in srgb, var(--ok) 28%, transparent);
  }

  to {
    outline-color: transparent;
    background-color: var(--accent-soft);
  }
}

.file-row.saved-flash {
  outline: 2px solid transparent;
  outline-offset: -2px;
  animation: saved-flash 600ms ease-out;
}

/* The same confirmation for the buffer itself - the one that's on screen whether the sidebar is
   showing the files tab, showing something else, or collapsed altogether. Mostly a colored frame:
   this fires on every Cmd+S, so it has to register without washing out the code. The color is a
   variable because the same overlay says two things: green for a save, red for an error logged
   while the console is collapsed (see logLine) - one pulse, two meanings, told apart by color. */
.save-flash {
  --flash: var(--ok);
  position: absolute;
  inset: 0;
  z-index: 5;
  opacity: 0;
  pointer-events: none;
  outline: 2px solid var(--flash);
  outline-offset: -2px;
  background: color-mix(in srgb, var(--flash) 8%, transparent);
}

.save-flash.saved-flash {
  animation: saved-flash-fade 600ms ease-out;
}

.save-flash.error-flash {
  --flash: var(--err);
  animation: saved-flash-fade 600ms ease-out;
}

@keyframes saved-flash-fade {
  from { opacity: 1; }
  to { opacity: 0; }
}

/* Two lines per row: what the pattern calls itself, then its file name / @by / @tags / date,
   dim underneath. Stacking them keeps a long tag list from squeezing the name in a narrow
   sidebar - both lines ellipsize instead of wrapping. */
.file-row .file-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.file-row .file-label,
.file-row .file-meta {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-row .file-meta {
  color: var(--text-dim);
  font-size: 10px;
}

.file-row button {
  flex-shrink: 0;
  visibility: hidden;
}

.file-row:hover button {
  visibility: visible;
}

/* the search box belongs to the lists below it, not to the save row above it */
#fileSearchInput {
  margin-top: 2px;
}

/* saved patterns get the larger share; work-in-progress sessions scroll in their own panel */
#fileList {
  flex: 3;
}

#wipList {
  flex: 2;
}

/* replays are a night each - a handful at most once the retention has done its work */
#replayList {
  flex: 1;
}

#wipHeading,
#replayHeading {
  margin-top: 10px;
}

.wip-month > summary {
  padding: 3px 6px;
  color: var(--text-dim);
  font-size: 10.5px;
  cursor: pointer;
  user-select: none;
}

.wip-month > summary:hover {
  color: var(--text);
}

.files-note {
  margin-top: 6px;
  font-size: 10.5px;
  font-style: italic;
  color: var(--text-dim);
}

.settings-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text);
}

.settings-row select,
.settings-row input {
  flex: 1;
  min-width: 0;
}

/* a checkbox row: the box keeps its natural size (settings-row stretches its inputs) and the
   whole label is clickable. Laid out like a settings-row whether or not it also carries that
   class - as an inline label the box sat on the text's baseline, a hair above the words. */
.settings-check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text);
  cursor: pointer;
}

/* the live half of a check row's label (a peer count, a status word): present or absent without
   moving the words before it */
.settings-aside {
  color: var(--text-dim);
}

/* a text-only check row under a field row: the field's box is taller than its text, so without
   this the next row's words sit right against its bottom edge */
.settings-row:not(.settings-check) + .settings-check {
  margin-top: 6px;
}

.settings-check input {
  flex: none;
  margin: 0;
  accent-color: var(--accent);
}

.settings-actions {
  display: flex;
  gap: 6px;
  margin-top: 6px;
}

/* The projector feed's address and its copy button, which are only there while the feed is on.
   Scoped, like every other panel here - there is no global .hidden utility. The address is a
   .settings-note so it ellipsizes rather than widening the sidebar, and it is selectable because
   the copy button cannot reach the clipboard everywhere. */
#projectorSection .hidden {
  display: none;
}

#projectorFeedUrl {
  font-family: var(--mono);
  user-select: text;
}

/* a settings row whose control is a block (a list) rather than a single inline field */
.settings-row-block {
  display: block;
  margin-top: 10px;
}

/* the extra-inputs device list: checkboxes stacked under their label, scrolling once there are
   more devices than fit rather than pushing the rest of the panel down */
.check-list {
  margin-top: 4px;
  max-height: 132px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 4px 6px;
  color: var(--text-dim);
}

.check-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
  color: var(--text);
  cursor: pointer;
}

.check-row input {
  flex: none;
  margin: 0;
  accent-color: var(--accent);
}

/* the resolved channel-range line under the device list - live state, not hint text */
.settings-note {
  flex: 1;
  min-width: 0;
  font-size: 11px;
  color: var(--text-dim);
  align-self: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A fault light for the audio device, not a hint: it only ever has text when the combined device
   has actually degraded, and :empty collapses it away entirely the rest of the time. Wraps, unlike
   .settings-note - this one is a sentence you have to be able to read all of. */
/* Amber, like the console line it echoes (#log .warn): every message that lands here is
   something still playing that wants rebuilding, not something that failed. */
.settings-warning {
  font-size: 11px;
  line-height: 1.45;
  color: var(--warn);
  margin-top: 8px;
  padding: 6px 8px;
  border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
  border-radius: 6px;
  background: color-mix(in srgb, var(--warn) 8%, transparent);
}

.settings-warning:empty {
  display: none;
}

#editorSection,
#librarySection {
  margin-top: 14px;
}

.track-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 4px 2px;
}

.track-head:not(:first-child) {
  margin-top: 8px;
  border-top: 1px solid var(--border);
  padding-top: 8px;
}

.track-name {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 700;
  color: var(--accent);
}

.badge {
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 1px 7px;
  border-radius: 99px;
  border: 1px solid var(--border);
}

.badge-muted {
  color: var(--text-dim);
  background: var(--hover-bg);
}

.badge-solo {
  color: var(--warn);
  background: var(--warn-soft);
  border-color: color-mix(in srgb, var(--warn) 45%, transparent);
}

.chain-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--text);
  padding: 2px 4px;
}

.chain-params {
  margin-top: 4px;
  padding: 2px 4px;
  font-size: 11px;
  color: var(--warn);
}

.slot-head {
  position: sticky;
  top: 0;
  background: var(--bg-panel);
  padding: 5px 6px 3px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
}

/* pack headers in the samples list are sticky like .slot-head but also clickable */
.sample-pack-head {
  cursor: pointer;
}

.sample-pack-head:hover {
  background: var(--hover-bg);
}

.param-row,
.plugin-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 3px 6px;
  border-radius: 4px;
  cursor: pointer;
  color: var(--text);
  font-family: var(--mono);
  font-size: 11.5px;
}

.param-row:hover,
.plugin-row:hover {
  background: var(--accent-soft);
}

/* Sample row lit up while its preview is auditioning (see previewSample). */
.sample-row.previewing,
.sample-row.previewing:hover {
  background: var(--accent-strong);
  color: var(--accent);
}

.dim {
  color: var(--text-dim);
  font-size: 10.5px;
  flex-shrink: 0;
}

.more-note {
  padding: 6px;
  font-size: 11px;
  font-style: italic;
  color: var(--text-dim);
}

.more-note-click {
  cursor: pointer;
  border-radius: 4px;
}

.more-note-click:hover {
  background: var(--accent-soft);
  color: var(--text);
}

/* --------------------------------------------------------------------- log */

footer#console {
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--border);
  background: var(--bg-raised);
}

.console-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 14px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

#log {
  height: 110px;
  overflow-y: auto;
  padding: 0 14px 6px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-dim);
}

#log .error {
  color: var(--err);
}

/* Something worked, differently than you'd expect - not the red that means it did not happen. */
/* A fix offered on a console line (see client.js, attachLogFix). */
#log .log-fix {
  margin-left: 6px;
  padding: 0 6px;
  font-size: 11px;
}

#log .warn {
  color: var(--warn);
}

/* A line about a span of code: clicking it puts the cursor there (see client.js's attachLogPlace). */
#log .log-at {
  cursor: pointer;
}

[data-console-collapsed] #log {
  display: none;
}

/* -------------------------------------------------- CodeMirror: poptart theme */

/* The plain textarea CodeMirror upgrades on load. Until client.js runs - a handful of vendor
   scripts later - this is what the editor pane paints, so it wears the editor's own colors;
   left unstyled it flashed as a white sheet of black text on every refresh. CodeMirror hides it
   once it's up, so this only ever shows for that first frame. */
#editor {
  flex: 1;
  width: 100%;
  padding: 10px 4px 10px 44px; /* roughly where the line-number gutter leaves the first column */
  font-family: var(--mono);
  font-size: 14px;
  line-height: 1.6;
  color: var(--text);
  background: var(--bg);
  border: none;
  outline: none;
  resize: none;
}

/* Even in the right colors, that first frame showed the pattern as flat unhighlighted text, then
   repainted it in color, then - on a share/history URL, whose buffer arrives over the network -
   replaced it outright with the file actually being opened. The pane keeps its space and its
   background until all of that has settled, and the code appears once, already highlighted and
   already the right file. */
[data-booting] #editor,
[data-booting] #editorPane .CodeMirror {
  visibility: hidden;
}

.CodeMirror {
  flex: 1;
  height: 100%;
  font-family: var(--mono);
  font-size: 14px;
  line-height: 1.6;
}

.cm-s-poptart.CodeMirror {
  background: var(--bg);
  color: var(--text);
  padding: 10px 4px;
}

.cm-s-poptart .CodeMirror-gutters {
  background: var(--bg);
  border-right: none;
}

.cm-s-poptart .CodeMirror-linenumber {
  color: var(--linenumber);
  padding: 0 12px 0 20px;
}

/* The GROUP carets: their own gutter between the line numbers and the code, where an editor's fold
   controls have always been (see foldFamilies in client.js). A fixed width, so the code's left edge
   holds still whether or not the song has a group in it - a gutter that sized to its content would
   shift the whole buffer sideways the moment you made one. */
.cm-s-poptart .poptart-group-fold {
  width: 14px;
}

.cm-group-caret {
  display: block;
  width: 14px;
  font-size: 9px;
  text-align: center;
  color: var(--linenumber);
  cursor: pointer;
  user-select: none;
}

.cm-group-caret:hover {
  color: var(--accent);
}

/* Folded, the caret is the only thing on screen saying the members are still down there, so it
   stays as legible as the code rather than fading back with the line numbers. */
.cm-group-caret-folded {
  color: var(--text-dim);
}

.cm-s-poptart .CodeMirror-cursor {
  border-left: 2px solid var(--accent);
}

.cm-s-poptart .CodeMirror-selected {
  background: var(--selection);
}

.cm-s-poptart .CodeMirror-matchingbracket {
  color: var(--accent) !important;
  outline: 1px solid var(--accent-soft);
  background: var(--accent-soft);
}

.cm-s-poptart .cm-string {
  color: var(--syn-string);
}

.cm-s-poptart .cm-number {
  color: var(--syn-number);
}

.cm-s-poptart .cm-keyword,
.cm-s-poptart .cm-operator {
  color: var(--syn-keyword);
}

.cm-s-poptart .cm-variable {
  color: var(--syn-variable);
}

.cm-s-poptart .cm-property {
  color: var(--syn-property);
}

/* A track label inside a group's braces: JavaScript calls it a property, but it is a label like
   the top-level ones around it and wears their color (marked by client.js's markMemberLabels).
   !important because the token's own .cm-property color sits on the same span. */
.cm-s-poptart span.cm-member-label {
  color: var(--syn-variable) !important;
}

.cm-s-poptart .cm-comment {
  color: var(--syn-comment);
  font-style: italic;
}

/* The label of the track conf is writing into (see client.js's updateMutedDim) - the sidebar's
   lit conf button, on the code itself. */
.cm-s-poptart span.cm-conf-label {
  background: var(--accent, #d08770);
  color: #1a1a1a !important;
  border-radius: var(--r-xs);
}

/* muted / un-soloed blocks (see client.js's updateMutedDim) - rendered like comments: one dim
   color, no syntax highlighting, so only code that's actually playing is lit up. !important
   beats every per-token color above. */
.cm-s-poptart span.cm-muted-code {
  color: var(--syn-comment) !important;
  font-style: italic;
}

/* the mini-notation atom currently sounding (see client.js's highlight ticker) - a plain
   box around the element ("white" = the theme's text color, so it stays
   visible on light themes too) */
.cm-playing {
  box-shadow: 0 0 0 1.5px var(--text);
  border-radius: 2px;
}

/* A slot that is NOT playing its `.preset(...)` pattern right now - its plugin's own window is
   open, or the preset panel has it. What is marked is the preset actually loaded, and the playback
   highlighter leaves the whole call alone while it is (see client.js's syncHeldPresets), so "held
   on this one" and "cycling through these" can never look the same. Yellow rather than the
   playing box: it is a different fact about the code, not a louder version of the same one. */
.cm-held {
  background: color-mix(in srgb, var(--warn) 25%, transparent);
  box-shadow: 0 0 0 1.5px var(--warn);
  border-radius: 2px;
}

/* Where the last evaluation failed, or a warning points (see eval-marks.js): a wavy underline on
   the span, and the line number in the same color. Neither takes any room, so nothing moves. */
.cm-eval-error {
  text-decoration: underline wavy var(--err);
  text-decoration-skip-ink: none;
  text-underline-offset: 3px;
}

.cm-eval-warn {
  text-decoration: underline wavy var(--warn);
  text-decoration-skip-ink: none;
  text-underline-offset: 3px;
}

.cm-s-poptart .cm-eval-error-line .CodeMirror-linenumber {
  color: var(--err);
}

.cm-s-poptart .cm-eval-warn-line .CodeMirror-linenumber {
  color: var(--warn);
}

/* The name of a call that opens an editor - `pianoroll`, `lfo`, a chain's `synth` - while the
   pointer is over it (see updateHandleMark in client.js). Only on hover: those names are on most
   lines of a song, and underlining them at rest would read as a page of links. Dotted rather than
   solid, and the text cursor is deliberately left alone, because the gesture is a DOUBLE click.
   Accent, which is what everything else here turns on hover. */
.cm-widget-handle {
  text-decoration: underline dotted var(--accent);
  text-underline-offset: 3px;
}

/* folded config blobs (captured plugin state, long lfo shapes) - click to expand */
.cm-config-fold {
  display: inline-block;
  padding: 0 6px;
  border-radius: 8px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  color: var(--accent);
  cursor: pointer;
  font-size: 0.85em;
  vertical-align: baseline;
}
.cm-config-fold:hover {
  border-color: var(--accent);
}

/* find bar (cmd+F): the editor addon's own dialog, in the app's colors */

.CodeMirror-dialog {
  background: var(--bg-panel);
  color: var(--text);
  font-family: var(--mono);
  font-size: 12px;
  padding: 4px 10px;
  z-index: 20;
}

.CodeMirror-dialog-top {
  border-bottom: 1px solid var(--border);
}

.CodeMirror-dialog input {
  color: var(--text);
  font-family: var(--mono);
  font-size: 12px;
}

/* autocomplete popup */

.CodeMirror-hints {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px var(--shadow);
  font-family: var(--mono);
  font-size: 12px;
  padding: 4px;
  max-height: 22em;
  /* Over the dialogs (40) and the panels (30 and up): the addon's own 10 put the list behind any
     dialog's backdrop. The doc box rides at the same height. */
  z-index: 100;
}

.CodeMirror-hint {
  color: var(--text);
  border-radius: 4px;
  padding: 3px 8px;
  /* rows carry a signature - cap them and clip the overflow rather than letting one long
     line stretch the popup (the doc panel beside it has the full text) */
  max-width: 24em;
  overflow: hidden;
  text-overflow: ellipsis;
}

li.CodeMirror-hint-active {
  background: var(--accent-soft);
  color: var(--accent);
}

/* a documented completion: name then signature */

.hint-args {
  color: var(--text-dim);
}

/* documentation tooltips - the panel beside the popup and the ctrl-hover one (settings ->
   editor -> documentation tooltips turns both off) */

.doc-box {
  position: fixed;
  z-index: 100; /* beside the hint list, at its height */
  max-width: 26em;
  padding: 8px 10px;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 8px 24px var(--shadow);
  pointer-events: none;
}

.doc-sig {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--accent);
}

.doc-desc {
  margin-top: 5px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text);
}

.doc-eg {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-dim);
  white-space: pre-wrap;
}

/* ------------------------------------------------------------- theme editor */

.theme-panel {
  position: fixed;
  top: 52px;
  right: calc(var(--sidebar-w) + 16px); /* it stops where the sidebar begins, collapsed or not */
  width: 250px;
  max-height: calc(100vh - 80px);
  display: flex;
  flex-direction: column;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  box-shadow: 0 12px 36px var(--shadow);
  z-index: 20;
  overflow: hidden;
}

.theme-panel.hidden {
  display: none;
}

.theme-panel-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 12px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  border-bottom: 1px solid var(--border);
}

.theme-vars {
  overflow-y: auto;
  padding: 6px 8px;
}

.theme-var-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 3px 6px;
  border-radius: 6px;
  font-size: 12px;
  cursor: pointer;
}

.theme-var-row:hover {
  background: var(--hover-bg);
}

.theme-var-row input[type='color'] {
  width: 34px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: none;
  cursor: pointer;
}

.theme-save-row {
  display: flex;
  gap: 6px;
  padding: 8px 12px;
  border-top: 1px solid var(--border);
}

.theme-save-row input {
  flex: 1;
  min-width: 0;
  padding: 6px 10px;
  font-size: 12px;
  font-family: inherit;
  color: var(--text);
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  outline: none;
}

.theme-save-row input:focus {
  border-color: var(--accent);
}

.theme-save-row input:disabled {
  opacity: 0.5;
}

.theme-save-row button {
  padding: 4px 12px;
  font-size: 12px;
}

/* Import / export, under save: the same footer, so no rule between them. */
.theme-file-row {
  border-top: none;
  padding-top: 0;
}

.theme-file-row button {
  flex: 1;
}

/* --------------------------------------------------------------- folder picker */

.dir-picker-backdrop {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--shadow);
  z-index: 40;
}

.dir-picker-backdrop.hidden {
  display: none;
}

/* The folder picker is opened FROM other dialogs (the pack panel's map asks for folders), so it
   sits one step above the rest of them rather than wherever the markup happens to put it. */
#dirPickerBackdrop {
  z-index: 41;
}

.dir-picker {
  display: flex;
  flex-direction: column;
  width: min(480px, 92vw);
  max-height: 70vh;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  box-shadow: 0 12px 36px var(--shadow);
  overflow: hidden;
}

.dir-picker-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 12px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  border-bottom: 1px solid var(--border);
}

.dir-picker-path {
  margin: 10px 12px 6px;
  font-size: 12px;
}

.dir-picker-list {
  flex: 1;
  overflow-y: auto;
  padding: 2px 8px;
  min-height: 120px;
}

.dir-row {
  padding: 5px 8px;
  border-radius: 6px;
  font-size: 12.5px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dir-row::before {
  content: '📁 ';
}

.dir-row.dir-up::before {
  content: '';
}

.dir-row:hover {
  background: var(--hover-bg);
}

.dir-up {
  color: var(--text-dim);
}

.dir-empty {
  padding: 10px 8px;
  font-size: 11px;
  font-style: italic;
  color: var(--text-dim);
}

.dir-picker-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-top: 1px solid var(--border);
}

.dir-picker-note {
  font-size: 10.5px;
  color: var(--err);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.name-dialog-input {
  margin: 12px 12px 6px;
  padding: 6px 10px;
  font-size: 12px;
  font-family: var(--mono); /* it's a file name - show it in the font it will be listed in */
  color: var(--text);
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  outline: none;
}

.name-dialog-input:focus {
  border-color: var(--accent);
}

/* The name dialog's note is ordinary information ("that name is taken, save will replace it"),
   not the failure .dir-picker-note reports - dim by default, and only amber when it's warning
   that confirming would overwrite something. */
.name-dialog-note {
  font-size: 10.5px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.name-dialog-note.warn {
  color: var(--warn);
}

.name-dialog-note.error {
  color: var(--err);
}

/* --------------------------------------------------------------- prebake editor */

.prebake-dialog {
  display: flex;
  flex-direction: column;
  width: min(760px, 94vw);
  height: min(560px, 82vh);
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  box-shadow: 0 12px 36px var(--shadow);
  overflow: hidden;
}

/* The panel's own CodeMirror fills the space between the header and footer. */
.prebake-dialog .CodeMirror {
  flex: 1;
  height: auto;
  font-size: 13px;
}

.prebake-note {
  font-size: 10.5px;
  font-style: italic;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.prebake-note.error {
  color: var(--err);
  font-style: normal;
}

/* --------------------------------------------------------------- lfo editor */

/* Centered and sized like the piano roll panel, deliberately: they are the same kind of thing -
   a drawing surface you open over the code - and one of them appearing somewhere else each time
   is the sort of small inconsistency that costs a beat to re-find mid-set. */
/* The preset panel: the same centered chrome as the lfo/record panels, but with nothing to draw -
   a preset is a plugin's own program, so all this holds is the picker, what the preset is for, and
   the button that captures one. */
.preset-panel {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 420px;
  max-width: calc(100vw - 32px);
  display: flex;
  flex-direction: column;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  box-shadow: 0 24px 64px var(--shadow), 0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent);
  z-index: 30;
  overflow: hidden;
}

/* Scoped, like every other panel here - there is no global .hidden utility. */
.preset-panel.hidden,
.preset-panel .hidden {
  display: none;
}

.preset-panel-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 10px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  border-bottom: 1px solid var(--border);
}

.preset-body {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 14px 12px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text-dim);
  border-top: 1px solid var(--border);
  margin-top: 10px;
}

/* Which plugin, on which track, this preset is pointed at - the one thing a program needs said
   about it, since it means nothing anywhere else. */
.preset-target {
  color: var(--accent);
}

/* Opening this panel stops the pattern swapping this one slot, so that the preset you picked is
   the one you can hear and edit. That is a change to what is coming out of the speakers, so it is
   stated in the head rather than left to be inferred from the sound not changing. */
.preset-held {
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--accent);
  background: var(--accent-soft);
}

/* The search box and the list ARE the panel: finding, picking, naming and throwing away presets is
   what anyone opens this for, so none of it sits behind a disclosure. */
.preset-search {
  margin: 12px 14px 0;
  padding: 6px 9px;
  font-size: 13px;
  font-family: var(--mono);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  outline: none;
}

.preset-search:focus {
  border-color: var(--accent);
}

.preset-list {
  max-height: 20em;
  margin: 6px 8px 0;
  padding: 0 6px;
}

/* The one being edited, marked so it stays findable while the arrow keys move the highlight
   somewhere else. */
.preset-list .def-pick-row.current .def-pick-name::after {
  content: ' ●';
  color: var(--accent);
}

.preset-status {
  padding: 0 14px 12px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text-dim);
}

.preset-status:empty {
  display: none;
}

.preset-status.ok {
  color: var(--accent);
}

.preset-status.bad {
  color: var(--err);
}

/* The generated device window. It borrows the preset panel's chrome and the desk's knob, so the
   only thing written here is the grid they sit in: a device with thirty parameters has to stay a
   window somebody can find a control in, not a wall. */
.device-panel {
  width: 560px;
}

.device-sections {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px 0 2px;
  max-height: 78vh;
  overflow-y: auto;
}

.device-section-head {
  padding: 8px 10px 2px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

/* Captions are read, not selected. A drag that begins on a knob's name must not start a text
   selection, and a panel whose labels turn blue on a stray click reads as a web page rather
   than an instrument. The inputs inside a number box keep their own selection: this is on the
   text elements, not on the container. */
.device-section-head,
.device-widget-name,
.device-widget-value,
.device-figure-head,
.device-figure-canvas {
  user-select: none;
  -webkit-user-select: none;
}

.device-widgets {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 8px 10px;
  padding: 4px 10px 8px;
}

/* The sections a device lays out side by side: each row a flex line, each section a column
   that shares the width with its neighbors. Two oscillators are twins and read as twins. */
.device-row {
  display: flex;
  align-items: flex-start;
  gap: 4px;
  padding: 0 4px;
}

.device-section {
  flex: 1 1 0;
  min-width: 0;
}

.device-row + .device-row {
  border-top: 1px solid var(--border);
  margin-top: 4px;
  padding-top: 2px;
}

/* One control: the knob (or select, toggle or number) with its name over it and its value under it. */
.device-widget {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  width: 64px;
}

/* A number box: the value itself, dragged up and down or typed. The knob's height, so a row of
   knobs and boxes lines up. */
.device-number {
  /* Digits of one width, so a number dragged past a power of ten does not shuffle sideways. */
  font-variant-numeric: tabular-nums;
  width: 100%;
  height: 28px;
  box-sizing: border-box;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-raised);
  font-family: var(--mono);
  font-size: 10px;
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: ns-resize;
  touch-action: none;
  user-select: none;
}

.device-number.dragging {
  border-color: var(--accent);
}

.device-number input {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: center;
  outline: none;
}

/* The name of the file a control is pointed at, in place of a menu of two thousand: a field in
   the device selects' own box, carrying their chevron - it is a choice, and a bare name beside a
   figure's title did not look like something to click (the Convolver's impulse went unfound).
   An arrow either side steps to the file before or after it. */
.device-file-box {
  display: flex;
  align-items: stretch;
  gap: 4px;
  width: 100%;
  min-width: 0;
}

.device-widget .device-file {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  text-align: left;
  padding: 4px 22px 4px 9px;
  border-radius: 5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23888e99' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 8px 5px;
}

.device-widget .device-file-step {
  flex: none;
  width: 24px;
  padding: 0;
  font-size: 14px;
  line-height: 1;
  border-radius: 5px;
  color: var(--text-dim);
}

.device-widget .device-file-step:hover {
  color: var(--text);
}

.device-widget-name {
  font-size: 9px;
  color: var(--text-dim);
  line-height: 1.1;
  text-align: center;
}

.device-widget-value {
  font-variant-numeric: tabular-nums;
  font-size: 9px;
  font-family: var(--mono);
  color: var(--text);
  line-height: 1;
}

/* A control something else is already moving. Turning it would be overwritten on the next block,
   so it does not turn, and what is moving it is named where its value would be. */
.device-widget.driven .mix-knob {
  opacity: 0.45;
  cursor: not-allowed;
}

.device-widget.driven .device-widget-value {
  color: var(--accent);
}

.device-widget select,
.device-widget .device-toggle {
  width: 100%;
  font-size: 10px;
}

/* A menu is its word: wider than a knob, and its chevron pulled in so the word has the room. */
.device-widget-enum {
  width: 96px;
}

.device-widget select {
  padding: 5px 20px 5px 8px;
  background-position: right 7px center;
  background-size: 8px 5px;
  border-radius: 5px;
  text-overflow: ellipsis;
}

.device-widget .small.on,
.iw-switch.on {
  color: var(--accent);
  border-color: var(--accent);
}

/* A figure: a picture of what some of the knobs above it add up to. Same gutter as a widget row,
   so a curve lines up with the controls it belongs to. */
.device-figure {
  padding: 2px 10px 6px;
}

/* Whose DSP a ported device is, said on its window rather than only on a tooltip. */
.device-credit {
  font-size: 9px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 45%;
}

.device-credit a {
  color: inherit;
}

.device-figure-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding-bottom: 2px;
}

/* The readout sits at the right-hand end whatever else is on the heading. */
.device-figure-head .device-figure-read {
  margin-left: auto;
}

/* A control a figure took over, drawn on its heading: the control alone, taking the room the
   readout leaves it. */
.device-widget-bare {
  display: flex;
  flex-direction: row; /* a knob cell stacks its name over the control; a bare one has no name */
  align-items: center;
  flex: 1;
  width: auto;
  min-width: 0;
}

.device-figure-head:has(.device-widget-bare) {
  align-items: center;
  padding-bottom: 6px;
}

.device-figure-title {
  font-size: 9px;
  color: var(--text-dim);
}

/* What the picture currently reads, in the same mono the knobs print their values in. */
.device-figure-read {
  flex: none; /* the readout keeps its width; the control beside it gives way */
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  font-size: 9px;
  font-family: var(--mono);
  color: var(--text);
  text-align: right;
}

.device-figure-canvas {
  display: block;
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 3px;
  /* Every figure poptart draws is draggable, and most of them on both axes at once, so it is the
     crosshair rather than the knobs' ns-resize. */
  cursor: crosshair;
  touch-action: none;
}

/* Something else is moving a parameter this picture is drawn from. The picture still moves, and
   the gesture still works on every part of it that is not driven - so the ban cursor is set by the
   client over the driven piece alone (see figureRefuses), never on the whole canvas. */
.device-figure.driven .device-figure-read {
  color: var(--accent);
}

/* The browser build has no plugins to scan for - its devices are the catalog, fixed when the page
   was built - so the button that would rescan them is not drawn. */
.no-scan #scanBtn {
  display: none;
}

/* Nor a projector feed: that is a server handing the frame to another program on the same
   machine, and the browser build has no server. The projector window itself works there. */
.web-build #projectorFeedRow,
.web-build #projectorFeedActions {
  display: none;
}

/* The wavetable browser: folders, files and a picture of the table, in one row. Wider than the
   preset panel it borrows its chrome from, because three columns is what it is. */
/* The file picker: folders, files, and a picture of the one selected. The three columns are
   given one height rather than three of their own, so their boxes line up. */
.file-pick-panel {
  width: 720px;
}

.file-pick-body {
  display: flex;
  align-items: stretch;
  gap: 8px;
  padding: 8px 14px;
  height: 268px;
  box-sizing: content-box;
}

.file-pick-col {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 190px;
}

.file-pick-col input {
  width: 100%;
  box-sizing: border-box;
}

.file-pick-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg);
  font-size: 11px;
}

.file-pick-files {
  flex: 1 1 0;
  min-width: 0;
}

.file-pick-list div {
  padding: 3px 8px;
  cursor: default;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.file-pick-list div:hover {
  background: var(--bg-raised);
}

.file-pick-list div.on {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--text);
}

.file-pick-count {
  color: var(--text-dim);
  margin-left: 6px;
}

.file-pick-preview {
  width: 220px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.file-pick-preview canvas {
  display: block;
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 3px;
}

.lfo-panel,
.groove-panel {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: fit-content;
  max-width: calc(100vw - 32px);
  display: flex;
  flex-direction: column;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  box-shadow: 0 24px 64px var(--shadow), 0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent);
  z-index: 30;
  overflow: hidden;
}

.lfo-panel.hidden,
.groove-panel.hidden {
  display: none;
}

/* Scoped, like the piano roll's - there is no global .hidden utility. */
.lfo-panel .hidden,
.groove-panel .hidden {
  display: none;
}

.lfo-panel-head,
.groove-panel-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 10px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  border-bottom: 1px solid var(--border);
}

/* Header dropdowns share ONE treatment - see the rule beside .pianoroll-ctl select. Only the type
   size is this panel's own. */
.lfo-panel-head select,
.groove-panel-head select {
  font-size: 11px;
}

#lfoCanvas,
#grooveCanvas {
  display: block;
  margin: 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  cursor: crosshair;
  outline: none;
  touch-action: none;
}

.lfo-controls,
.groove-controls {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 12px 8px;
  font-size: 12px;
  color: var(--text-dim);
}

.lfo-controls label,
.groove-controls label {
  display: flex;
  align-items: center;
  gap: 6px;
}

.lfo-controls input[type='number'],
.groove-controls input[type='number'] {
  width: 62px;
  padding: 3px 6px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  outline: none;
}

.lfo-controls input[type='number']:focus,
.groove-controls input[type='number']:focus {
  border-color: var(--accent);
}

.lfo-controls select,
.groove-controls select {
  padding: 3px 22px 3px 8px;
  background-position: right 8px center;
  font-size: 12px;
}

/* The env() editor: the shape editor's chrome around a device window's envelope figure and its
   number boxes - the same picture and the same boxes, opened from the code instead of a device. */
/* Five boxes of a device widget's width, their gaps and the gutter: one row, never two. */
.env-mod-panel {
  width: 400px;
}

.env-mod-panel .device-figure {
  padding: 10px 10px 4px;
}

.env-mod-panel .device-widgets {
  justify-content: space-between;
  padding: 4px 10px 10px;
}

/* A setting the code writes as something other than a number: its text, read-only, in the accent
   a driven control is named in. Cut to the box, so a long pattern does not widen the row. */
.env-mod-panel .device-widget.driven .device-number {
  display: block;
  padding: 0 4px;
  line-height: 26px;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--accent);
  cursor: default;
}

/* The color picker: the shape editor's chrome at the env() editor's width. Both tabs are one
   height, so switching between them moves nothing; a long "in this song" row scrolls inside. */
.color-panel {
  width: 400px;
}

.color-panel .lfo-panel-head .side-tab {
  font-weight: 400;
}

.color-tab-body {
  box-sizing: border-box;
  height: 290px;
  overflow-y: auto;
}

/* The wheel, drawn borderless: the colors run to its rim. */
#colorWheel {
  display: block;
  width: 200px;
  height: 200px;
  margin: 10px auto;
  cursor: crosshair;
  touch-action: none;
}

.color-controls {
  justify-content: center;
}

.color-controls .pr-box-slider {
  width: 62px;
}

/* Seven characters of the mono face: a whole "#rrggbb", never cut. */
.color-controls input[type='text'] {
  box-sizing: content-box; /* the 7ch is the text's; padding and border sit outside it */
  width: 7ch;
  padding: 3px 6px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  outline: none;
}

.color-controls input[type='text']:focus {
  border-color: var(--accent);
}

/* A color to pick: a step cell of the piano roll's, in the color. An empty recent slot keeps its
   place, outlined, so the row never shifts as picks arrive. */
.color-swatch {
  flex: none;
  box-sizing: border-box;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
}

.color-swatch:hover,
.color-swatch:focus-visible {
  border-color: var(--text);
}

.color-swatch.empty {
  border-style: dashed;
  background: none;
  cursor: default;
}

span.color-swatch {
  cursor: default;
}

.color-group {
  display: grid;
  grid-template-columns: 96px 1fr;
  align-items: start;
  gap: 6px;
  padding: 4px 12px;
  font-size: 12px;
  color: var(--text-dim);
}

.color-group-name {
  line-height: 16px;
}

.color-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

#colorPaletteTab {
  padding: 6px 0;
}

/* The groove lanes docked under the roll's grid: a strip in the panel's own flow, the way the
   value lane lives inside the canvas above it. The canvas keeps #grooveCanvas's own face; only its
   floating margin goes, since the strip's padding is the spacing here and the left offset is set
   inline to land the lanes flush under the grid (see grooveDockLayout). */
.pr-groove-strip {
  padding: 0 12px 12px;
}

.pr-groove-strip.hidden {
  display: none;
}

.pr-groove-strip #grooveCanvas {
  margin: 0;
}

/* A box slider with nothing to act on - the accents knob on a roll that swings plainly. Same
   dimming the app's disabled buttons wear; still visible, so the column never reflows. */
.pr-box-slider.disabled {
  opacity: 0.45;
  pointer-events: none;
}

/* The grid select sits in the head, beside the title - the same row treatment the controls row
   gives its labels (see .lfo-controls label). */
.groove-panel-head label {
  display: flex;
  align-items: center;
  gap: 6px;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
}

/* The box sliders take the width the number fields beside them would have had. */
.lfo-controls .pr-box-slider,
.groove-controls .pr-box-slider {
  width: 62px;
}

.lfo-help {
  padding: 6px 12px 10px;
  font-size: 10.5px;
  font-style: italic;
  color: var(--text-dim);
}

/* --------------------------------------------- record panel (.record() / ctrl+b) */

.record-panel {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: fit-content;
  max-width: calc(100vw - 32px);
  display: flex;
  flex-direction: column;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  box-shadow: 0 24px 64px var(--shadow), 0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent);
  z-index: 30;
  overflow: hidden;
}

/* Scoped, like every other panel here - there is no global .hidden utility, so a bare
   class="hidden" on a child would do nothing at all. */
.record-panel.hidden,
.record-panel .hidden {
  display: none;
}

.record-panel-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 12px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 6%, var(--bg-panel)), var(--bg-panel));
  border-bottom: 1px solid var(--border);
}

.record-track {
  font-family: var(--mono);
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0;
  color: var(--accent);
}

/* The bitmap is twice the display size (see index.html): the waveform is drawn at ~2 buckets per
   screen pixel, which is what keeps a busy track from reading as one solid block. */
#recordCanvas {
  display: block;
  width: 760px;
  height: 200px;
  max-width: calc(100vw - 56px);
  margin: 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
}

.record-controls,
.record-actions {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 14px 10px;
  font-size: 12px;
  color: var(--text-dim);
}

.record-actions {
  padding-bottom: 14px;
}

/* The one line that says what happened. Empty (and invisible) until a bounce lands, so nothing
   permanent sits under the controls. */
.record-status {
  padding: 0 14px 12px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text-dim);
}

.record-status:empty {
  display: none;
}

.record-status.ok {
  color: var(--accent);
}

.record-status.bad {
  color: var(--err);
}

.record-controls label,
.record-check {
  display: flex;
  align-items: center;
  gap: 6px;
}

.record-controls input:not([type='checkbox']) {
  padding: 3px 6px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  outline: none;
}

.record-controls input[type='number'] {
  width: 56px;
}

.record-controls input[type='text'] {
  width: 140px;
}

.record-controls input:focus {
  border-color: var(--accent);
}

/* A ctrl+b panel has no .record() call to write settings back into, so its fields are shown but
   inert rather than hidden - what it recorded with still reads. */
.record-controls input:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.record-controls label:has(input:disabled) {
  opacity: 0.6;
}

/* Armed counts the take in, live is the take itself - the same two states the transport's
   record button uses, so one glance reads the same in both places. */
#recordGo.rec-armed {
  color: var(--warn);
  border-color: var(--warn);
}

#recordGo.rec-live {
  color: var(--err);
  border-color: var(--err);
}

/* --------------------------------------------- piano roll editor (centered modal) */

.pianoroll-panel {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  /* Fixed, not fit-content: minimizing the control column must not resize the panel around the
     grid - the grid takes the freed space instead (see #pianorollCanvas). 92 column + 660 grid +
     its 12px margins + the panel's own border, all border-box. */
  width: 778px;
  max-width: calc(100vw - 32px);
  display: flex;
  flex-direction: column;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  box-shadow: 0 24px 64px var(--shadow), 0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent);
  z-index: 30;
  overflow: hidden;
}

.pianoroll-panel.hidden {
  display: none;
}

/* Scoped, like every other panel here - there is no global .hidden utility, so a bare
   class="hidden" on a child (the roll picker, the follow lock) would do nothing at all. */
.pianoroll-panel .hidden {
  display: none;
}

#pianorollTitle {
  white-space: nowrap;
}

/* The roll picker doubles as the panel's title while a named roll is on screen, so its width is
   FIXED. Sized to its content it reflowed the whole header - and, once the buttons wrapped to a
   second line, resized the panel - every time the pattern moved from `0` to `chorus`, which is
   exactly when you are trying to read it. A longer name is clipped; the tooltip carries it whole. */
.def-pick-wrap {
  position: relative; /* the find-or-create popover hangs off the name */
}

/* The roll's name is a plain title until you touch it, and an input once you do - so reading it
   costs nothing and renaming is one click. Its width is FIXED: sized to its content, the header
   reflowed (and, once the buttons wrapped, resized the panel) every time the pattern moved from
   `lead` to `chorus`, which is exactly when you are trying to read it. */
.def-name {
  width: 7.5em;
  padding: 3px 7px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 5px;
  outline: none;
  text-overflow: ellipsis;
}

.def-name:hover {
  border-color: var(--border);
}

.def-name:focus {
  background: var(--bg);
  border-color: var(--accent);
}

.def-picker {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 5;
  width: 15em;
  padding: 5px;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 10px 28px var(--shadow);
}

.def-picker input {
  width: 100%;
  padding: 4px 7px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  outline: none;
}

.def-picker input:focus {
  border-color: var(--accent);
}

.def-pick-list {
  max-height: 14em;
  overflow-y: auto;
  margin-top: 4px;
}

.def-pick-row {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 4px 7px;
  border-radius: 5px;
  font-size: 12px;
  font-family: var(--mono);
  text-transform: none;
  letter-spacing: 0;
  color: var(--text);
  cursor: pointer;
}

/* One highlight for both ways of getting here - the arrow keys and the pointer. The chosen row
   also carries the glow every browser in the app shares (see .row-picked, under the organize
   modal) - so a selection reads the same here, in organize and in the snippet browser. */
.def-pick-row.on,
.def-pick-row:hover {
  background: var(--accent-soft);
  color: var(--accent);
}

/* The name takes the slack, so the note and the delete button sit against the right edge
   whether a row has one of them, both, or neither. */
.def-pick-row .def-pick-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.def-pick-row .def-pick-note {
  font-size: 10px;
  color: var(--text-dim);
}

/* Deleting is a real edit to the buffer, so it stays out of the way until you are on the row -
   the list is mostly used for finding a roll, not for throwing one away. */
.def-pick-row .def-pick-del {
  padding: 0 2px;
  font-size: 13px;
  line-height: 1;
  color: var(--text-dim);
  opacity: 0;
  cursor: pointer;
}

.def-pick-row.on .def-pick-del,
.def-pick-row:hover .def-pick-del {
  opacity: 0.7;
}

.def-pick-row .def-pick-del:hover {
  opacity: 1;
  color: var(--err);
}

/* "Play this one here" - writes the name into the call the panel is looking through. Kept to the
   same manners as the delete button (out of the way until you are on the row), since it is the same
   kind of thing: a second gesture on a row whose first gesture is opening it. */
.def-pick-row .def-pick-dup,
.def-pick-row .def-pick-use {
  padding: 0 2px;
  font-size: 13px;
  line-height: 1;
  color: var(--text-dim);
  opacity: 0;
  cursor: pointer;
}

.def-pick-row.on .def-pick-dup,
.def-pick-row:hover .def-pick-dup,
.def-pick-row.on .def-pick-use,
.def-pick-row:hover .def-pick-use {
  opacity: 0.7;
}

.def-pick-row .def-pick-dup:hover,
.def-pick-row .def-pick-use:hover {
  opacity: 1;
  color: var(--accent);
}

.def-pick-empty {
  padding: 4px 7px;
  font-size: 11px;
  font-family: var(--mono);
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-dim);
}

/* One row, always: the close button belongs in the top-right corner at every zoom and window
   size, so nothing here is allowed to wrap it onto a second line. The title/name/scale group
   takes the slack and ellipsizes (see .pianoroll-head-main); the tool cluster never shrinks. */
.pianoroll-panel-head {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 6px;
  padding: 9px 12px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 6%, var(--bg-panel)), var(--bg-panel));
  border-bottom: 1px solid var(--border);
}

/* Never given `overflow: hidden` - the find-or-create popover hangs out of this group, and a
   clipping ancestor would cut it off. The pieces inside shrink and ellipsize on their own. */
.pianoroll-head-main {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 auto;
  min-width: 0; /* so it gives way rather than pushing the tools off the edge */
}

.pianoroll-head-main > * {
  min-width: 0;
}

.pianoroll-head-main #pianorollTitle,
.pianoroll-head-main .pianoroll-scale,
.pianoroll-head-main .pianoroll-chord {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap; /* one row always - a long chord name truncates rather than wrapping */
}

/* Squeezed, the name gives up its fixed width before the buttons beside it give up anything. */
.pianoroll-head-main .def-name {
  flex: 0 1 7.5em;
  min-width: 0;
}

.pianoroll-head-tools {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto; /* the tools and the ✕ keep their size whatever else has to give */
  margin-left: auto;
}

/* One height for the tool row, contents centered, so a glyph-sized button (⌨) and a word-sized one
   (fold) stand level. */
.pianoroll-head-tools button {
  height: 22px;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 8px;
}

/* The roll and the controls that shape it sit side by side: the timing controls in a column down
   the left, the grid itself to their right. */
.pianoroll-body {
  display: flex;
  align-items: stretch;
}

.pianoroll-side {
  display: flex;
  flex-direction: column;
  gap: 7px;
  flex: 0 0 auto;
  width: 92px;
  padding: 12px 10px;
  background: color-mix(in srgb, var(--accent) 4%, var(--bg-panel));
  border-right: 1px solid var(--border);
}

/* Minimized to a rail holding just the restore button: the controls go, the panel keeps its size,
   and the grid widens into what the column gives up. */
.pianoroll-side.collapsed {
  width: 28px;
  padding: 12px 4px;
}

.pianoroll-side.collapsed .pr-side-ctl,
.pianoroll-side.collapsed .pr-side-row {
  display: none;
}

/* Sits in the column's bottom-right corner: `margin-top: auto` takes up whatever slack is left
   below the controls, so the toggle is out of their way rather than pushing GRID and the rest of
   them down a row. It keeps that spot in both states, so the button doesn't move out from under
   the pointer when it's clicked. Narrow padding so it still fits the rail once the column shuts. */
.pr-side-toggle {
  margin-top: auto;
  align-self: flex-end;
  padding: 2px 4px;
}

.pianoroll-side.collapsed .pr-side-toggle {
  align-self: stretch;
  padding: 2px 0;
}

/* Stacked in the side panel, a control's label sits above it and reads as a section heading. */
.pianoroll-side .pr-side-ctl {
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  /* The label is flush with its box's true left edge, but the box's rounded top-left corner reads
     as starting further in, so the label looks like it hangs off. 2px puts it back optically. */
  padding-left: 2px;
}

.pianoroll-side .pr-side-ctl input[type='number'],
.pianoroll-side .pr-side-ctl select,
.pianoroll-side .pr-side-ctl .pr-box-slider {
  width: 100%;
}

/* A control that reads as a value box and works as a slider: drag it sideways and a fill sweeps
   behind the number (see prMakeBoxSlider). A track and a handle would cost a whole row of a 92px
   column to say what the fill says inside the box the number was going to take anyway. */
.pr-box-slider {
  position: relative;
  overflow: hidden;
  box-sizing: border-box;
  padding: 3px 6px;
  font-size: 12px;
  font-family: var(--mono);
  text-align: center;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  cursor: ew-resize;
  user-select: none;
  outline: none;
}

.pr-box-slider:hover,
.pr-box-slider:focus-visible {
  border-color: var(--accent);
}

/* Painted behind the number; left/width come from the value, growing from the left edge or (on a
   bipolar control like swing) from wherever its zero sits. */
.pr-box-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  background: var(--accent-soft);
  pointer-events: none;
}

.pr-box-val {
  position: relative; /* over the fill, which is absolutely placed */
}

.pianoroll-side .pr-side-row {
  display: flex;
  gap: 6px;
}

/* Buttons split their row evenly, whether the row holds two of them (÷2 ×2) or one (⧉). */
.pianoroll-side .pr-side-row .small {
  flex: 1;
  min-width: 0;
}

/* Same mono, un-uppercased face the head's text buttons wear - without the head's min-width,
   which would fight the even split above. */
.pianoroll-side .pr-text-btn {
  font-family: var(--mono);
  text-transform: none;
  letter-spacing: 0;
}

.pianoroll-panel-head .pr-text-btn,
.groove-panel-head .pr-text-btn {
  min-width: 40px;
  font-family: var(--mono);
  text-transform: none;
  letter-spacing: 0;
}

/* The key setScale() set, next to the panel title - what the grid's tonic/in-key shading means.
   Empty (and so invisible) until a scale is in force. */
.pianoroll-scale {
  font-family: var(--mono);
  text-transform: none;
  letter-spacing: 0;
  font-weight: 600;
  color: var(--accent);
}

.pianoroll-scale:not(:empty) {
  padding: 1px 6px;
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 999px;
  background: var(--accent-soft);
  cursor: pointer; /* it's a button: clicking snaps every note into the key */
}

.pianoroll-scale:not(:empty):hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 22%, transparent);
}

.pianoroll-scale:not(:empty):active {
  transform: translateY(1px);
}

/* What the selected notes spell as a chord, next to the key - a readout, not a button. Empty
   (and so invisible) unless 2+ pitches are selected and they analyze to a name. */
.pianoroll-chord {
  font-family: var(--mono);
  text-transform: none;
  letter-spacing: 0;
  font-weight: 600;
  color: var(--text-dim);
}

.pianoroll-chord:not(:empty) {
  padding: 1px 6px;
  border: 1px solid color-mix(in srgb, var(--text-dim) 35%, transparent);
  border-radius: 999px;
}

.pianoroll-ctl {
  display: flex;
  align-items: center;
  gap: 5px;
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-dim);
}

.pianoroll-ctl input[type='number'] {
  width: 46px;
  padding: 3px 6px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  outline: none;
}

/* Every dropdown that sits in a panel's HEADER row - the arrangement's snap, the LFO's shape
   presets - is this one shape, so two panels open side by side don't show two different chevron
   insets. Anything new in a header joins this list rather than inventing its own metrics. */
.pianoroll-ctl select,
.lfo-panel-head select,
.groove-panel-head select {
  padding: 3px 22px 3px 8px;
  background-position: right 8px center;
}

.pianoroll-ctl select {
  font-size: 12px;
  font-family: var(--mono);
}

.pianoroll-ctl input[type='number']:focus {
  border-color: var(--accent);
}

/* A header control that doesn't apply to the axis on screen - `scale` on the index rows, where
   there is no key. Grayed in place rather than removed: a toolbar that reshuffles itself under the
   pointer is worse than a button that plainly isn't for here. */
.pianoroll-panel-head button:disabled {
  opacity: 0.35;
  cursor: default;
}

/* An engaged toggle in the roll's header (preview on, folded to the scale) lights up, so its
   state is readable without hovering for the tooltip - and likewise one in its side column, where
   the bend overlay is switched on. */
.pianoroll-panel-head button.active,
.pianoroll-side button.active,
.groove-panel-head button.active {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 700;
}

/* ⌨ is a glyph, not a word - at the tools' text size it is a smudge, so it gets glyph size in the
   same button height. (On, it takes the header's ordinary .active tint - a pulse was tried and
   found distracting.) */
#pianorollKeys {
  font-size: 17px;
  padding: 0 7px;
}

/* The list a <select> opens in place of the platform's popup (see openSelMenu in client.js): the
   context menu's skin, over everything, scrolling when a list is long. The select itself stays
   the trigger, lit while its list is open. */
.sel-menu {
  position: fixed;
  z-index: 300;
  max-height: 60vh;
  overflow-y: auto;
  padding: 4px;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 10px 28px var(--shadow);
  font-size: 12px;
}

.sel-menu.hidden {
  display: none;
}

/* A long list, in columns (openSelMenu sets how many). A group never splits across two. */
.sel-menu.sel-menu-cols {
  column-gap: 4px;
  max-height: none;
}

.sel-menu-group {
  break-inside: avoid;
}

.sel-menu-head {
  padding: 5px 9px 3px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

.sel-menu button {
  display: block;
  width: 100%;
  position: relative;
  text-align: left;
  background: none;
  border: 0;
  border-radius: 5px;
  padding: 4px 12px 4px 22px;
  font: inherit;
  color: var(--text);
  white-space: nowrap;
  transition: none;
}

.sel-menu button.hi {
  background: var(--hover-bg);
}

.sel-menu button.on {
  color: var(--accent);
}

.sel-menu button.on::before {
  content: '\2713';
  position: absolute;
  left: 8px;
}

.sel-menu button:disabled {
  opacity: 0.4;
  cursor: default;
}

select.sel-open {
  border-color: var(--accent);
}

/* The app's context menus - the piano roll's value lane (prOpenLaneMenu) and the editor's own
   (openEditorMenu). One widget, positioned and dismissed by openCtxMenu. */
.ctx-menu {
  position: fixed;
  z-index: 40;
  min-width: 13em;
  padding: 4px;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 10px 28px var(--shadow);
  font-size: 12px;
}

.ctx-menu.hidden {
  display: none;
}

.ctx-menu-head {
  padding: 4px 9px 5px;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

.ctx-menu button {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-radius: 5px;
  padding: 5px 9px;
  font: inherit;
  color: var(--text);
}

.ctx-menu button:hover {
  background: var(--hover-bg);
}

.ctx-menu .ctx-param {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 9px;
  color: var(--text-dim);
}

.ctx-menu .ctx-menu-head.ctx-drag {
  cursor: grab;
  user-select: none;
}

.ctx-menu .ctx-menu-head.ctx-drag.dragging {
  cursor: grabbing;
}

.ctx-menu .ctx-param[hidden] {
  display: none;
}

.ctx-menu .ctx-param > span {
  width: 6em;
}

.ctx-menu .ctx-param input[type=range] {
  width: 9em;
  --cap-w: 8px;
  --cap-h: 14px;
  --groove: 3px;
}

.ctx-menu .ctx-param input[type=number] {
  width: 4em;
  font: inherit;
  color: var(--text);
}

.ctx-menu .ctx-param select {
  font: inherit;
  color: var(--text);
}

.ctx-menu .ctx-param output {
  width: 2.5em;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.ctx-menu .ctx-choice {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
}

.ctx-menu .ctx-choice button {
  display: inline-block;
  width: auto;
  padding: 3px 8px;
  color: var(--text-dim);
}

.ctx-menu .ctx-choice-col {
  flex-direction: column;
}

.ctx-menu .ctx-choice-col button {
  width: 100%;
  text-align: left;
}

/* A choice with more than a few options folds into a dropdown: the picked word, and the list
   absolutely below it so opening it never reflows the popover. */
.ctx-menu .ctx-drop-holder {
  position: relative;
  flex: 1;
}

.ctx-menu .ctx-drop-trigger {
  display: block;
  width: 100%;
  text-align: left;
  padding: 3px 8px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--bg-raised);
}

.ctx-menu .ctx-drop-trigger::after {
  content: '\25BE';
  float: right;
  opacity: 0.6;
}

.ctx-menu .ctx-drop-trigger.open::after {
  content: '\25B4';
}

.ctx-menu .ctx-drop {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 1;
  padding: 4px;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 10px 28px var(--shadow);
}

.ctx-menu .ctx-drop.hidden {
  display: none;
}

.ctx-menu .ctx-choice button.on {
  color: var(--accent);
  background: var(--hover-bg);
}

.ctx-menu .ctx-actions {
  display: flex;
  justify-content: flex-end;
  gap: 4px;
  padding: 6px 4px 2px;
}

.ctx-menu .ctx-actions button {
  display: inline-block;
  width: auto;
}

.ctx-menu .ctx-actions button[hidden] {
  display: none;
}

.ctx-menu .ctx-actions button.primary {
  color: var(--accent);
}

.ctx-menu hr {
  height: 0;
  margin: 4px 6px;
  border: 0;
  border-top: 1px solid var(--border);
}

/* Takes whatever the control column isn't using - flex-basis 0 so the backing store's pixel size
   (dpr-scaled, and set from the width this lands on) never feeds back into the layout. prSizeCanvas
   watches it and re-sizes/redraws the roll to match. */
#pianorollCanvas {
  display: block;
  margin: 12px;
  flex: 1 1 0;
  min-width: 240px;
  height: 476px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  cursor: crosshair;
  outline: none;
  touch-action: none;
}

/* --------------------------------------------- arrangement painter */

/* Wider than the roll: the horizontal axis is whole bars and a song is many of them. */
/* The arrangement is a PAGE, not a panel: it sits inside main in the editor pane's place, and
   while it is up (body.arrange-on) the editor pane steps aside. It still wears the piano roll's
   panel chrome for its header, which is why the class is shared; everything that made that chrome
   a centered floating box is undone here. The view switch in the header is how you flip. */
.arrange-panel {
  position: static;
  z-index: auto; /* a flex item stacks by z-index even when static; the chrome's 30 would float the page over the dialogs */
  transform: none; /* the chrome centers a floating panel by translating it half its size; a page stays put */
  flex: 1;
  width: auto;
  max-width: none;
  max-height: none;
  min-width: 0;
  min-height: 0; /* a flex item in the pane's COLUMN: without this the canvas floors its height */
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* The page fills ONE DECK's pane and no more: the panel is MOVED into that pane when it opens (see
   openArrangeEditor), under the pane's own head, and what steps aside is the pane's body - the
   editor, or the waveform when the deck holds a file.
   Moving it rather than placing it with `order` is what keeps the pane's HEAD on screen: that head
   carries the deck's code | arrange switch, and hiding the whole pane would take away the control
   you press to get back. Outside DJ mode this reads exactly as it always has - one pane, the
   arrangement in it; in a mix the other deck's code stays up beside it, which is the point. */
body.arrange-on.arrange-deck-a #editorPane > .CodeMirror,
body.arrange-on.arrange-deck-a #editorPane > .song-pane,
body.arrange-on.arrange-deck-b #deckBPane > .CodeMirror,
body.arrange-on.arrange-deck-b #deckBPane > .song-pane {
  display: none;
}

.arrange-panel .arrange-body {
  flex: 1;
  min-height: 0;
}

.arrange-body {
  position: relative;
  padding: 12px;
}

/* The lane picker's anchor: no size of its own, moved over the gutter cell of whichever lane was
   asked (see arOpenLanePicker), with .def-picker hanging off it as it does off a panel head. The
   anchor itself must never take a click - it sits over the canvas. */
.arrange-pick-anchor {
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
}

.arrange-pick-anchor .def-picker {
  pointer-events: auto;
  /* Rises from the anchor instead of hanging off it: the strips are the bottom of the panel, and
     the panel clips what runs past its edge. */
  top: auto;
  bottom: calc(100% + 4px);
}

#arrangeCanvas {
  display: block;
  width: 100%;
  /* A first frame's worth: AR_LANES_TOP (ruler + loops strip) + rows + one automation strip.
     arSizeCanvas sets the real height inline over this one, and keeps the whole panel inside the
     window - rows and pinned lanes both scroll, so nothing here has to grow past the display. */
  height: 424px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
  cursor: crosshair;
  outline: none;
  touch-action: none;
}

/* The lane-rename box, laid over the lane's name in the gutter. */
.arrange-lane-name {
  position: absolute;
  box-sizing: border-box;
  height: 22px;
  padding: 0 6px;
  font: inherit;
  font-size: 11px;
  color: var(--text);
  background: var(--bg-panel);
  border: 1px solid var(--accent);
  border-radius: 4px;
  outline: none;
}

.arrange-lane-name.hidden {
  display: none;
}

/* --------------------------------------------- phrase position + midi record (header) */

/* Its options are added by script (presets, saved themes, an unsaved draft), so it starts out
   empty and narrow. A floor wide enough for the longest of them keeps the controls beside it from
   sliding along when they arrive. */
#themeSelect {
  min-width: 132px;
  padding: 0 28px 0 14px; /* a name reads left-aligned, like a menu; the chevron keeps its margin */
  justify-content: flex-start;
}

#themeEditBtn {
  padding: 0 10px;
}

/* Its circles are built by script, so the box holds their space from the first paint instead of
   growing into it: PHRASE_CYCLES (4) circles of 15px with 4px between them. */
/* On the desk each deck counts its own phrase (see deckPhraseLabel); this one reads deck A's
   code clock alone, which says nothing about a deck holding a file. Hidden, not removed, so
   the header keeps its layout. */
body.mix-on #phraseViz {
  visibility: hidden;
}

#phraseViz {
  display: flex;
  align-items: center;
  gap: 4px;
  width: 72px;
  margin-right: 4px;
}

.phrase-c {
  display: block;
  width: 15px;
  height: 15px;
}

.phrase-c .ring {
  fill: none;
  stroke: var(--border);
  stroke-width: 2.5;
}

.phrase-c .arc {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.5;
  stroke-dasharray: 0 1;
  transform: rotate(-90deg); /* fill starts at 12 o'clock, like a real clock */
  transform-origin: center;
}

#phraseViz.rec .arc {
  stroke: var(--err);
}

.rec-wrap {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0; /* ● rec and its chevron are one segmented control - see #recBtn / #recOptsBtn */
}

/* The DJ toggle: an ordinary header button (it used to be the last segment of a code | arrange |
   dj switch), lit while the mode is on the way the roll header's toggles are - background and
   accent, no bold, so the word doesn't change width when it lights. */
#viewDjBtn {
  height: 32px;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  padding: 0 14px;
  font-size: 13px;
  line-height: 1;
}

#viewDjBtn.active {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}

/* label swaps between "● rec" and countdowns - fixed width keeps the header from jittering. The
   options chevron is the button's other half: one segmented control, joined at the seam. */
#recBtn {
  min-width: 88px;
  border-radius: 8px 0 0 8px;
}

#recOptsBtn {
  padding: 0 9px;
  border-left-width: 0; /* shares ● rec's right border */
  border-radius: 0 8px 8px 0;
}

#recOptsBtn .chev {
  display: block;
  color: var(--text-dim);
}

#recOptsBtn:hover .chev {
  color: var(--text);
}

#recBtn.rec-armed {
  color: var(--warn);
  border-color: var(--warn);
}

#recBtn.rec-live {
  color: var(--err);
  border-color: var(--err);
  animation: rec-pulse 1s ease-in-out infinite;
}

@keyframes rec-pulse {
  50% {
    opacity: 0.55;
  }
}

.rec-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 30;
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  padding: 12px 14px;
  box-shadow: 0 12px 36px var(--shadow);
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
}

.rec-panel.hidden {
  display: none;
}

.rec-panel label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.rec-panel select {
  min-width: 84px;
  padding: 4px 26px 4px 10px;
  font-size: 12px;
  border-radius: 6px;
}

.rec-panel select {
  padding: 3px 22px 3px 8px;
  background-position: right 8px center;
  font-size: 12px;
}

/* --------------------------------------------------------------- hotkeys */

.hotkeys {
  width: min(440px, 92vw);
}

.hotkeys-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 2px 12px 10px;
}

.hotkeys-section {
  margin: 10px 0 4px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

.hotkeys-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
  font-size: 12px;
}

.hotkeys-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Fixed width, so learning (and a long two-chord default) never moves the row. */
.hotkeys-chord {
  flex: none;
  width: 150px;
  font-family: var(--mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hotkeys-chord.learning {
  border-color: var(--accent);
  color: var(--accent);
}

.hotkeys-reset {
  flex: none;
}

/* --------------------------------------------------------------- midi import */

.midi-import {
  width: min(420px, 92vw);
}

.midi-import-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
}

.midi-import-summary {
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-dim);
}

.midi-import-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
}

.midi-import-row > span:first-child {
  min-width: 64px;
  color: var(--text-dim);
}

.midi-import-row input[type='text'],
.midi-import-row select {
  flex: 1;
  font-size: 12px;
}

.midi-import-row.disabled {
  opacity: 0.45;
  pointer-events: none;
}

/* Shown only while a file is being dragged over the window (see the drop handlers in
   client.js) - a transient target marker, not a permanent hint. */
.file-drop-overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  box-shadow: inset 0 0 0 2px var(--accent);
}

.file-drop-overlay.hidden {
  display: none;
}

.file-drop-overlay span {
  padding: 10px 18px;
  border-radius: 10px;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  box-shadow: 0 12px 36px var(--shadow);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--text);
}

/* ------------------------------------------------------------------- mixer
   The ctrl+g modal (see client.js's mixer section). Its own backdrop class rather than
   dir-picker-backdrop on purpose: that class silences the transport hotkeys, which must keep
   working while mixing. */

.mixer-backdrop {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--shadow);
  z-index: 40;
}

.mixer-backdrop.hidden {
  display: none;
}

.mixer-dialog {
  display: flex;
  flex-direction: column;
  width: min(980px, 94vw);
  max-height: 92vh;
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  box-shadow: 0 12px 36px var(--shadow);
  overflow: hidden;
}

.mixer-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  border-bottom: 1px solid var(--border);
}

.mixer-view-btn {
  text-transform: none;
  letter-spacing: normal;
  min-width: 64px;
}
/* Pinned to "overall" while the engine analyzes only the master (too many tracks - the note
   says so). Greyed in place, as the pianoroll head does with its keyless buttons. */
.mixer-view-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

.mixer-plots {
  display: flex;
  gap: 10px;
  padding: 10px 12px 4px;
}

.mixer-plot {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.mixer-plot canvas {
  width: 100%;
  height: 190px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
}

.mixer-plot-label {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 2px 0;
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--text-dim);
}

.mixer-freeze-btn {
  font-size: 9px;
  padding: 0 8px;
}

.mixer-freeze-btn.mixer-freeze-on {
  color: var(--accent);
  border-color: var(--accent);
}

.mixer-strips {
  display: flex;
  gap: 6px;
  padding: 8px 12px 12px;
  overflow-x: auto;
}

.mixer-strip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  min-width: 76px;
  padding: 8px 6px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-raised);
}

/* A grouped track's strip sits IN its group: pulled up against the strip before it with the
   seam between them left open, so `kick` and its `#fill` read as one desk section, the way the
   base's mute and solo already treat them. */
.mixer-strip:has(+ .mixer-strip-variant) {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.mixer-strip-variant {
  margin-left: -6px; /* the strips' gap, closed */
  border-left-style: dashed;
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

.mixer-strip-variant .mixer-strip-name-text {
  color: var(--text-dim);
}

.mixer-strip-gone {
  opacity: 0.55;
}

/* Muted (or soloed-away) but still listed - the strip a mute button needs to stay un-mutable
   from. Marked on the frame rather than by fading the name: a dimmed label is hard to read and
   reads as "gone", which is the opposite of what a strip you can still un-mute should say. */
.mixer-strip-silent {
  border-style: dashed;
  background: color-mix(in srgb, var(--bg-raised) 60%, transparent);
}

.mixer-strip-silent .mixer-strip-swatch {
  opacity: 0.4;
}

/* An unfolded group: its members are on the desk beside it and carry the meters and the curve
   (see mixerAnalyzedLabels), so its own meter and swatch step back. The fader, mute and solo are
   still the group's and stay at full strength - it is the READOUT that has moved, not the
   controls. Same treatment the silent strip gives its swatch, for the same reason: the color is
   the plot's legend, and this strip is not in the plot. */
.mixer-strip-aside .mixer-meter,
.mixer-strip-aside .mixer-strip-swatch {
  opacity: 0.3;
}

/* While a fader or knob is held, the other tracks fade back in the plots - the strips say the
   same thing, so the two never disagree about what you are looking at. */
.mixer-strip-unfocused {
  opacity: 0.45;
}

.mixer-strip-focus {
  border-color: var(--text-dim);
}

.mixer-strip,
.mixer-strip-unfocused {
  transition: opacity 0.12s ease;
}

.mixer-bass-btn {
  font-size: 9px;
  padding: 0 6px;
  width: 100%;
  font-variant-numeric: tabular-nums;
  touch-action: none;
  cursor: ew-resize;
}

.mixer-bass-btn.on-bass {
  color: var(--ok);
  border-color: var(--ok);
}

.mixer-strip-ms {
  display: flex;
  gap: 4px;
}

.mixer-ms-btn {
  font-size: 9px;
  padding: 0 7px;
}

.mixer-ms-btn.on-mute {
  color: var(--warn);
  border-color: var(--warn);
}

.mixer-ms-btn.on-solo {
  color: var(--accent);
  border-color: var(--accent);
}

/* The name row doubles as the plots' legend - swatch in the track's curve color, then the
   label. The swatch carries the color so the text stays readable on light themes. */
.mixer-strip-name {
  display: flex;
  align-items: center;
  gap: 5px;
  max-width: 72px;
  font-size: 11px;
  font-weight: 700;
}

.mixer-strip-swatch {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 2px;
}

.mixer-strip-name-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The name is the block's label, and clicking it types over it (see startMixerRename). */
.mixer-strip-name-text.mixer-renamable {
  cursor: text;
}

.mixer-strip-rename {
  flex: 1;
  min-width: 0;
  padding: 0 3px;
  font-size: 11px;
  font-weight: 700;
  font-family: var(--mono);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 3px;
  outline: none;
}

.mixer-strip-body {
  display: flex;
  align-items: stretch;
  gap: 4px;
  height: 140px;
}

.mixer-meter {
  width: 26px;
  height: 140px;
}

/* A vertical range: modern writing-mode styling, bottom-up. */
.mixer-fader {
  writing-mode: vertical-lr;
  direction: rtl;
  width: 22px;
  height: 140px;
  margin: 0;
  accent-color: var(--accent);
}

.mixer-fader.mixer-patterned,
.mixer-knob.mixer-patterned {
  /* This control's channel is patterned in the code - the write composes rather than replaces
     (gain) or takes over (pan); the tooltip carries the words. */
  outline: 1px dashed var(--warn);
  outline-offset: 1px;
  border-radius: 3px;
}

.mixer-db {
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

/* width and pan, side by side in signal order, with their readouts lined up underneath. */
.mixer-knob-row,
.mixer-knob-labels {
  display: flex;
  gap: 4px;
  justify-content: center;
}

.mixer-knob-label {
  width: 30px;
  font-size: 9px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mixer-knob {
  width: 30px;
  height: 30px;
  cursor: default;
  touch-action: none;
}

.mixer-note {
  min-height: 16px;
  padding: 0 12px 10px;
  font-size: 11px;
  color: var(--text-dim);
}

.mixer-note:empty {
  padding: 0;
  min-height: 0;
}

/* ---------------------------------------------------------------------------------------------
   ★ - the library. A definition's row in any picker carries a star: hollow for one that lives only
   in this buffer, filled for one that is pinned into every project (see makeDefRegistry's pin).
   Out of the way like the row's other second gestures, except when it is filled - that is a
   state worth reading off the list, not only acting on. --------------------------------------- */
.def-pick-row .def-pick-star {
  display: inline-flex;
  align-self: center;
  padding: 0 2px;
  line-height: 1;
  color: var(--text-dim);
  opacity: 0;
  cursor: pointer;
}

.def-pick-row .def-pick-star svg {
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linejoin: round;
}

.def-pick-row .def-pick-star.same svg,
.def-pick-row .def-pick-star.differs svg,
.def-pick-row .def-pick-star.library svg {
  fill: currentColor;
}

/* The open row's name in an inline list renames on click (see renameInline). */
.def-pick-row .def-pick-renamable {
  cursor: text;
}

.def-pick-row .def-pick-rename {
  flex: 1;
  min-width: 0;
  padding: 0 4px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 4px;
  outline: none;
}

.def-pick-row .def-pick-star.same,
.def-pick-row .def-pick-star.differs,
.def-pick-row .def-pick-star.library {
  opacity: 0.85;
  color: var(--accent);
}

/* Pinned, but this buffer's copy has moved on: the star is a little less sure of itself. */
.def-pick-row .def-pick-star.differs {
  opacity: 0.5;
}

.def-pick-row.on .def-pick-star,
.def-pick-row:hover .def-pick-star {
  opacity: 0.85;
}

.def-pick-row .def-pick-star:hover {
  opacity: 1;
  color: var(--accent);
}

/* ---------------------------------------------------------------------------------------------
   Pack panel (see the pack panel section in client.js). Three columns: packs | in the pack | pick
   from. Wider than the other dialogs because it holds three lists, and the file names are the
   content. ------------------------------------------------------------------------------------ */
.pack-dialog {
  width: min(1040px, 94vw);
  max-height: 80vh;
  height: min(640px, 80vh);
}

.pack-dialog .hidden {
  display: none;
}

.pack-dialog .dir-picker-head {
  text-transform: none;
  letter-spacing: 0;
}

.pack-dialog .dir-picker-head > span:first-child {
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.pack-dialog .def-pick-wrap {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.pack-cols {
  flex: 1;
  display: grid;
  grid-template-columns: 15em minmax(0, 1fr) minmax(0, 1.3fr);
  min-height: 0;
}

.pack-col {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  border-right: 1px solid var(--border);
}

.pack-col:last-child {
  border-right: 0;
}

.pack-col-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px 4px;
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

.pack-col-sub {
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--mono);
}

.pack-search {
  margin: 4px 10px 0;
  padding: 5px 8px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  outline: none;
}

.pack-search:focus {
  border-color: var(--accent);
}

.pack-list {
  flex: 1;
  max-height: none;
  margin: 4px 4px 6px;
  padding: 0 6px;
  overflow-y: auto;
}

.pack-list .def-pick-row.current .def-pick-name::after {
  content: ' ●';
  color: var(--accent);
}

.pack-entries {
  flex: 1;
  overflow-y: auto;
  padding: 2px 8px 8px;
}

/* One file in the pack: its index first, since the index is how a pattern says it. */
.pack-entry {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  border-radius: 6px;
  font-size: 12px;
  font-family: var(--mono);
  color: var(--text);
}

.pack-entry:hover {
  background: var(--hover-bg);
}

.pack-entry-index {
  min-width: 2ch;
  text-align: right;
  color: var(--accent);
}

.pack-entry-name,
.pack-file-name,
.pack-dir-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pack-entry-btn {
  padding: 0 3px;
  font-size: 12px;
  line-height: 1;
  color: var(--text-dim);
  opacity: 0;
  cursor: pointer;
  user-select: none;
}

.pack-entry:hover .pack-entry-btn,
.pack-file-row:hover .pack-entry-btn,
.pack-dir-row:hover .pack-entry-btn {
  opacity: 0.7;
}

.pack-entry-btn:hover {
  opacity: 1;
  color: var(--accent);
}

.pack-entry-btn.off {
  visibility: hidden;
}

.pack-entry-del:hover {
  color: var(--err);
}

/* Dragging a row to reorder: a line where it would land. */
.pack-entry.drop-before {
  box-shadow: inset 0 2px 0 var(--accent);
}

.pack-entry.drop-after {
  box-shadow: inset 0 -2px 0 var(--accent);
}

.pack-entries.dragging {
  cursor: grabbing;
}

/* Files from the desktop held over the list: the list is where they land. */
.pack-entries.drop-target {
  box-shadow: inset 0 0 0 2px var(--accent);
  border-radius: 6px;
}

.pack-entries.dragging .pack-entry:hover {
  background: none;
}

/* A focused list is the one the arrow keys drive; the ring says which, quietly. */
.pack-entries:focus-visible,
.pack-browse-list:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: -1px;
  border-radius: 6px;
}

/* Selected rows in either list - what ← / → act on. */
.pack-entry.selected,
.pack-file-row.selected,
.pack-dir-row.selected {
  background: var(--accent-soft);
  color: var(--accent);
}

.pack-entry.selected .pack-entry-index {
  color: inherit;
}

/* The row that is in the transport right now. */
.pack-entry.playing .pack-entry-name::after,
.pack-file-row.playing .pack-file-name::after {
  content: ' ▶';
  font-size: 9px;
  color: var(--accent);
}

.pack-foot {
  gap: 8px;
}

.pack-play-name {
  max-width: 16em;
  font-size: 11px;
  font-family: var(--mono);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pack-play-bar {
  position: relative;
  flex: 1;
  height: 8px;
  min-width: 6em;
  border-radius: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
  cursor: pointer;
  overflow: hidden;
  touch-action: none;
}

.pack-play-head {
  height: 100%;
  width: 0;
  background: var(--accent);
  opacity: 0.8;
}

.pack-play-time {
  min-width: 8em;
  font-size: 10.5px;
  font-family: var(--mono);
  color: var(--text-dim);
  text-align: right;
}

.pack-browse-path {
  margin: 4px 10px 4px;
}

.pack-browse-search {
  margin: 0 10px 4px;
}

/* A search hit reads as <folders it is in>/<file>: the path is why it matched, the name is what
   you are picking, so the path leads and fades and the name carries the weight - and when the row
   is too narrow for both, it is the path that gets cut short, never the filename. */
.pack-file-name.has-path {
  display: flex;
}

.pack-file-dir,
.pack-file-base {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pack-file-dir {
  flex: 0 5 auto;
  color: var(--text-dim);
  opacity: 0.75;
}

.pack-file-base {
  flex: 0 1 auto;
}

.pack-browse-list {
  min-height: 0;
  padding: 2px 6px 8px;
}

.pack-dir-row {
  display: flex;
  align-items: center;
  gap: 6px;
}

.pack-file-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  border-radius: 6px;
  font-size: 12.5px;
  cursor: pointer;
  user-select: none;
}

.pack-dir-row {
  user-select: none;
}

.pack-file-row::before {
  content: '♪ ';
  color: var(--text-dim);
}

.pack-file-row:hover {
  background: var(--hover-bg);
}

/* In the pack already - the browser says so, and clicking takes it back out. */
.pack-file-row.on {
  color: var(--accent);
}

.pack-file-row.on::before {
  content: '✓ ';
  color: var(--accent);
}

.pack-note {
  flex: 0 1 auto;
  font-size: 10.5px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pack-note.error {
  color: var(--err);
}

/* --- the map view of the pick-from column --------------------------------------------------- */

/* folders | map - which way the column is picking. */
.pack-pick-mode {
  display: inline-flex;
  gap: 2px;
  margin-left: 4px;
}

/* A toggle that is on: the pick mode, the 🎧 audition switch. */
.pack-dialog .small.on {
  color: var(--accent);
  border-color: var(--accent);
}

.pack-map {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: 0 10px 8px;
}

/* Sound families, each borrowed from a palette color every theme already defines against its
   own background - so the map re-colors with the theme. The type a point IS (kick, clap, ride…)
   rides on hover and on the legend; the hue only says which family. */
:root {
  --map-kick: var(--err);
  --map-snare: var(--syn-number);
  --map-hat: var(--syn-string);
  --map-cymbal: var(--syn-property);
  --map-perc: var(--syn-keyword);
  --map-tonal: var(--accent);
  --map-voice: var(--solo);
  --map-loop: var(--text);
  --map-other: var(--text-dim);
}

.pack-map-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 6px;
  padding: 4px 0 6px;
}

/* One family: click isolates it (the rest fade), click again shows all. */
.map-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 1px 7px;
  font-size: 10.5px;
  font-family: var(--mono);
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  user-select: none;
}

.map-chip:hover {
  color: var(--text);
}

.map-chip.on {
  color: var(--text);
  background: var(--accent-soft);
  border-color: var(--accent);
}

.map-chip .sw {
  width: 7px;
  height: 7px;
  border-radius: 50%;
}

.map-chip .n {
  opacity: 0.7;
}

.pack-map-canvas {
  display: block;
  flex: 1;
  min-height: 0;
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  outline: none;
  cursor: crosshair;
}

.pack-map-canvas:focus-visible {
  border-color: var(--accent);
}

/* No sources yet, or none indexed: what to do about it, in the middle of where the map would be. */
.pack-map-empty {
  position: absolute;
  inset: 40px 10px 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text-dim);
  text-align: center;
  pointer-events: none;
}

.pack-map-empty .small {
  pointer-events: auto;
}

/* A build in progress: phase and a bar along the map's foot. The bar is the transport's. */
.pack-map-build {
  position: absolute;
  left: 20px;
  right: 20px;
  bottom: 18px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 10.5px;
  font-family: var(--mono);
  color: var(--text-dim);
}

.pack-map-build .pack-play-bar {
  flex: 1;
}

/* --- the map's folders, in the settings tab --- */
.map-source-list {
  max-height: 160px;
}

.map-source-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 0;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text);
}

/* The path shows its tail (the folder's own name) when it doesn't fit, not its head. */
.map-source-row .path {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  direction: rtl;
  text-align: left;
}

.map-source-note {
  margin-top: 6px;
  white-space: normal;
}

.map-source-note.error {
  color: var(--err);
}


/* --- mix mode: deck B pane + the mix strip (see client.js's mix section) --- */

#deckBPane {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  position: relative; /* the active-deck ring overlays it (#editorPane is already relative) */
  border-left: 1px solid var(--border);
}
#deckBPane.hidden,
#mixStrip.hidden {
  display: none;
}
/* Stacked decks (the ⇅ button on the strip): the two detail waveforms leave their panes for
   #songStack, a full-width region above both, one directly over the other at one scale - with
   the playhead pinned at center in both, the two songs' beats line up (or don't) on a single
   vertical line, which is the whole reason to look at two waveforms at once. The panes keep
   their side-by-side rows underneath (title, overview, controls), squashed to what's left. */
#songStack.hidden {
  display: none;
}
body.mix-on.mix-stacked main {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* waveforms / their drag handle / the decks - both sized rows are properties, so which one is
     springy can move as regions fold (see djApplyRegionSizes). */
  grid-template-rows: var(--dj-row-stack) auto var(--dj-row-decks);
}
/* A folded region is gone and its rail stands in its place - which is the whole difference
   between folding and hiding: the row is still there, saying what is down there. */
body.dj-stack-min #songStack,
body.dj-strip-min #mixStrip {
  display: none;
}
body.mix-on.dj-decks-min #editorPane,
body.mix-on.dj-decks-min #deckBPane {
  display: none;
}
.dj-rail {
  display: none;
  height: 18px; /* DJ_RAIL_PX - the drag arithmetic counts on this */
  flex: 1;
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: var(--bg-panel);
  color: var(--text-dim);
  font-size: 10px;
  line-height: 1;
  letter-spacing: 0.06em;
  cursor: pointer;
  user-select: none;
}
.dj-rail:hover {
  color: var(--accent);
}
body.mix-on.dj-stack-min.mix-stacked #djStackRail,
body.mix-on.dj-decks-min #djDecksRail,
body.mix-on.dj-strip-min #djStripRail {
  display: flex;
}
/* In the stacked grid the rails stand in their region's row, across both deck columns. */
body.mix-on.mix-stacked #djStackRail {
  grid-row: 1;
  grid-column: 1 / -1;
}
body.mix-on.mix-stacked #djDecksRail {
  grid-row: 3;
  grid-column: 1 / -1;
}
body.mix-on.mix-stacked #songStack {
  grid-row: 1;
  grid-column: 1 / -1;
  /* Two rows, one per DECK, each half the region whether or not that deck has anything to draw.
     As a flex column the single lane of a one-deck session stretched over the whole region, which
     reads as "the playing deck is the top deck" and moves the other deck's lane the moment it
     loads - so a stacked pair never sat still. A deck's lane is now always in that deck's half,
     and an idle deck leaves its half empty. */
  display: grid;
  /* minmax(0, ...), not a bare 1fr: a grid track's automatic minimum is its content's, and a
     canvas has an intrinsic height - so bare 1fr rows refuse to shrink past it, the region
     overflows and the lower lane is pushed out of view. That is the same failure the flex
     version had, by a different route. */
  grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
  gap: 4px;
  padding: 8px 8px 0;
  min-height: 0;
  /* Clipped, so a region dragged below what two canvases can draw shows less waveform rather
     than spilling the lower one over the seam. */
  overflow: hidden;
  /* no border of its own: the drag handle right below is the seam */
}
/* No floor, so the two waveforms give up height together all the way down. A min-height here
   (60px until 2026-08-26) stopped both of them shrinking at the same moment, and since the
   region goes on getting shorter the lower one was simply pushed out of it - which read as
   "the bottom one squashes while the top keeps its size". */
#songStack .song-detail {
  min-height: 0;
  /* Explicit, not left to the grid item's default stretch: a canvas carries an aspect ratio from
     its width/height attributes, and with a definite width that ratio decides its height - so a
     full-width lane came out 490px tall inside a 150px row and drew most of the waveform under
     the clip. The flex version set the height outright (flex-basis), which is what hid this. */
  height: 100%;
}
/* Which half a lane lives in. Both of a deck's lanes claim the same row - only one of the pair
   is ever visible (a deck holds a file or it holds code), so they never collide. */
#songStack .stack-deck-a {
  grid-row: 1;
}
#songStack .stack-deck-b {
  grid-row: 2;
}
#songStack .song-detail.hidden {
  display: none; /* this deck has nothing to draw - its half of the stack stays empty */
}
#songStack .song-detail.active {
  border-color: var(--accent);
}
/* The livecoded decks' strip: sized by the .song-detail rules above, but there is nothing here to
   scrub - a pattern is code, not a recording - so it gives back that class's grab cursor. The
   wheel still zooms (both strips share one scale). */
.dj-live-wave {
  cursor: default;
}
body.mix-on.mix-stacked #editorPane {
  grid-row: 3;
  grid-column: 1;
}
body.mix-on.mix-stacked #deckBPane {
  grid-row: 3;
  grid-column: 2;
}
/* One deck full-screen (djSetZoom): the other pane steps out and this one takes both columns. */
body.mix-on.dj-zoom-a #deckBPane,
body.mix-on.dj-zoom-b #editorPane {
  display: none;
}
body.mix-on.mix-stacked.dj-zoom-a #editorPane,
body.mix-on.mix-stacked.dj-zoom-b #deckBPane {
  grid-column: 1 / -1;
}
body.mix-on.dj-zoom-b #deckBPane {
  border-left: 0; /* nothing to its left to divide it from */
}
/* ...and everything else of DJ mode with it, so a deck alone looks like a session outside it:
   no mixer, no stacked waveforms, no seams or rails for regions that are not on the page. */
body.mix-on.dj-zoomed #mixStrip,
body.mix-on.dj-zoomed #songStack,
body.mix-on.dj-zoomed .dj-rail,
body.mix-on.dj-zoomed .dj-resize {
  display: none;
}

/* ---------------------------------------------------- DJ mode: resizable regions
   Three stacked regions in DJ mode - waveforms, decks, mixer - with a drag handle on each seam.
   Neither deck is ever sized on its own: the panes are grid COLUMNS of one row, so a row height
   is always both decks at once. A region dragged past its minimum folds to a labeled rail
   (.dj-rail) that a click reopens; the handle stays a handle either way, and pushing it further
   folds whatever is next in its path. Which region is springy moves as they fold, which is why
   both sized rows here are properties written by djApplyRegionSizes. */
:root {
  --dj-row-stack: minmax(160px, 45%); /* the stacked waveforms; a px value once dragged */
  --dj-row-decks: minmax(0, 1fr); /* the decks: the springy region until they fold */
  --dj-row-main: minmax(0, 1fr); /* main (waveforms + decks) in the page grid */
  --dj-row-strip: auto; /* the mix strip's page-grid row - 1fr once it is the springy one */
  --dj-strip-h: auto; /* the strip's own height - its natural one until dragged */
}
.dj-resize {
  display: none;
  position: relative;
  height: 7px;
  flex: none;
  cursor: row-resize;
  background: var(--bg-panel);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  touch-action: none;
}
body.mix-on #djStripResize,
body.mix-on.mix-stacked #djStackResize {
  display: block;
}
/* The armed deck - the one Cmd+Enter plays and Cmd+. stops (see djSetActiveDeck). A ring rather
   than a fill: it has to read at a glance across the room without competing with the waveform.
   Drawn as an OVERLAY on top of the pane's own children rather than as an inset shadow on the
   pane: the editor and the waveform pane paint their own backgrounds over the whole pane, which
   swallowed three sides of the ring and all of it on a deck holding code. The pane, not the
   editor inside it, so the ring takes in the deck's head - its title and its buttons. */
body.mix-on #editorPane.deck-active::after,
body.mix-on #deckBPane.deck-active::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent);
  border-radius: 3px;
  pointer-events: none; /* it sits over the editor - it must not eat a single click */
  z-index: 6; /* over CodeMirror's own layers and the song pane's canvases */
}
/* the stack seam spans both deck columns - it divides the whole A+B region, not deck A's */
body.mix-on.mix-stacked #djStackResize {
  grid-row: 2;
  grid-column: 1 / -1;
}
/* the grip: a short centered rule, brightening on hover and while dragging */
.dj-resize::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 46px;
  height: 3px;
  border-radius: 2px;
  transform: translate(-50%, -50%);
  background: var(--border);
}
.dj-resize:hover::before,
.dj-resize.dragging::before {
  background: var(--accent);
}
.dj-resize.dragging {
  background: color-mix(in srgb, var(--accent) 12%, var(--bg-panel));
}

/* Nothing here turns a seam into a chip: a handle stays a handle however little is left on
   either side of it, and the folded region's own rail is what says so (see .dj-rail). */
/* The strip's dragged height clips its contents rather than reflowing them - the knob rows
   keep their positions as the desk gets shorter. Minimized, it is gone entirely. */
body.mix-on #mixStrip {
  height: var(--dj-strip-h);
  min-height: 0;
  overflow: hidden;
}
body.dj-strip-min #mixStrip {
  display: none;
}
body.mix-on.mix-stacked .song-pane {
  flex: none; /* nothing left to grow into - the stack has the waveform */
}
body.mix-on.mix-stacked .song-overview {
  height: 48px;
}
#mixStack.on {
  color: var(--accent);
  border-color: var(--accent);
}
#deckBHead,
#deckAHead {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-panel);
}
/* Deck A's head is always there now - it carries the pane's code | arrange switch, which is the
   only view control outside DJ mode. What grows in DJ mode is the DECK chrome beside it: the title,
   the song picker, play and update. */
#deckAHead .deck-only {
  display: none;
}
/* Centered like deck B's buttons: inline-flex ignores the buttons' text-align, so without the
   justify rule deck A's ▶ play sat left in its fixed width while deck B's sat centered. */
body.mix-on #deckAHead .deck-only {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* ...except the song title, which reads left-aligned like a menu and ends in "…" when it runs
   long (see .deck-song) - a flex box would clip its text instead of ellipsizing it. */
body.mix-on #deckAHead .deck-song {
  display: block;
}
/* Every control in a deck head keeps its own width; the song title is the one thing that gives
   way, and the full-screen button sits at the far end whatever is left between. */
#deckBHead > *,
#deckAHead > * {
  flex: none;
  white-space: nowrap;
}
#deckBHead > .deck-song,
#deckAHead > .deck-song {
  flex: 0 1 auto;
  min-width: 48px;
  max-width: 24em;
}
#deckBHead > .deck-zoom,
#deckAHead > .deck-zoom {
  margin-left: auto;
}
/* A deck too narrow for the title AND the controls - each deck is half a laptop screen - gives the
   title a line of its own, sharing it only with the full-screen button, and the controls the line
   under it. Asked of the deck's own width, not the window's, so a full-screened deck goes back to
   one row. */
body.mix-on #editorPane,
body.mix-on #deckBPane {
  container: deck / inline-size;
}
@container deck (max-width: 700px) {
  #deckBHead,
  #deckAHead {
    flex-wrap: wrap;
    row-gap: 4px;
  }
  #deckBHead > .deck-song,
  #deckAHead > .deck-song {
    order: -2;
    flex-basis: calc(100% - 40px);
    max-width: none;
  }
  #deckBHead > .deck-zoom,
  #deckAHead > .deck-zoom {
    order: -1;
  }
  #deckBHead > .deck-head-gap,
  #deckAHead > .deck-head-gap {
    display: none;
  }
  /* The song pane under it, by the same rule. The title row's title steps aside - the head
     above already says it on a line of its own - and leaves its slack to the readouts; a deck
     too narrow even for those gives the time a line under them rather than cutting it. (These
     rules are written one class longer than the base ones further down, which they must beat.) */
  .song-pane .song-title-row {
    flex-wrap: wrap;
    justify-content: flex-end;
    row-gap: 2px;
  }
  .song-title-row > .song-title {
    visibility: hidden;
  }
  /* The transport row says "bpm" only where there is room for it (the box keeps the tooltip),
     and its two halves - the platter, the tempo - take a line each when they cannot share one. */
  .song-ctl-row .song-bpm-label {
    font-size: 0;
  }
  .song-pane .song-ctl-row {
    flex-wrap: wrap;
    row-gap: 4px;
  }
}
/* The set's queue bookkeeping - never shown (the song button below is the picker). */
#deckAFile,
#deckBFile {
  display: none;
}
/* The deck heads' play/stop: the main button's colors (green at rest, red while playing) at
   .small size, on a fixed width so the flip doesn't reflow the head around it. */
#deckAPlay,
#deckBPlay {
  min-width: 62px;
  text-align: center;
  background: color-mix(in srgb, var(--ok) 16%, transparent);
  border-color: var(--ok);
  color: var(--ok);
}
#deckAPlay:hover,
#deckBPlay:hover {
  background: color-mix(in srgb, var(--ok) 28%, transparent);
}
/* The song on the deck, as a button: pressing it opens organize, whose rows carry →A / →B.
   Reads left-aligned like a menu, ellipsizes a long title, dims while nothing is picked. */
.deck-song {
  flex: 1;
  min-width: 0;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.deck-song.empty {
  color: var(--text-dim);
}
/* A control that has nothing to act on yet (the tempo slider before both decks have a tempo):
   dimmed, not clickable, no hover. */
#deckBHead button:disabled,
#deckAHead button:disabled,
.mix-center button:disabled {
  opacity: 0.4;
  cursor: default;
}
#deckBHead button:disabled:hover,
#deckAHead button:disabled:hover,
.mix-center button:disabled:hover {
  background: var(--bg-panel);
  border-color: var(--border);
}
.deck-loop.on,
.deck-zoom.on {
  color: var(--accent);
  border-color: var(--accent);
}
/* The loop length, inside the loop button (scrolling over it halves or doubles). Fixed width and
   tabular, because a length changing under the hand must not resize the button under the pointer. */
.deck-loop-len {
  min-width: 52px;
  text-align: center;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 10px;
  color: var(--text-dim);
}
.deck-loop-len-btn {
  min-width: 28px;
  padding: 2px 4px;
}
/* A little air between the arrangement's regions switch and the loop controls: they both say
   "loop" and they are not the same thing. */
.deck-head-gap {
  width: 6px;
}

#mixStrip {
  display: flex;
  gap: 12px;
  align-items: stretch;
  /* nothing on the desk is text to copy - the labels between the knobs would otherwise flip
     the cursor to an I-beam every few pixels */
  user-select: none;
  -webkit-user-select: none;
  /* the seam above is the drag handle's border, not the strip's */
  background: var(--bg-panel);
  padding: 6px 10px;
  /* the symmetric region ends at deck B's right edge: the strip sits in grid column 1 (the
     sidebar has its own column - see .layout), so its center IS the panes' true midline */
}
.mix-deck-head {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text-dim);
  font-size: 11px;
}
.mix-tracks {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Takes whatever height the strip has (basis 0: a long list must not make the strip taller
     on its own - the knob columns set the natural height, a drag sets any other) and scrolls
     inside it. The scroll gutter is padded out of the way of the controls: on deck A the
     scrollbar sits at the right, over the fader's end; deck B is mirrored (its rows pack to
     the right), so its list runs right-to-left and the scrollbar sits at the LEFT instead. */
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
  padding-right: 10px;
}
#mixTracksB {
  direction: rtl;
  padding-right: 0;
  padding-left: 10px;
}
#mixTracksB .mix-track {
  direction: ltr;
}
/* More stems than fit: a fade over the hidden edge with a chevron, set by client.js from the
   list's scroll position (mixTracksHint) - a bare cut-off row read as "that's all of them".
   The wrap exists for these: the list itself scrolls, so anything absolute inside it would
   scroll away with the rows. */
.mix-tracks-wrap {
  position: relative;
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.mix-tracks-wrap::before,
.mix-tracks-wrap::after {
  content: '\25BE';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 18px;
  line-height: 22px;
  text-align: center;
  font-size: 10px;
  color: var(--text-dim);
  background: linear-gradient(transparent, var(--bg-panel) 70%);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s;
}
.mix-tracks-wrap::before {
  content: '\25B4';
  top: 0;
  bottom: auto;
  line-height: 14px;
  background: linear-gradient(var(--bg-panel) 30%, transparent);
}
.mix-tracks-wrap.more-below::after,
.mix-tracks-wrap.more-above::before {
  opacity: 1;
}
.mix-track {
  display: flex;
  align-items: center;
  gap: 5px;
}
.mix-name {
  width: 70px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 11px;
}
/* A stem inside an UNFOLDED group: stepped in from its group's row (client.js sets the padding,
   on the side its deck reads from - deck B's rows pack to the right) and named a shade quieter,
   which is what says the group's own row above it is the one that mixes them all. */
.mix-track-member .mix-name {
  color: var(--text-dim);
}
/* The fold caret on a group's row - the ctrl+G mixer's gesture, in the desk's own size. Held open
   invisibly on every other row so the gates, names and faders stay in one column down the deck. */
.mix-fold {
  /* The gate's footprint next to it - it is the same kind of control, on the same row, and a
     caret drawn smaller than everything around it is one you have to hunt for. */
  width: 14px;
  min-width: 14px;
  height: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
.mix-fold:hover {
  color: var(--text);
}
.mix-fold.empty {
  visibility: hidden;
  pointer-events: none;
}
.mix-gate {
  width: 14px;
  height: 14px;
  min-width: 14px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: transparent;
  padding: 0;
  cursor: pointer;
}
.mix-gate.on {
  background: var(--ok);
  border-color: var(--ok);
}
.mix-gate.solo {
  background: var(--solo);
  border-color: var(--solo);
}
.mix-center {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 0 6px;
}
/* --- the desk's horizontal faders: crossfader, tempo migration and the per-stem mini-faders.
   The same look as the channel faders (mix-dfader): a recessed groove with a capped grip whose
   index line is the accent. The stock range control was the one piece of chrome on the desk
   that wasn't drawn by the theme. Each has a size (cap width x height, groove height); the
   crossfader's groove also carries its center detent. --- */
.dj-fader {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
  background: transparent;
  cursor: default;
  --cap-w: 10px;
  --cap-h: 18px;
  --groove: 4px;
  height: var(--cap-h);
}
.dj-fader::-webkit-slider-runnable-track {
  height: var(--groove);
  border-radius: calc(var(--groove) / 2);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  box-sizing: border-box;
}
.dj-fader::-moz-range-track {
  height: var(--groove);
  border-radius: calc(var(--groove) / 2);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  box-sizing: border-box;
}
.dj-fader::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: var(--cap-w);
  height: var(--cap-h);
  margin-top: calc((var(--groove) - var(--cap-h)) / 2 - 1px);
  border-radius: 3px;
  border: 1px solid var(--border);
  /* the cap's index line: a 2px accent stripe down the middle of the cap */
  background: linear-gradient(var(--accent), var(--accent)) center / 2px 60% no-repeat, var(--bg-panel);
  box-shadow: 0 1px 2px var(--shadow);
  transition: border-color 0.12s;
}
.dj-fader::-moz-range-thumb {
  width: var(--cap-w);
  height: var(--cap-h);
  border-radius: 3px;
  border: 1px solid var(--border);
  background: linear-gradient(var(--accent), var(--accent)) center / 2px 60% no-repeat, var(--bg-panel);
  box-shadow: 0 1px 2px var(--shadow);
}
.dj-fader:hover::-webkit-slider-thumb,
.dj-fader:active::-webkit-slider-thumb,
.dj-fader:focus-visible::-webkit-slider-thumb {
  border-color: var(--accent);
}
.dj-fader:hover::-moz-range-thumb,
.dj-fader:active::-moz-range-thumb,
.dj-fader:focus-visible::-moz-range-thumb {
  border-color: var(--accent);
}
.dj-fader:focus {
  outline: none;
}
.dj-fader:disabled {
  opacity: 0.4;
  cursor: default;
}
/* The crossfader in its scale: a row of ticks above and below the slot, like the printed
   scale beside a desk's crossfader - taller at the ends and the center. The ticks span the
   CAP CENTER's travel (width minus one cap), so the grip's index line lands exactly on a tick
   at each end and at the detent, and the position reads off the scale even though the cap
   hides the groove beneath it. */
.xf-scale {
  --xf-w: 180px;
  --xf-cap: 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}
.xf-scale::before,
.xf-scale::after {
  content: '';
  width: calc(var(--xf-w) - var(--xf-cap));
  height: 8px;
  --t: var(--border-strong);
  background:
    linear-gradient(var(--t), var(--t)) 0 0 / 1px 100% no-repeat,
    linear-gradient(var(--t), var(--t)) 50% 0 / 1px 100% no-repeat,
    linear-gradient(var(--t), var(--t)) 100% 0 / 1px 100% no-repeat,
    linear-gradient(to right, var(--t) 1px, transparent 1px) 0 100% / calc(100% / 10) 45% repeat-x;
}
.xf-scale::after {
  /* below the slot the short ticks hang from the top edge instead */
  background:
    linear-gradient(var(--t), var(--t)) 0 0 / 1px 100% no-repeat,
    linear-gradient(var(--t), var(--t)) 50% 0 / 1px 100% no-repeat,
    linear-gradient(var(--t), var(--t)) 100% 0 / 1px 100% no-repeat,
    linear-gradient(to right, var(--t) 1px, transparent 1px) 0 0 / calc(100% / 10) 45% repeat-x;
}
#crossfader {
  width: var(--xf-w);
  --cap-w: var(--xf-cap);
  --cap-h: 24px;
  --groove: 6px;
}
/* The per-stem mini-faders: a hand-sized cap on a slot that takes the width the row has. */
.mix-ctl {
  --cap-w: 12px;
  --cap-h: 20px;
  --groove: 4px;
  flex: 1;
  min-width: 90px;
}
.mix-actions {
  display: flex;
  gap: 6px;
}
/* The FX unit: three compact rows between the crossfader and the action buttons. Everything
   fixed-width or ellipsized, so neither a long plugin name nor the pad lighting can widen the
   desk's center column. The depth knob is an ordinary mix-knob. */
.mix-fx {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 180px; /* the crossfader's width - the center column's one agreed measure */
}
.mix-fx-row {
  display: flex;
  align-items: center;
  gap: 4px;
}
.mix-fx-row select {
  flex: 1;
  min-width: 0;
  text-overflow: ellipsis;
}
.mix-fx-name {
  flex: 1;
  min-width: 0;
}
.mix-fx-on {
  min-width: 40px;
  text-align: center;
}
.mix-fx-on.on,
#mixFxStar.on {
  color: var(--accent);
  border-color: var(--accent);
}
.mix-fx-assign {
  min-width: 58px;
  text-align: center;
}
.mix-fx-knob {
  display: inline-flex;
  justify-content: center;
  flex: none;
}

/* --- the FX chain editor: a modal that is a real editor (see openFxEditor) --- */
#fxEdModal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--bg) 70%, transparent);
}
/* An ID's `display: flex` outranks the `.hidden` class, so the hidden state has to be said at
   the same specificity - as #askModal's is. Without it the panel is up on every page load,
   before anything has wired its close button (see fxEditorBuild). */
#fxEdModal.hidden {
  display: none;
}
#fxEdPanel {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(820px, 92vw);
  max-height: 80vh;
  padding: 10px 12px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--bg-panel);
  box-shadow: 0 8px 32px var(--shadow);
}
/* Scoped, like every other panel here - there is no global .hidden utility, and `button.small`
   outranks a bare class anyway. Without this the delete button is up on a chain that does not
   exist yet (openFxEditor hides it by class). */
#fxEdPanel .hidden {
  display: none;
}
.fx-ed-head,
.fx-ed-foot {
  display: flex;
  align-items: center;
  gap: 8px;
}
.fx-ed-name {
  width: 220px;
}
/* The code row: the grayed head, then the editor taking the rest. */
.fx-ed-body {
  display: flex;
  align-items: flex-start;
  gap: 0;
  min-height: 0;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-raised);
  overflow: hidden;
}
.fx-ed-song {
  flex: none;
  padding: 6px 0 6px 8px;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-dim);
  user-select: none;
}
.fx-ed-cm {
  flex: 1;
  min-width: 0;
  overflow: auto;
}
.fx-ed-cm .CodeMirror {
  height: auto;
  min-height: 120px;
  max-height: 46vh;
  background: transparent;
  font-size: 13px;
}
.fx-ed-wet {
  display: flex;
  align-items: center;
  gap: 5px;
  color: var(--text-dim);
}
.fx-ed-msg {
  font-size: 11px;
  color: var(--text-dim);
}
.fx-ed-msg.bad {
  color: var(--err);
}
/* Swap is a toggle button, drawn like the desk's other latching buttons (stack, cue, midi).
   The checkbox is still there - client.js reads .checked and listens for change - it just
   isn't painted; the label wearing the button look is what you press. */
.mix-swap-label {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  font-size: 11px;
  color: var(--text);
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
  user-select: none;
  transition: background 0.12s, border-color 0.12s;
}
.mix-swap-label:hover {
  background: var(--hover-bg);
  border-color: var(--border-strong);
}
.mix-swap-label input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  margin: 0;
  pointer-events: none;
}
.mix-swap-label:has(input:checked) {
  color: var(--accent);
  border-color: var(--accent);
}
.mix-swap-label:has(input:focus-visible) {
  outline: 1px solid var(--accent);
  outline-offset: 1px;
}
.mix-cue.on {
  color: var(--ok);
  border-color: var(--ok);
}
#mixLearn.on {
  color: var(--warn);
  border-color: var(--warn);
}
/* The ctrl+hover tooltip (see tipBox in client.js): every title in the app, shown only while
   ctrl is held over the control - never a wall of text over what you are reaching for. */
#tipBox {
  position: fixed;
  z-index: 200;
  max-width: 360px;
  padding: 6px 9px;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--text);
  background: var(--bg-panel);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  box-shadow: 0 6px 20px var(--shadow);
  pointer-events: none;
  white-space: pre-wrap;
}
#tipBox.hidden {
  display: none;
}

/* Every scrollable surface in the app carries the theme (thin, border-colored thumb) - stock
   scrollbars were the one bit of chrome the theme didn't reach, and each new pane was picking
   them back up one at a time. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
*::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
*::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 4px;
}
*::-webkit-scrollbar-track {
  background: transparent;
}
/* The deck B editor sizes by flex (the global .CodeMirror height:100% overflowed the pane past
   its header, which is what put a stray scrollbar on the pane itself). */
#deckBPane .CodeMirror {
  height: auto;
  min-height: 0;
}

/* --- the song decks' waveform panes (songs phase 3; one per deck since deck A learned to
       hold files): shown instead of the editor while the deck holds a disk file. The editor
       stays in the DOM (deck B's holds the descriptor card, deck A's your untouched wip
       buffer), just not on screen. --- */
#deckBPane.song-on .CodeMirror,
#editorPane.song-on .CodeMirror {
  display: none;
}
.song-pane {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  gap: 6px;
  padding: 8px 8px 3px; /* the control row is the bottom edge - no ledge under it */
}
.song-pane.hidden {
  display: none;
}
.song-title-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-size: 12px;
}
.song-title {
  color: var(--accent);
  font-weight: 600;
  flex: 1; /* the growing element - rate and time keep right, however many spans the row holds */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.song-time {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-dim);
  white-space: nowrap;
}
.song-phrase {
  min-width: 9ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.song-rate {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--accent);
  white-space: nowrap;
  /* Room for the longest it says ("-100.0%") whether or not it is showing, so the rate coming
     and going never moves the readouts beside it. */
  min-width: 7ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.song-ctl-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
}
/* The row's two halves - cue and the platter, then the tempo controls - each kept whole, so a
   deck too narrow for one line (see the deck container query) breaks between them, never inside. */
.song-ctl-group {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  row-gap: 4px;
}
/* The transport glyphs (jog |◀ ▶| and nudge ◀ ▶) are drawn, not typed: as text, the bar and
   the arrow are two characters of a font with no say in how they line up - the bar sat off the
   arrow's point, and at enough zoom wrapped onto its own line. One grid for all four. */
.song-ctl-row .small:has(.song-ctl-icon),
.deck-live-ctl:has(.song-ctl-icon) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-left: 7px;
  padding-right: 7px;
}
.song-ctl-icon {
  display: block;
  width: 12px;
  height: 1.2em; /* the text line's height, so these buttons stand as tall as `cue` and `sync` */
  fill: currentColor;
}
.song-ctl-row .small.on,
.deck-live-ctl.on {
  color: var(--accent);
  border-color: var(--accent);
}
.song-ctl-row .small.held,
.deck-live-ctl.held {
  color: var(--bg-panel);
  background: var(--accent);
  border-color: var(--accent);
}
/* The platter and sync on the deck head are for a deck playing code; a deck holding a file has
   them on its own pane, and these step aside. */
.deck-live-ctl.hidden,
body.mix-on #deckAHead .deck-live-ctl.hidden {
  display: none;
}
.song-ctl-gap {
  flex: 1;
}
/* A hairline between control groups (cue | platter), where a flex gap would push them apart. */
.song-ctl-sep {
  width: 1px;
  align-self: stretch;
  background: var(--border);
}
/* While the cue is HELD (the preview is running) this wears the `held` state the platter
   buttons already have; the marker on the two waveforms is the other half of the feedback. */
.song-cue {
  min-width: 40px;
}
.song-bpm-label {
  font-size: 11px;
  color: var(--text-dim);
  white-space: nowrap;
}
.song-bpm {
  width: 58px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.song-bpm.detected {
  /* a phase 5 ESTIMATE, not a tag or a typed value - dimmed italics until the hand confirms */
  color: var(--text-dim);
  font-style: italic;
}
.song-pane.active .song-detail {
  border-color: var(--accent); /* the active "window": Cmd+Enter plays, Cmd+. stops this deck */
}
.song-detail {
  flex: 1;
  min-height: 80px;
  width: 100%;
  cursor: grab;
  touch-action: none; /* pointer capture owns the drag - no scroll/zoom gestures underneath */
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-panel);
}
.song-detail.dragging {
  cursor: grabbing;
}
.song-overview {
  height: 56px;
  flex: none;
  width: 100%;
  cursor: pointer;
  touch-action: none;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-panel);
}

/* --- the per-deck knob columns (trim / low / mid / high / filter), mirrored around center --- */
.mix-tracks-col {
  /* basis 0, not auto: both track columns get EQUAL width regardless of content, which is what
     keeps the shared center controls straddling the exact midline between the two panes. */
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
/* Deck B mirrors deck A around the center: its rows run right-to-left (fader | name | gate,
   packed against the right edge) the way its head already reads cue-then-title. */
#mixTracksB .mix-track {
  flex-direction: row-reverse;
}
#mixTracksB .mix-name {
  text-align: right;
}
.mix-knobs {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  padding: 2px 6px;
}
/* One row of the knob column. Most rows hold a single knob; the filter shares its row with its
   resonance, which keeps the column (and so the whole strip's height) exactly as tall as it was
   when the filter stood alone. Deck A's row reverses so the pair mirrors about the midline like
   everything else on the desk - and so the FILTER is the inboard knob on both sides, nearest the
   hand resting at the crossfader. Resonance is set once a track; the filter is ridden. */
.mix-knob-row {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 6px;
}
#mixKnobsA .mix-knob-row {
  flex-direction: row-reverse;
}
.mix-knob-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}
.mix-knob-wrap > span {
  font-size: 9px;
  color: var(--text-dim);
  line-height: 1;
}
.mix-knob {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-raised);
  position: relative;
  cursor: default;
  touch-action: none;
}
.mix-knob::after {
  content: '';
  position: absolute;
  left: calc(50% - 1px);
  bottom: 50%;
  width: 2px;
  height: 11px;
  background: var(--accent);
  border-radius: 1px;
  transform-origin: 50% 100%;
  transform: rotate(var(--ang, 0deg));
}
.mix-knob.dragging {
  border-color: var(--accent);
}

/* --- the channel faders + channel meters, flanking the center (client-built; see
   mixMakeFader/mixMeterPaint). Mirrored pairs like everything on the desk: deck A runs
   knobs | fader | meter toward the center, deck B meter | fader | knobs away from it. --- */
.mix-dfader-col,
.mix-tempo-col,
.mix-meter-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 2px 0;
}
/* The deck's pitch fader column: the same fader chrome as the channel fader beside it, plus the
   range switch and two readouts. Everything is fixed-width and tabular, so a tempo ticking over
   under the hand cannot widen the column and shift the whole desk. */
.mix-tempo-col {
  width: 54px;
  flex: none;
}
.mix-tempo-col > .mix-tempo-range {
  min-width: 46px;
  padding: 0 2px;
  font-size: 9px;
}
.mix-tempo-read,
.mix-tempo-pct {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  white-space: nowrap;
}
.mix-tempo-read {
  font-size: 10px;
  color: var(--text);
}
.mix-tempo-pct {
  font-size: 9px;
  color: var(--text-dim);
}
/* A fader that is following someone else's grid: dimmed, and its grip holds still. Not
   `disabled` - the element is a div, and the throw it is showing still counts the moment sync
   lets go (see deckTempoApply). */
.mix-tempo-fader.locked {
  opacity: 0.4;
  pointer-events: none;
}
.mix-dfader-col > span {
  font-size: 9px;
  color: var(--text-dim);
  line-height: 1;
}
.mix-dfader {
  flex: 1;
  min-height: 0;
  width: 24px;
  position: relative;
  cursor: default;
  touch-action: none;
}
.mix-dfader::before {
  /* the groove */
  content: '';
  position: absolute;
  left: calc(50% - 2px);
  top: 4px;
  bottom: 4px;
  width: 4px;
  border-radius: 2px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
}
.mix-dfader-grip {
  position: absolute;
  left: 0;
  width: 100%;
  height: 18px;
  /* --pos is the fader value (0 bottom .. 1 top), set by mixMakeFader */
  top: calc((100% - 18px) * (1 - var(--pos, 1)));
  border-radius: 3px;
  border: 1px solid var(--border);
  background: var(--bg-panel);
}
.mix-dfader-grip::after {
  /* the fader cap's index line */
  content: '';
  position: absolute;
  left: 2px;
  right: 2px;
  top: calc(50% - 1px);
  height: 2px;
  background: var(--accent);
}
.mix-dfader.dragging .mix-dfader-grip {
  border-color: var(--accent);
}
.mix-meter {
  flex: 1;
  min-height: 0;
  width: 34px;
}
.mix-meter-db {
  font-family: var(--mono);
  font-size: 9px;
  color: var(--text-dim);
  line-height: 1;
  min-width: 32px;
  text-align: center;
}
.mix-meter-db.over {
  color: #f85149;
}
/* The master's peak and its clip light (the desk's center, and the mixer's header). */
.master-peak {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: none;
}
.master-peak-light {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--border);
}
.master-peak.over .master-peak-light {
  background: #f85149;
}
.master-peak.over .mix-meter-db {
  color: #f85149;
}

/* --- the organize modal (playlists; see the library section of client.js) --- */
/* the app's own confirm dialog (askDialog in client.js) - above every other modal */
#askModal {
  position: fixed;
  inset: 0;
  background: var(--shadow);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 80;
}
#askModal.hidden {
  display: none;
}
#askPanel {
  width: min(420px, 90vw);
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 12px 40px var(--shadow);
  padding: 16px;
}
#askPanel p {
  margin: 0 0 14px;
  font-size: 13px;
  line-height: 1.45;
  white-space: pre-line; /* a question with two parts (what goes, what stays) keeps its break */
}
.ask-row {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

/* One labeled control between the question and the buttons (askSelect) - how the piano roll's
   ctrl+Q asks which division to snap to. */
.ask-field {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 14px;
  font-size: 12px;
  color: var(--text-dim);
}

.ask-field select {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  color: var(--text);
}

/* ---------------------------------------------------------------------------------------------
   The library browser (the organize window - see the library browser section in client.js).

   Two regions: the sources on the left, and whatever the chosen one holds on the right, as a
   table with sortable columns. The table is laid out FIXED, with a width on every column class,
   so a row whose artist or tempo arrives late cannot shuffle the columns under the pointer.
   --------------------------------------------------------------------------------------------- */

#orgModal {
  position: fixed;
  inset: 0;
  background: var(--shadow);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 60;
}
#orgModal.hidden {
  display: none;
}
#orgPanel {
  /* Wider and taller than the old three-pane window: the columns are the feature now. */
  width: min(1060px, 94vw);
  height: min(640px, 88vh);
  display: flex;
  flex-direction: column;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 12px 40px var(--shadow);
  overflow: hidden;
}
#orgPanel header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}
.org-title {
  font-weight: 600;
}
#orgSearch {
  width: 260px;
}
#orgCols {
  flex: 1;
  min-height: 0;
  display: grid;
  /* the tracks are written by orgApplyCols (sidebar | seam | table) */
  grid-template-columns: minmax(0, 0.28fr) 7px minmax(0, 1fr);
}
.org-col {
  min-width: 0;
  /* Grid items default to min-height auto, so without this a long list stretches the region
     (and its separator border) past the panel into the footer instead of scrolling. */
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  position: relative;
}
/* The seam between the two regions: the DJ seams' grip, turned on its side. */
.org-seam {
  position: relative;
  cursor: col-resize;
  touch-action: none;
  border-left: 1px solid var(--border);
  border-right: 1px solid var(--border);
  background: var(--bg-panel);
}
.org-seam::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 3px;
  height: 46px;
  border-radius: 2px;
  transform: translate(-50%, -50%);
  background: var(--border);
}
.org-seam:hover::before,
.org-seam.dragging::before {
  background: var(--accent);
}
.org-seam.dragging {
  background: color-mix(in srgb, var(--accent) 12%, var(--bg-panel));
}
/* Folded to a rail: the region's contents go, its name stands on end; a click brings it back. */
.org-rail {
  display: none;
}
.org-col.min > * {
  display: none;
}
.org-col.min > .org-rail {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  height: 100%;
  padding-top: 10px;
  writing-mode: vertical-rl;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
  cursor: pointer;
  user-select: none;
}
.org-col.min > .org-rail:hover {
  color: var(--accent);
}

/* --- the sidebar: the sources, and the folder trees under two of them --- */

#orgSide {
  flex: 1;
  min-height: 0;
  margin: 0;
  padding: 6px 4px 8px;
  list-style: none;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
#orgSide::-webkit-scrollbar {
  width: 8px;
}
#orgSide::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 4px;
}
#orgSide::-webkit-scrollbar-track {
  background: transparent;
}
.org-side-row {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  border-radius: 5px;
  font-size: 12px;
  cursor: pointer;
  position: relative;
}
.org-side-row:hover {
  background: var(--bg-raised);
}
.org-side-row.on,
.org-side-row.on:hover {
  background: var(--accent-soft);
  color: var(--accent);
}
/* Where a learned browse knob is standing (orgMidiPaintCursor). Distinct from `.on`, which is
   the source that is OPEN: the knob can be hunting three rows away from it, and until enter is
   pressed nothing has been opened. An inset ring rather than a fill, so it reads ON TOP of the
   open row when they are the same one. */
.org-side-row.org-cursor {
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* The four source headings read as headings; the folders and sets under them read as contents. */
.org-side-group > .org-side-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.org-side-crumb > .org-side-label {
  font-family: var(--mono);
  font-size: 11px;
}
.org-side-folder > .org-side-label {
  color: var(--text-dim);
}
.org-side-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.org-side-row em {
  font-style: normal;
  color: var(--text-dim);
  font-size: 10px;
  flex: none;
}
.org-twisty {
  flex: none;
  width: 12px;
  padding: 0;
  background: none;
  border: none;
  font: inherit;
  font-size: 9px;
  line-height: 1;
  color: var(--text-dim);
  cursor: pointer;
  text-align: center;
}
.org-twisty:hover {
  color: var(--accent);
}
.org-twisty-none {
  cursor: default;
}
/* The + on the playlists heading, and the active-set marker on a set, both ride the row. */
.org-side-row > button.small {
  flex: none;
  width: 20px;
  padding: 1px 0;
  text-align: center;
}
.org-side-row.org-over {
  background: var(--selection);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* --- the bar over the table: where you are, and this source's own actions --- */

#orgBar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  border-bottom: 1px solid var(--border);
}
#orgWhere {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
}
#orgCount {
  flex: none;
  font-style: normal;
  font-size: 10px;
  color: var(--text-dim);
}
/* Scoped, like every other panel here - there is no global .hidden utility, so a bare `hidden`
   class on one of these would do nothing at all. */
#orgUp.hidden,
#orgImport.hidden,
#orgMeasure.hidden,
#orgRescan.hidden,
#orgClean.hidden {
  display: none;
}
#orgUp,
#orgImport,
#orgMeasure,
#orgRescan,
#orgClean {
  flex: none;
  white-space: nowrap;
}

/* The filter button. Its label says what the filter is set to, so it changes width as the filter
   does - hence a width of its own, and a place LEFT of the bar's spacer: the spacer takes up the
   slack, and nothing in the bar moves when the filter is switched. */
#orgFilter {
  flex: none;
  width: 92px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#orgFilter.on {
  color: var(--accent);
  border-color: var(--accent);
}

/* The guided mapping session's feedback line, when a control has just bound (orgMidiGuide). The
   dialog's own dim line otherwise - this says only that the last question was answered. */
.org-guide-got {
  color: var(--ok);
}

/* --- the table --- */

#orgTableWrap {
  flex: 1;
  min-height: 0;
  overflow: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
#orgTableWrap::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
#orgTableWrap::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 4px;
}
#orgTableWrap::-webkit-scrollbar-track {
  background: transparent;
}
#orgTable {
  width: 100%;
  /* Fixed, so the column widths below are the widths - a late-arriving artist or tempo must not
     move the columns under the pointer. */
  table-layout: fixed;
  border-collapse: collapse;
  font-size: 12px;
}
#orgTable thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 6px 8px;
  text-align: left;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  background: var(--bg-panel);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  overflow: hidden;
}
#orgTable thead th:hover {
  color: var(--accent);
}
#orgTable thead th.sorted {
  color: var(--text);
}
.org-caret {
  font-style: normal;
  font-size: 8px;
  margin-left: 3px;
  color: var(--accent);
}
#orgBody:focus {
  outline: none; /* the selection IS the focus here, as in the pack browser */
}
#orgBody tr {
  cursor: default;
  border-top: 2px solid transparent;
  border-bottom: 2px solid transparent;
}
#orgBody td {
  padding: 3px 8px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 45%, transparent);
}
#orgBody tr:hover td {
  background: var(--bg-raised);
}
/* THE selected row. The accent wash the other browsers use, plus a hairline along the row's top
   and bottom so it reads as lit rather than merely tinted - a table row cannot take the glow the
   list rows get (a box-shadow on a tr does not paint reliably), so the ring is drawn per cell. */
#orgBody tr.selected td,
#orgBody tr.selected:hover td {
  background: var(--accent-soft);
  color: var(--accent);
  border-top: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
}
/* Tonight's rows. Two states, two washes, both weaker than the selection's so a selected row
   still reads as the selected one on top of either: a track ON a playing deck, and a track that
   has already gone out this session. Backgrounds rather than text colors, because the point is
   to be seen while scanning a long crate rather than read. */
#orgBody tr.org-on-deck td {
  background: color-mix(in srgb, var(--ok) 16%, transparent);
}
#orgBody tr.org-on-deck:hover td {
  background: color-mix(in srgb, var(--ok) 24%, transparent);
}
#orgBody tr.org-played td {
  background: color-mix(in srgb, var(--warn) 10%, transparent);
  color: var(--text-dim);
}
#orgBody tr.org-played:hover td {
  background: color-mix(in srgb, var(--warn) 18%, transparent);
}
/* The selection outranks both - it is where the keyboard is. Only the two-class combinations are
   stated here: restating the plain `.selected` rule from above would be a second rule under a
   selector that already exists, which is the trick that silently un-stuck the table headers. */
#orgBody tr.org-on-deck.selected td,
#orgBody tr.org-on-deck.selected:hover td,
#orgBody tr.org-played.selected td,
#orgBody tr.org-played.selected:hover td {
  background: var(--accent-soft);
  color: var(--accent);
}
#orgBody tr.playing td.org-c-name {
  color: var(--accent);
}
#orgBody tr.playing td.org-c-name::before {
  content: '▶ ';
  font-size: 9px;
}
/* Already in the collection - the disk view's ✓, which is what tells you at a glance what you
   have got without opening tracks. */
#orgBody tr.org-added td.org-c-name::after {
  content: ' ✓';
  color: var(--ok);
}
#orgBody tr.org-missing td.org-c-name {
  color: var(--err);
  font-style: italic;
}
/* A cell being corrected. Sized to the cell it replaces so the table does not move when a row
   goes into editing - the columns are fixed, and the field has to live inside one. */
input.org-edit {
  width: 100%;
  min-width: 0;
  padding: 0 2px;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 3px;
}
.org-c-bpm input.org-edit,
.org-c-key input.org-edit {
  font-family: var(--mono);
}
.org-rowdir {
  font-style: normal;
  color: var(--text-dim);
}
.org-rowmissing {
  font-style: italic;
  color: var(--err);
}
/* The drop markers, for reordering a set in its own order. */
#orgBody tr.org-before td {
  border-top: 2px solid var(--accent);
}
#orgBody tr.org-after td {
  border-bottom: 2px solid var(--accent);
}
.org-over {
  background: var(--selection);
}

/* The columns. Name takes what is left; everything else is sized to what it holds, which is what
   keeps the table from moving as rows come and go. */
.org-c-n {
  width: 34px;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--text-dim);
  text-align: right;
}
#orgTable thead th.org-c-n {
  text-align: right;
}
.org-c-name {
  width: auto;
}
.org-c-folder {
  width: 120px;
  color: var(--text-dim);
}
.org-c-text {
  width: 140px;
}
/* The stars. Five glyphs always, so rating a row never changes its width (and the hollow ones
   are the click target for raising it).

   Wide enough for all five, with room to spare: the star glyph usually comes from a fallback font
   whose advance width this stylesheet cannot know, and at 72px the cell was ellipsizing after
   three. The cell also refuses to ellipsize at all - a clipped star is bad, but "★★★…" is a
   rating you cannot read or click. */
.org-c-rating {
  width: 104px;
}
#orgBody td.org-c-rating {
  text-overflow: clip;
}
.org-stars {
  display: inline-block;
  white-space: nowrap;
  cursor: pointer;
  letter-spacing: 1.5px;
  font-size: 12px;
  color: var(--text-dim);
  user-select: none;
}
.org-stars.rated {
  color: var(--accent);
}
.org-stars:hover {
  color: var(--accent);
}
/* A tempo or key we MEASURED rather than read off the file - an estimate, and marked as one. */
/* A measuring pass that is standing aside for the decks, rather than working. */
.org-waiting {
  opacity: 0.7;
  font-style: italic;
}
.org-guess {
  opacity: 0.62;
  font-style: italic;
}
/* No width here: this column's width, its floor and the point below which it drops the
   hundredths all live together in ORG_COLUMNS.bpm, because they are one decision. */
.org-c-bpm {
  font-family: var(--mono);
  text-align: right;
}
#orgTable thead th.org-c-bpm,
#orgTable thead th.org-c-when {
  text-align: right;
}
.org-c-type {
  width: 64px;
  color: var(--text-dim);
}
.org-c-key {
  width: 48px;
  font-family: var(--mono);
}
.org-c-when {
  width: 68px;
  color: var(--text-dim);
  text-align: right;
}
.org-c-tags {
  width: 140px;
}
.org-c-path {
  width: 180px;
  color: var(--text-dim);
  direction: rtl; /* a long path keeps its tail - the folder the file is actually in */
  text-align: left;
}
/* The resize grip on a header's right edge. Sits over the boundary rather than beside it, so the
   pointer finds it without hunting, and it is the one part of a header that is not a sort.
   It needs a positioned header to sit in, and it already has one: the `position: sticky` above
   is a containing block for an absolute child just as `relative` would be. Restating `relative`
   here (the same selector, further down the file) is what un-stuck the headers - they scrolled
   away with the rows until 2026-10-04. */
.org-th-grip {
  position: absolute;
  top: 0;
  right: -3px;
  width: 7px;
  height: 100%;
  cursor: col-resize;
  touch-action: none;
}
.org-th-grip:hover::after,
.org-th-grip.dragging::after {
  content: '';
  position: absolute;
  top: 2px;
  bottom: 2px;
  left: 3px;
  width: 1px;
  background: var(--accent);
}
.org-th-label {
  overflow: hidden;
  text-overflow: ellipsis;
}
.org-c-tags i {
  font-style: normal;
  font-size: 10px;
  color: var(--text-dim);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0 4px;
  margin-right: 3px;
}
td.org-empty {
  padding: 10px;
  color: var(--text-dim);
  font-style: italic;
}
.org-empty {
  color: var(--text-dim);
  font-style: italic;
}

/* THE chosen row, wherever a browser has one - the preset picker's highlighted row, the browser
   sidebar's open source, the snippet browser's current row. The accent wash all three already
   had, plus a glow: a hairline in the accent and a soft halo, so the chosen row reads as lit
   rather than merely tinted, and reads the same from one window to the next. (The table's own
   selected rows are lit per cell instead - see above - because a tr cannot carry this.) */
.def-pick-row.on,
.org-side-row.on,
.snippet-row.current {
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent),
    0 0 8px color-mix(in srgb, var(--accent) 22%, transparent);
}

#orgFoot {
  padding: 6px 12px;
  border-top: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 11px;
}
#orgPlayRow {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}
#orgPlayRow.hidden {
  display: none;
}
#orgNote.error {
  color: var(--warn);
}

/* ---------------------------------------------------------------------------------------------
   Snippets - the save dialog and the browser (see the snippets section in client.js).

   Both are dir-pickers, so they inherit the panel, the header and the footer; what's added here is
   the code window the save dialog edits in, the two-column browser, and the chip row both use to
   show what a snippet CARRIES - the rolls, shapes, presets and packs riding along with the code.
   --------------------------------------------------------------------------------------------- */

.snippet-dialog {
  width: min(720px, 94vw);
  height: min(560px, 84vh);
  max-height: none;
}

/* The arrangement's color chooser: the platform's own input, kept out of sight and opened
   from the clip menu (see arPickColor). Not display:none - a hidden input can't be clicked open. */
.arrange-color-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
  left: 0;
  top: 0;
}

.snippet-browser {
  width: min(860px, 94vw);
  height: min(560px, 84vh);
  max-height: none;
}

.snippet-fields {
  display: flex;
  gap: 8px;
  padding: 10px 12px 0;
}

.snippet-fields .name-dialog-input {
  flex: 1;
  margin: 0;
}

/* The body of the snippet, editable before it is kept. Fills what the fields and the chip row
   leave, the same way the prebake editor fills its dialog. */
.snippet-dialog .CodeMirror {
  flex: 1;
  height: auto;
  margin: 10px 12px 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 13px;
}

/* What rides along with the code. One chip per definition, each droppable - a snippet about to
   carry a captured plugin program says so here rather than surprising you with the file size. */
.snippet-carries {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  padding: 9px 12px;
  min-height: 0;
}

.snippet-carries:empty {
  display: none;
}

.snippet-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 5px 2px 8px;
  font-size: 10.5px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-dim);
}

.snippet-chip b {
  font-weight: 700;
  color: var(--text);
}

/* Dropped by hand, or a name whose definition couldn't be found to copy - either way it isn't
   coming along, and the chip says so instead of vanishing. */
.snippet-chip.off {
  opacity: 0.45;
  text-decoration: line-through;
}

.snippet-chip button {
  background: none;
  border: 0;
  padding: 0 2px;
  font: inherit;
  line-height: 1;
  color: var(--text-dim);
  cursor: pointer;
}

.snippet-chip button:hover {
  color: var(--text);
}

.snippet-browse-body {
  display: flex;
  flex: 1;
  min-height: 0;
  gap: 1px;
  background: var(--border);
  margin-top: 10px;
  border-top: 1px solid var(--border);
}

.snippet-list {
  flex: 0 0 44%;
  overflow-y: auto;
  padding: 6px 8px;
  background: var(--bg-panel);
  outline: none;
}

.snippet-side {
  display: flex;
  flex: 1;
  min-width: 0;
  flex-direction: column;
  background: var(--bg-panel);
}

/* The browser's right-hand pane: the snippet's body, a read-only preview until a click on a row
   unlocks it (see unlockSnippetEditor). Borderless and flush to the column - the seam beside the list is the divider,
   so a second frame around this would only draw a box inside a box. */
.snippet-browser .CodeMirror {
  flex: 1;
  min-height: 0;
  height: auto;
  margin: 0;
  border: 0;
  border-radius: 0;
  font-size: 11.5px;
}

.snippet-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 8px;
  border-radius: 6px;
  cursor: pointer;
}

.snippet-row:hover {
  background: var(--bg-raised);
}

.snippet-row.current {
  background: var(--accent-soft);
  color: var(--accent);
}

.snippet-row.current .snippet-row-meta {
  color: var(--accent);
  opacity: 0.7;
}

/* Pinned (clicked): a hover elsewhere no longer moves the highlight, and the row says so with a
   solid edge on the left where the hover-only highlight has none. */
.snippet-row.pinned {
  box-shadow:
    inset 3px 0 0 var(--accent),
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent),
    0 0 8px color-mix(in srgb, var(--accent) 22%, transparent);
}

.snippet-row-main {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}

/* Clicking the row opens it; clicking the NAME renames it in place - so the name is sized to its
   own text rather than stretching over the whole row and swallowing clicks meant for the row. The
   affordance is the preset picker's (a text cursor, no underline: .def-pick-renamable). */
.snippet-row-label {
  align-self: flex-start;
  max-width: 100%;
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: text;
}

.snippet-row-rename {
  width: 100%;
  margin: 0;
  padding: 0 4px;
  font: inherit;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 4px;
  outline: none;
}

/* An edit under way on the right: the list goes quiet (no hover preview, no insert - see
   snippetEditDirty) and the footer swaps insert for cancel/save. */
.snippet-when-editing,
.editing .snippet-when-browsing {
  display: none;
}

.editing .snippet-when-editing {
  display: inline-block;
}

.editing .snippet-list {
  opacity: 0.45;
  pointer-events: none;
}

.snippet-row-meta {
  font-size: 10px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.snippet-empty {
  padding: 10px 8px;
  font-size: 11.5px;
  color: var(--text-dim);
}

/* Context-menu categories: hovering a trigger unfolds a nested menu beside it. The nested menu
   keeps the ctx-menu skin but sits absolutely inside its row, so dismissal sees one DOM tree;
   .flip swaps it to the left when the right edge would run off screen. */
.ctx-menu .ctx-sub-holder {
  position: relative;
}

.ctx-menu .ctx-sub-trigger::after {
  content: '▸';
  float: right;
  margin-left: 1.5em;
  opacity: 0.6;
}

.ctx-menu .ctx-sub {
  position: absolute;
  left: calc(100% - 2px);
  top: -5px;
  max-height: 70vh;
  overflow-y: auto;
}

.ctx-menu .ctx-sub.flip {
  left: auto;
  right: calc(100% - 2px);
}

/* ---------------------------------------------------------------------------------------------
   Slice editor (see the slice editor section in client.js). One wide canvas and a foot: the
   waveform IS the panel, so everything else is kept to a strip along the bottom. Sized like the
   pack dialog because a waveform wants the width for the same reason three lists do. --------- */
.slice-dialog {
  width: min(1040px, 94vw);
  max-height: 80vh;
  height: min(480px, 80vh);
}

.slice-dialog .hidden {
  display: none;
}

.slice-dialog .dir-picker-head {
  text-transform: none;
  letter-spacing: 0;
}

.slice-dialog .dir-picker-head > span:first-child {
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.slice-dialog .def-pick-wrap {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* The file being chopped, next to the set's name - a chop map means nothing without it. */
.slice-file {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-dim);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  max-width: 34ch;
}

.slice-body {
  flex: 1;
  min-height: 0;
  padding: 8px 10px;
}

.slice-canvas {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  outline: none;
  cursor: crosshair;
}

.slice-canvas:focus {
  border-color: var(--border-strong, var(--border));
}

/* Enough controls now that a narrow window would push the count and the note off the end of the
   row: it wraps onto a second line instead, which is the one place the panel can afford the pixels. */
.slice-foot {
  gap: 10px;
  flex-wrap: wrap;
  row-gap: 6px;
}

/* ▶ and ■ are different widths, so the foot shifted every time the audition started or stopped.
   The button is sized for the wider of them and the glyph is centered inside it. */
.slice-play {
  min-width: 2.4em;
  text-align: center;
}

.slice-sens {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 10.5px;
  color: var(--text-dim);
}

/* The desk's fader (.dj-fader), at the size the ctx-menu's params use - the app draws its own
   sliders, and this is the small one. */
.slice-sens input[type='range'] {
  width: 110px;
  --cap-w: 8px;
  --cap-h: 14px;
  --groove: 3px;
}

/* Stood down: these markers were drawn by hand, and re-slicing would throw them away (auto-slice
   is the gesture that says to). Dimmed as a pair, so the label reads as off too. */
.slice-sens-off {
  opacity: 0.45;
}

/* The fit group reads as one control: the toggle, what it resolved to, and the two ways to try
   the number either side of it. */
.slice-fit {
  display: flex;
  align-items: center;
  gap: 3px;
}

.slice-fit .small.on {
  color: var(--accent);
  border-color: var(--accent);
}

/* The grid pair reads as one control too: the verb and the division it chops at. */
.slice-grid {
  display: flex;
  align-items: center;
  gap: 3px;
}

/* The foot's dropdowns at the .small buttons' size: the slice grid's division, and the envelope
   panel's loop region and direction. */
.slice-grid-sel,
.slice-foot-sel {
  padding: 2px 20px 2px 7px;
  font-size: 11px;
  border-radius: 4px;
  background-position: right 6px center;
  background-size: 8px 5px;
}

/* Mono digits on the division, like the fit readout's; the loop selects hold words. */
.slice-grid-sel {
  font-family: var(--mono);
}

.slice-fit-val {
  font-family: var(--mono);
  min-width: 6.5em;
}

.slice-fit-val:disabled {
  opacity: 1; /* not an "unavailable" look - it is the readout, and already says auto */
  cursor: default;
}

.slice-sens-val {
  font-family: var(--mono);
  min-width: 2.5em;
}

.slice-count {
  font-size: 10.5px;
  color: var(--text-dim);
  font-family: var(--mono);
}

/* The backup's audio switch sits above the buttons it changes, with room between them. */
.store-audio {
  margin-bottom: 8px;
}

.about-body {
  padding: 12px 16px 14px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--text-dim);
}

.about-body p {
  margin: 0 0 10px;
}

.about-body p:last-child {
  margin-bottom: 0;
}

.about-body a {
  color: var(--accent);
}

/* A curve picker a figure took over (a waveshaper's mode, a grain's window): the lfo panel's own
   name box and list, with the name taking the heading's width - it is the hero of the device. */
.device-shape-pick {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 1;
  min-width: 0;
}

.device-shape-pick .def-name {
  flex: 1;
  width: auto;
  min-width: 0;
}

.device-widget-bare .device-draw {
  flex: none;
  margin: 0;
  font-size: 11px;
  padding: 0 10px;
  border-radius: 5px;
}

/* ------------------------------------------------------------------ embedded
   `?embed` (index.html): a playable example in the guide - the editor and nothing around it. The
   floating panels stay what they are; the guide grows the frame while one is open. */
html[data-embed] .layout > header,
html[data-embed] aside,
html[data-embed] #console,
html[data-embed] #mixStrip,
html[data-embed] #djStripRail,
html[data-embed] #djStripResize,
html[data-embed] #deckAHead,
html[data-embed] #replayBar {
  display: none !important;
}
html[data-embed] .layout {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: 1fr;
}
html[data-embed] main {
  grid-row: 1;
}
html[data-embed],
html[data-embed] body {
  background: var(--bg-raised);
}
html[data-embed] .cm-s-poptart.CodeMirror {
  background: var(--bg-raised);
}
html[data-embed] .cm-s-poptart .CodeMirror-gutters {
  background: var(--bg-raised);
}

/* ---------------------------------------------------------------- replays
   The bar an open replay stands in, across the page under the header: the deck head's look, so it
   reads as transport rather than as a panel. Everything in it holds its width - the time and the
   play/pause button change under the hand and must not move the timeline. */
#replayBar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  border-bottom: 1px solid var(--border);
  background: var(--bg-panel);
  min-width: 0;
}
#replayBar.hidden {
  display: none;
}
.replay-title {
  flex: none;
  font-size: 11px;
  color: var(--text-dim);
  white-space: nowrap;
}
#replayBar .small:has(.song-ctl-icon) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-left: 7px;
  padding-right: 7px;
}
.replay-play {
  flex: none;
  min-width: 64px;
}
#replayTimeline {
  flex: 1;
  min-width: 0;
  height: 26px;
  border: 1px solid var(--border);
  border-radius: 3px;
  background: var(--bg);
  cursor: pointer;
  touch-action: none;
}
#replayTimeline:focus-visible {
  outline: 1px solid var(--accent);
}
.replay-time {
  flex: none;
  min-width: 128px;
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 10px;
  color: var(--text-dim);
}
#replayKeep.on {
  color: var(--accent);
  border-color: var(--accent);
}

/* ------------------------------------------------------- narrow windows: the header + the desk */

/* These sit at the END of the file on purpose: the rules they override are thousands of lines up,
   and at equal specificity the later rule wins. */

/* The header's ⋯ (see headerMenuItems): shown only while the controls it holds are folded away.
   Sized like the theme's edit button it stands in for. */
#headerMoreBtn {
  display: none;
  padding: 0 10px;
}

/* The header never wraps and never pushes the page sideways: as the window narrows it gives up its
   least important pieces first, and play/stop and update stay on screen at every width. The widths
   are where the full row stops fitting: with every control, it is 1313 px (each later step is that,
   less what the steps before it gave up). The clip is the last resort below the last step (see 4),
   and only sideways - the rec options still drop down under their button. */
.layout > header {
  overflow-x: clip;
}
/* 1. The headphone knobs keep their knobs and lose their words (the knobs' tooltips say them). */
@media (max-width: 1320px) {
  .phones-knob > span {
    display: none;
  }
}
/* 2. The phrase circles. */
@media (max-width: 1225px) {
  #phraseViz {
    display: none;
  }
}
/* 3. The theme picker, its edit button and the docs link fold into the ⋯ menu. */
@media (max-width: 1140px) {
  #themeSelect,
  #themeEditBtn,
  #docsLink {
    display: none;
  }
  #headerMoreBtn {
    display: inline-flex;
  }
}
/* 4. Below the narrowest supported window: the wordmark goes (the logo says it), the spacing
   tightens, and the engine's status pill is the one thing that may shrink, ending in "…" -
   it still colors itself for ready and down. */
@media (max-width: 910px) {
  .layout > header {
    /* With room to spare the spacer takes it all and this changes nothing; with none left, the
       row overflows at its START, so what the clip takes is the logo end, never play and update. */
    justify-content: flex-end;
    gap: 6px;
    padding-left: 10px;
    padding-right: 10px;
  }
  .layout > header h1 {
    display: none;
  }
  .layout > header > * {
    flex: none;
  }
  .layout > header > .spacer {
    flex: 1;
  }
  .layout > header > .status {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* The desk. Its fixed columns (the knobs, faders, meters and the center) take 704 px with the
   strip's padding, 684 inside it (a container query measures the inside); the two
   track columns share what is left. Folded in the same order as everything else - words first,
   then pieces - against the strip's own width, so a collapsed sidebar gets the roomy desk back. */
body.mix-on #mixStrip {
  container: strip / inline-size;
}
/* 1. The decks' names go (the desk is mirrored: A is left, B right), and the stem names give way
   to the faders beside them, ending in "…". 1060 = the fixed 684 inside the strip's padding, plus
   two 188 px track columns, the width a stem's row takes with its whole name and fader. */
@container strip (max-width: 1059px) {
  .mix-deck-head > strong {
    display: none;
  }
  #mixStrip .mix-name {
    flex: 0 1 70px;
    min-width: 0;
  }
}
/* 2. The knob columns and the center drop their side padding; each deck's cue and mute-all stand one above the other,
   and a stem's fader takes the line under its gate and name. The two heads stay packed toward the
   center, mirroring each other. */
@container strip (max-width: 959px) {
  .mix-knobs,
  .mix-center {
    padding-left: 0;
    padding-right: 0;
  }
  .mix-deck-head {
    flex-direction: column;
    align-items: flex-end; /* deck A's toward the center, as on one line */
    gap: 3px;
  }
  .mix-tracks-col:last-child .mix-deck-head {
    align-items: flex-start;
  }
  .mix-deck-head > .spacer {
    display: none;
  }
  #mixStrip .mix-track {
    flex-wrap: wrap;
    row-gap: 2px;
  }
  #mixStrip .mix-ctl {
    flex-basis: 100%;
    min-width: 0;
  }
}
/* With the sidebar open, a window this narrow leaves the desk less than it can fold into, so the
   sidebar stops above the desk and the desk takes the window's full width (its center then sits at
   the window's middle rather than under the seam between the decks). 1110 = the sidebar's 320 px
   plus the 790 px the folded desk needs. */
@media (max-width: 1110px) {
  body.mix-on aside {
    grid-row: 3;
  }
  body.mix-on #djStripResize,
  body.mix-on #mixStrip,
  body.mix-on #djStripRail {
    grid-column: 1 / -1;
  }
}
