* {
  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 / 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. */
  grid-template-rows: auto var(--dj-row-main) auto var(--dj-row-strip) auto;
  height: 100vh;
}
.layout > header {
  grid-column: 1 / -1;
}
main {
  grid-column: 1;
  grid-row: 2;
}
aside {
  grid-column: 2;
  grid-row: 2 / span 3; /* down past the handle and mix-strip rows */
}
#djStripResize {
  grid-column: 1;
  grid-row: 3;
}
#mixStrip,
#djStripRail {
  grid-column: 1;
  grid-row: 4;
}
#console {
  grid-column: 1 / -1;
  grid-row: 5;
}

/* ------------------------------------------------------------------ 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);
}

.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);
}

/* 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;
}

/* 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;
}

#wipHeading {
  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;
}

/* 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);
}

[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;
}

/* 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, Strudel-style ("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;
}

/* 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);
}

/* 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: 336px;
  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 {
  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: 1;
  min-width: 0;
  max-width: 60%;
}

.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 {
  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, so the picture still moves but
   the gesture does nothing - the same bargain a driven knob makes. */
.device-figure.driven .device-figure-canvas {
  cursor: not-allowed;
}

.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 DJ desk, a sample map, or a disk to browse: the browser build does not draw their
   buttons (client.js, desktopOnly). */
.web-build #viewDjBtn,
.web-build #sampleMapBtn,
.web-build #orgTabDisk {
  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 {
  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 {
  display: none;
}

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

.lfo-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 {
  font-size: 11px;
}

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

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

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

.lfo-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 {
  border-color: var(--accent);
}

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

.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 {
  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 {
  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 {
  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. */
#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;
}

/* --------------------------------------------------------------- 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;
  display: flex;
  flex-direction: column;
  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 */
}
/* Basis 0 with NO floor: the two waveforms give up height together, all the way down. A
   min-height here (60px until 2026-08-26) stops both of them shrinking at the same moment, and
   since the region goes on getting shorter the lower one is simply pushed out of it - which
   read as "the bottom one squashes while the top keeps its size". */
#songStack .song-detail {
  flex: 1 1 0;
  min-height: 0;
}
#songStack .song-detail.hidden {
  display: none; /* that deck holds no song - the other's waveform takes the whole stack */
}
#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;
}

/* ---------------------------------------------------- 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 */
}
body.mix-on .deck-active .deck-b-title {
  color: var(--accent);
}
/* 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, update and keep. */
#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 (see .deck-song). */
body.mix-on #deckAHead .deck-song {
  justify-content: flex-start;
}
#deckBHead .deck-b-title,
#deckAHead .deck-b-title {
  font-weight: 600;
  color: var(--accent);
  white-space: nowrap;
}
/* 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);
}
/* "keep" while the pane holds a song file (the single-editor world can't hold one) and 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);
}

#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;
}
#mixTempoSlider {
  --cap-w: 10px;
  --cap-h: 20px;
}
/* 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;
}
/* 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);
}
.mix-tempo {
  display: flex;
  align-items: center;
  gap: 6px;
}
#mixTempoSlider {
  width: 110px;
}
.mix-tempo-detent {
  /* FIXED width, not min-width: the whole strip is a flex row whose two track columns split
     whatever the center leaves (flex 1 1 0 each), so anything in the center that resizes with
     its text drags every knob, fader and meter sideways with it. A native tempo with a decimal
     ("121.1" vs "121") did exactly that. Wide enough for a 3-digit bpm and one decimal. */
  width: 52px;
  padding: 2px 2px;
  overflow: hidden;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.mix-tempo-detent.at {
  color: var(--ok);
  border-color: var(--ok);
}
.mix-tempo-status {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  color: var(--text-dim);
}
#mixTempoNow {
  /* fixed for the same reason as the detents above - it counts through a ramp, one frame per
     100ms, and every digit it gains or loses would otherwise shove the desk */
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  width: 66px;
  text-align: right;
}
#mixTempoSecs {
  width: 44px;
}

/* 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-rate {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--accent);
  white-space: nowrap;
}
.song-ctl-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
}
/* 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) {
  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 {
  color: var(--accent);
  border-color: var(--accent);
}
.song-ctl-row .small.held {
  color: var(--bg-panel);
  background: var(--accent);
  border-color: var(--accent);
}
.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-meter-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 2px 0;
}
.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 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;
}
.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);
}

#orgModal {
  position: fixed;
  inset: 0;
  background: var(--shadow);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 60;
}
#orgModal.hidden {
  display: none;
}
#orgPanel {
  width: min(880px, 92vw);
  height: min(560px, 84vh);
  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 (col | seam | col | seam | col) */
  grid-template-columns: minmax(0, 1fr) 7px minmax(0, 1.2fr) 7px minmax(0, 1.4fr);
}
.org-col {
  min-width: 0;
  /* Grid items default to min-height auto, so without this a long list stretches every column
     (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 two columns: 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 column'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);
}
.org-col h3 {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 7px 10px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.org-col ul {
  flex: 1;
  min-height: 0;
  margin: 0;
  padding: 0 6px 8px;
  list-style: none;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.org-col ul::-webkit-scrollbar {
  width: 8px;
}
.org-col ul::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 4px;
}
.org-col ul::-webkit-scrollbar-track {
  background: transparent;
}
.org-col li {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  border-radius: 5px;
  font-size: 12px;
  cursor: default;
  border-top: 2px solid transparent;
  border-bottom: 2px solid transparent;
}
.org-col li.on {
  background: var(--accent-soft);
  color: var(--accent);
}
.org-col li:hover {
  background: var(--bg-raised);
}
.org-col li.on:hover,
.org-col li.selected:hover {
  background: var(--accent-soft);
}
.org-col li > span {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.org-col li em {
  font-style: normal;
  color: var(--text-dim);
  font-size: 11px;
}
.org-col li .org-bpm {
  font-family: var(--mono);
}
/* A row's buttons (✎ ↑ ↓ ✕, +, the star) float over the row's tail on hover instead of
   holding a lane open: four hidden-but-present buttons were costing every title ~110px.
   Slotted from the right by nth-last-of-type, so however many a row has they pack the same. */
.org-col li {
  position: relative;
  padding-right: 30px; /* the always-on star's lane */
}
.org-col li button {
  display: none;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 22px;
  padding: 1px 0;
  text-align: center;
}
.org-col li:hover button,
.org-col li button.org-star {
  display: inline-block;
}
.org-col li button:nth-last-of-type(1) { right: 4px; }
.org-col li button:nth-last-of-type(2) { right: 28px; }
.org-col li button:nth-last-of-type(3) { right: 52px; }
.org-col li button:nth-last-of-type(4) { right: 76px; }
.org-col li button:disabled {
  opacity: 0.35;
}
.org-del:hover {
  color: var(--err);
}
.org-col li.org-pick {
  cursor: pointer;
}
.org-col li.org-pick:hover {
  color: var(--accent);
}
.org-star {
  color: var(--ok);
}
.org-n {
  font-style: normal;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--text-dim);
  width: 16px;
  text-align: right;
}
.org-empty {
  color: var(--text-dim);
  font-style: italic;
}
.org-missing {
  color: var(--err);
  font-style: italic;
}
.org-over {
  background: var(--selection);
}
.org-before {
  border-top-color: var(--accent) !important;
}
.org-after {
  border-bottom-color: var(--accent) !important;
}
/* pane 3's disk mode (songs phase 2): the all songs / disk tabs and the path bar */
.org-tab {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  opacity: 0.55;
  cursor: pointer;
}
.org-tab.on {
  opacity: 1;
}
#orgSort.hidden {
  display: none;
}
/* 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. */
#orgSongAdd.hidden {
  display: none;
}
/* Outranks the always-on `.org-col li button.org-star` above, which is what makes the star the
   one row button that does not wait for a hover. */
.org-col li button.org-star.hidden {
  display: none;
}
/* The ← add buttons sit in a flex row that can get narrow - without this the arrow and the word
   land on separate lines. */
#orgSongAdd,
#orgDiskAdd {
  flex: none;
  white-space: nowrap;
}
#orgDiskBar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px 6px;
}
#orgDiskBar.hidden {
  display: none;
}
#orgBrowsePath {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  direction: rtl; /* a long path keeps its tail (the folder you are in) visible */
  text-align: left;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-dim);
}
.org-browse-dir {
  cursor: pointer;
}
#orgAll li.org-added > span::after {
  content: ' ✓';
  color: var(--ok);
}
/* The disk tab picks like the pack browser: focus ring off (the selection IS the focus),
   selected rows in the accent wash, the playing row's name in the accent. */
#orgAll:focus,
#orgItems:focus {
  outline: none;
}
.org-col li.selected {
  background: var(--accent-soft);
  color: var(--accent);
}

/* THE selected row, wherever a browser has one - the preset picker's highlighted row, organize's
   selection, 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. */
.def-pick-row.on,
.org-col li.selected,
.org-col li.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);
}
/* The row being auditioned, in either list. In the contents pane the first child is the slot
   number, so the marker goes on the name - the child that carries the title in both. */
#orgAll li.playing > span:first-child,
#orgItems li.playing > span {
  color: var(--accent);
}
#orgAll li.playing > span:first-child::before,
#orgItems li.playing > span::before {
  content: '▶ ';
  font-size: 9px;
}

.org-tags {
  display: flex;
  gap: 3px;
  flex: none !important;
  overflow: hidden;
}
.org-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;
}
#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 menu a figure took over (the grain window's), sized like the file field beside the others,
   with its draw button in the same row. */
.device-widget-bare select {
  flex: 1;
  min-width: 0;
  width: auto;
  font-size: 12px;
  padding: 4px 22px 4px 9px;
  background-position: right 8px center;
}

.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 {
  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);
}
