/*
 * The DNX visual language, shared by every page.
 *
 * Extracted from the manager, which is where it was designed — after Overbridge's layer panel, and
 * refined against hardware use. It lived inline in `manager.html`, so the expander grew its own
 * lighter, rounder look and the two tools stopped feeling like one application.
 *
 * **Reuse this rather than styling a new page.** A tool that invents its own slot cell invents its
 * own answer to "is this occupied", and the grid is the one control every tool here shares.
 *
 * Only genuinely page-specific rules belong in a page's own <style> — a layout unique to that page,
 * never a colour, a control or a cell.
 */

/*
 * Visual language borrowed from Elektron's Overbridge editor, which the user already reads
 * fluently: near-black ground, panels a shade lighter with a hairline border, section titles
 * small and letter-spaced, and colour used to say *what kind of thing this is* rather than for
 * decoration. Teal is the sequencer, amber the kit, salmon destructive.
 *
 * Deliberately not copied: knobs and bracketed value fields. Those exist to edit continuous
 * synth parameters. A librarian moves discrete things between slots, and dressing that as a
 * synth would be costume rather than design.
 */
:root {
  /*
   * Tells the browser this page is dark, so **the parts it paints itself** come out dark too: the
   * dropdown list a `<select>` opens, scrollbars, the text caret, focus rings. Without it those
   * are rendered light over a dark page, which is how the +Drive picker ended up light grey on
   * white — a fault no amount of styling the closed control would have fixed, because the open
   * list is not ours to style.
   *
   * `dark` rather than `light dark`: every colour below is a fixed hex and this page does not
   * follow the system, so claiming otherwise would be a lie the UA acts on.
   */
  color-scheme: dark;
  --bg: #16191b;
  --panel: #1e2325;
  --panel-2: #252b2e;
  --line: #313a3d;
  --line-soft: #272f31;
  --fg: #dfe6e7;
  --muted: #78878b;
  --dim: #556366;
  --teal: #79b8ac;
  --teal-dim: #2f4a47;
  --amber: #c8a06a;
  --salmon: #cf6f62;
  --steel: #6f95b5;
  --focus: #8fd4c6;
}
* { box-sizing: border-box; }
/*
 * Two page shapes, and a page says which it is.
 *
 * `body.app` fills the viewport and never scrolls as a whole — the manager, where the grid and the
 * side panel scroll independently and the top bar stays put. `body.page` flows top to bottom like a
 * document and scrolls normally.
 *
 * **This distinction was missing and it cost a whole layout.** The expander was given the app
 * shell's `main` — a two-column grid sized to the viewport — while its own markup used `<header>`
 * and `<aside>`, which this file has never styled. It had no layout to lose, so it came apart at
 * the first resize. Anything reused has to say what it assumes.
 */
body {
  margin: 0; background: var(--bg); color: var(--fg);
  font: 13px/1.45 ui-sans-serif, system-ui, "Segoe UI", sans-serif;
}
html:has(body.app), body.app { height: 100%; }
body.app { display: flex; flex-direction: column; }
body.page { padding-bottom: 4rem; }
button, input, select { font: inherit; color: inherit; }

/*
 * A control that inherits its colour must be given a background too.
 *
 * The rule above sets `color: inherit` — light grey, from the dark page — and leaves the
 * background at the UA default, which is white. That is unreadable, and it was invisible for as
 * long as every `<select>` on the page happened to sit inside something that painted behind it.
 * Adding the +Drive picker to the top bar exposed it, and `#scope` had the same fault all along.
 *
 * Inheriting half a colour pair is the bug. Both halves are set here, on the element, so the next
 * control added anywhere on the page is legible without anyone remembering this.
 */
select {
  background: #1b2123;
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: .22rem .4rem;
  max-width: 18rem;
}
select:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
select:disabled { opacity: .45; }
/*
 * The dropdown list itself is painted by the OS, not by the page, and on Windows it takes the
 * UA's white unless the options say otherwise. Styling only the closed control leaves the open
 * list unreadable — which is the half the user is actually reading when it matters.
 */
select option { background: #1b2123; color: var(--fg); }

/*
 * `hidden` works by a UA rule of `display: none`, which *any* author rule setting `display`
 * beats. Half this file sets `display` on a class that is also used with `hidden` — .tabs,
 * .grid, .legend — so those elements rendered whether hidden or not: bank tabs above an empty
 * page, and the pattern grid still on screen underneath the track grid. Restoring the UA
 * behaviour once is the fix; `!important` is what that takes, and it is what normalize.css
 * does for the same reason.
 */
[hidden] { display: none !important; }

/* ---- top bar ------------------------------------------------------------------------ */
/*
 * **The whole bar lives in `toolnav.css`** — `.topbar`, `.spacer`, the brand, the tool row and the
 * page slide. Not here, because the probe does not link this file: it is a self-contained page with
 * its own `<style>`, and every page links that one.
 *
 * `.topbar` used to be the exception, which is how the bar came to look different on the probe and
 * to sit in a different place on the manager. Only what a *page* puts inside the bar stays here.
 */
.device {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .09em;
  border: 1px solid var(--teal-dim); color: var(--teal);
  padding: .12rem .45rem; border-radius: 3px;
}
.projname { color: var(--fg); font-weight: 600; }

/* ---- generic panel ------------------------------------------------------------------ */
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 4px; }
.panel > h2 {
  margin: 0; padding: .45rem .7rem; font-size: .67rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .11em; color: var(--dim);
  border-bottom: 1px solid var(--line-soft);
  display: flex; align-items: center; gap: .5rem;
}
.panel > h2 .btn.ghost {
  margin-left: auto; background: none; border-color: var(--line-soft);
  font-size: .64rem; letter-spacing: .07em; text-transform: uppercase;
  padding: .18rem .5rem; color: var(--muted);
}
.panel > .body { padding: .7rem; }

/* ---- tab strip, the Overbridge TRK bar ---------------------------------------------- */
.tabs { display: flex; gap: 2px; padding: .5rem .9rem 0; background: #1a1f21; }
.tab {
  background: #1e2426; border: 1px solid var(--line); border-bottom: none;
  border-radius: 3px 3px 0 0; padding: .35rem .95rem; cursor: pointer;
  color: var(--muted); font-size: .78rem; letter-spacing: .04em;
}
.tab:hover { color: var(--fg); }
.tab[aria-selected="true"] { background: var(--panel-2); color: #fff; border-color: var(--line); }
.tab .n { color: var(--dim); font-size: .68rem; margin-left: .4rem; }
.tab[aria-selected="true"] .n { color: var(--teal); }
/* Listed and empty, which is not the same as not listed yet. An empty bank carries no number,
   because a row of zeroes is noise, so the dimming is all that separates it from a bank nobody has
   asked about. The tooltip says which one it is. */
.tab.empty { opacity: .55; }

/* ---- layout ------------------------------------------------------------------------- */
/*
 * A full-height tool's main region: fills the window, scrolls inside itself, one column.
 *
 * **The two-column workspace is opted into, not assumed.** This rule used to carry
 * `grid-template-columns: minmax(0,1fr) 20rem`, which is the *manager's* layout — content beside a
 * side panel — and every `body.app` got it. When the probe became a `body.app` it inherited a grid
 * it has no side panel for: its three children landed in cells, row stretching blew a paragraph up
 * to 3,108px, and the results were pushed 3,400px down a 876px screen. It looked fine until
 * something arrived in the results, which is why a first glance missed it.
 *
 * A page that wants the workspace asks for it by name.
 */
body.app > main { flex: 1; display: flex; flex-direction: column; gap: .7rem;
  padding: .7rem .9rem 0; min-height: 0; }

/** The manager's workspace: the grid beside a fixed side panel. */
/*
 * The manager's two columns, with the song card spanning both above them.
 *
 * **Row 1 is stated because a grid item stretches to its row by default**: the song card folded to a
 * 32px heading was still 196px tall, because the row had taken a share of the window and the panel
 * grew to fill it. `min-content` sizes it to the card, folded or not.
 *
 * **Row 2 is `auto`, and that is not a detail.** It was `1fr` for one commit, which pinned the
 * second row to whatever height was left over — so the side column was clipped with 99px of the
 * history panel below the fold, the page grew a scrollbar of its own, and the status bar moved. Two
 * scroll contexts fighting, reported as "the bottom bar relocates, and goes back when the song bar
 * collapses".
 *
 * `auto` is what this grid had before the song card existed. Only row 1 ever needed stating.
 */
body.app > main.split {
  display: grid;
  grid-template-columns: minmax(0,1fr) 20rem;
  /*
   * **Row 2 takes the slack, and the song card never stretches.**
   *
   * The side column spans both rows, so its height forces the row tracks to add up to at least its
   * own — and `min-content` on row 1 does not stop that, because a spanning item grows the tracks it
   * spans. The song card then stretched into a row far taller than its contents: 446px of card for
   * 196px of content, and 616px once the side column got taller still.
   *
   * That is what "it adds rows as the card grows but no actual rows are visible" was. **No rows were
   * being added.** Every click added a history entry, the side column grew, row 1 grew with it, and
   * the card stretched. The row count in the heading said 1 the whole time.
   *
   * `1fr` on row 2 sends the excess there instead. It was `1fr` once before and clipped the side
   * column (#215) — but that was when `.side` sat *inside* row 2 and got squeezed by it. It spans
   * both rows now and carries its own `max-height`, which is what makes the same keyword safe here.
   * Verified in both states, including a side column twice the window's height.
   */
  grid-template-rows: min-content 1fr;
}
/* Belt as well as braces: never stretch, whatever the row ends up being. */
body.app > main.split > .panel.songs { align-self: start; }
/*
 * A flowing page keeps one column and a comfortable measure at any width.
 *
 * **The measure is a variable because not every `body.page` is prose.** 78rem is the right cap for
 * a document you read top to bottom — the expander — and the wrong one for the library, which is
 * two panes of data side by side and was throwing away 460px of a 1707px window. A page overrides
 * `--page-max` in its own `<style>`; that is exactly the "layout unique to that page" this file's
 * header allows, where a colour or a control would not be.
 *
 * The status line below reads the same variable. It used to repeat the number, so widening one
 * would have left the other indented to a column that no longer existed.
 */
body.page { --page-max: 78rem; }
/*
 * **`wide` is the third page shape, and it is a modifier rather than a new one.**
 *
 * A `body.page` that is two panes of data rather than a column of prose. The library was throwing
 * away 460px of a 1707px window while its tag column wrapped to two lines on most rows — 78rem is
 * a measure chosen for reading, and nothing on that page is read that way.
 *
 * Here rather than in the page's own `<style>` because `body.page` is this file's selector, and a
 * page redefining it is precisely the drift `test/web.test.ts`'s overlap budget exists to catch. It
 * caught this on the first attempt. A page opts in with a class; it does not restate the rule.
 *
 * Still capped. Two 900px panes are a workspace; two 1400px panes are a spreadsheet nobody can
 * scan across.
 */
body.page.wide { --page-max: 116rem; }
body.page > main { display: block; padding: .9rem; max-width: var(--page-max); margin: 0 auto; }

/*
 * The full-bleed bars align their contents with that column, not with the window edge.
 *
 * They span the width — a bar that stopped at 78rem would look like a floating card — but their
 * text starts where the sections start. Without this the status line sat at the far left of a wide
 * window, under content that begins several centimetres to its right, and read as something left
 * over from another layout rather than as this page's own status.
 */
/*
 * **The status line only.** `.topbar` was indented by this same rule and must not be: it is chrome,
 * it spans the app, and indenting it moved the brand and the tool row when the page underneath
 * changed shape. See `toolnav.css`.
 */
body.page > .status { padding-left: calc((100% - min(var(--page-max), 100%)) / 2 + .9rem); padding-right: calc((100% - min(var(--page-max), 100%)) / 2 + .9rem); }
.side { display: flex; flex-direction: column; gap: .7rem; min-height: 0; overflow: auto; }

/* ---- the slot grid, after Overbridge's T1..T16 layer panel --------------------------- */
/*
 * **`minmax(0, 1fr)`, not `1fr`.** `1fr` is shorthand for `minmax(auto, 1fr)`, so a track never
 * shrinks below its content's minimum — the columns stop being equal and the row simply gets wider
 * than the box, painting over whatever is beside it.
 *
 * Measured on the library, whose two panes are side by side: eight `1fr` tracks computed to
 * 111.8 + 85.7 + 98.8 + 118.3 + 118.3 + 105.3 + 105.3 + 105.3 = **849px inside a 602px container**,
 * and the last cell of every row ended 281px into the pane next door. A preset called
 * `AUTOSHAKER%16` is all it takes.
 *
 * Nothing clipped, because grid overflow is visible by default — so it did not look like a layout
 * bug, it looked like two panels drawn on top of each other.
 */
.grid { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 5px; }
.slot {
  /* A grid item defaults to `min-width: auto` for the same reason, so the cell has to allow itself
     to be narrower than its longest word before the track above can honour its share. */
  min-width: 0;
  position: relative; text-align: left; cursor: pointer; min-height: 4.1rem;
  background: #1b2123; border: 1px solid var(--line-soft); border-radius: 3px;
  padding: .32rem .4rem; display: flex; flex-direction: column; gap: .1rem;
  transition: border-color .08s, background .08s;
}
.slot:hover { border-color: var(--dim); }
/*
 * The cell is the target; its labels are decoration. Without this, moving the cursor from the
 * cell onto one of its own spans fires `dragleave` on the cell and then `dragenter` again, so
 * the drop decoration flickers off and on while the pointer is standing still inside it. The
 * outline was subtle enough to get away with it; a word across the middle is not.
 */
.slot > span { pointer-events: none; }
.slot .id { font-size: .66rem; letter-spacing: .06em; color: var(--dim); font-weight: 600; }
.slot .nm {
  font-size: .74rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-family: ui-monospace, Consolas, monospace;
}
.slot .tc { margin-top: auto; font-size: .64rem; color: var(--dim); }
.slot.occupied { background: #1d2726; border-color: var(--teal-dim); }
.slot.occupied .id { color: var(--teal); }
.slot.occupied .nm { color: var(--fg); }
.slot.occupied .tc { color: var(--teal); opacity: .8; }
.slot.unsupported { border-style: dashed; border-color: var(--amber); }
.slot[aria-selected="true"] { outline: 2px solid var(--focus); outline-offset: -2px; background: #24322f; }
/*
 * A preset that is in the pool and locked by nothing.
 *
 * Not an error and not empty — it holds real work, it is simply costing one of 128 places for no
 * reason. Dimmed rather than coloured, because the finding is an absence.
 */
.slot.unused .nm { color: var(--muted); }
.slot.unused .tc { color: var(--dim); font-style: italic; }

/*
 * ---- the track grid ------------------------------------------------------------------
 *
 * **No column override.** Tracks inherit the pattern grid's eight across, so 16 tracks land as
 * two rows of eight — the same shape as a bank of 16 patterns, and the same shape as the
 * device's own two rows of trig keys.
 *
 * It used to be four across, on the reasoning that names needed the room. That was the wrong
 * trade: the user reported it as harder to drag around, and dragging is what this grid is for.
 * Two grids with the same geometry also means a drag looks the same wherever it happens, which
 * matters more than a few characters of preset name.
 *
 * Inheriting rather than restating `repeat(8, 1fr)` is deliberate — two numbers that must agree
 * are two numbers that can disagree.
 */
.slot .mc { font-size: .62rem; color: var(--dim); letter-spacing: .04em; }
.slot.midi { border-color: var(--amber); }
.slot.midi .id, .slot.midi .mc { color: var(--amber); }
/* The pattern whose tracks are shown below, so the two sections are visibly connected. */
.slot.opened { box-shadow: inset 0 -3px 0 var(--teal); }
/*
 * Where a pending drop will land.
 *
 * Deliberately not the selection ring and not the drag-hover wash: those mean 'you picked this'
 * and 'you are over this right now'. This is the third thing — 'this is what Apply will write to'
 * — and it has to survive after the cursor has gone, which is exactly when the other two vanish.
 */
.slot.landing {
  border-color: var(--amber);
  box-shadow: inset 0 0 0 1px var(--amber);
}

/*
 * A slot showing what is *about* to be in it.
 *
 * Dimmed on purpose: the dimming is the tense. This is the future, and Apply is what makes it the
 * present — at which point the cell renders normally, because the merge has really happened. A
 * preview that looked identical to a result would be worse than none, since it would claim
 * something had been written when nothing had.
 */
.slot.pending .nm { color: var(--muted); font-style: italic; }
.slot.pending .tc { color: var(--amber); }
.slot.pending .id { color: var(--amber); }

/*
 * A stacked sub-section: patterns, then that pattern's tracks, then its trigs later. The rule
 * above it and the generous gap are the separation — without them the second grid reads as
 * more of the first, which is exactly how it looked when the two overlapped by accident.
 */
.sub { margin-top: 1.4rem; padding-top: .9rem; border-top: 1px solid var(--line); }
.sub h3 {
  margin: 0 0 .6rem; font-size: .67rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: .11em; color: var(--teal); display: flex; align-items: center; gap: .5rem;
}
.sub h3 .btn.ghost {
  margin-left: auto; background: none; border-color: var(--line-soft); color: var(--muted);
  font-size: .64rem; padding: .1rem .45rem; line-height: 1.2;
}

/*
 * ---- drag and drop -------------------------------------------------------------------
 *
 * The destination says which of the three actions is about to happen, in two ways at once: a
 * hue, and the word itself across the middle of the cell. One amber outline for all three was
 * the first cut, and it told the user only *that* something would happen — the status bar had
 * the *what*, at the other end of the page from where they were looking.
 *
 * Three hues far enough apart to read at a glance and without relying on colour alone, since
 * the word is there too: blue moves, green copies, amber exchanges.
 *
 * Deliberately not a variation on the selection outline. Selection is a thin 2px ring; a drop
 * target is a thick ring, a saturated wash and a label — nobody has to work out which is which.
 * And there is no fourth state for a refusal: the browser's own "no" cursor already says it.
 */
.slot.dragging { opacity: .4; }

.slot.target {
  outline: 3px solid var(--drop); outline-offset: -3px;
  background: var(--drop-bg);
  transition: none;   /* the modifier can change mid-drag; a fade would lag behind the truth */
}
.slot.target.move { --drop: var(--steel);  --drop-bg: #1e2a33; }
.slot.target.copy { --drop: var(--teal);   --drop-bg: #1c2e2b; }
.slot.target.swap { --drop: var(--amber);  --drop-bg: #302719; }

/*
 * The label is drawn from the attribute rather than injected as a child: a re-render rebuilds
 * every cell, and a stray <span> would outlive the drag that put it there.
 */
.slot.target::after {
  content: attr(data-action);
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: .78rem; font-weight: 700; letter-spacing: .16em;
  color: var(--drop);
  background: rgba(10, 13, 14, .72);   /* a scrim, so the word is legible over a full cell */
  pointer-events: none;                /* or the cell stops receiving its own dragover */
}
kbd {
  font: inherit; font-size: .92em; background: #2a3133; border: 1px solid var(--line);
  border-radius: 2px; padding: 0 .25rem;
}

/* ---- scope selector ----------------------------------------------------------------- */
.lbl { display: block; font-size: .66rem; letter-spacing: .07em; text-transform: uppercase;
  color: var(--dim); margin-bottom: .3rem; }
#scope { width: 100%; background: var(--panel-2); border: 1px solid var(--line-soft);
  border-radius: 3px; padding: .34rem .4rem; font-size: .76rem; }
#scope:focus { outline: 2px solid var(--focus); outline-offset: -2px; }

/* ---- operation bar ------------------------------------------------------------------ */
.ops { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.btn {
  background: #232a2c; border: 1px solid var(--line); border-radius: 3px;
  padding: .34rem .7rem; cursor: pointer; color: var(--fg); letter-spacing: .02em;
}
.btn:hover:not(:disabled) { border-color: var(--teal); color: #fff; }
.btn:disabled { opacity: .35; cursor: default; }
.btn.danger:hover:not(:disabled) { border-color: var(--salmon); color: var(--salmon); }
.btn.primary { background: var(--teal-dim); border-color: var(--teal); color: #eafffb; }
.hint { color: var(--dim); font-size: .72rem; }

/* ---- history ------------------------------------------------------------------------ */
.hist { list-style: none; margin: 0; padding: 0; font-size: .75rem; }
.hist li { padding: .22rem .1rem; color: var(--muted); border-bottom: 1px solid var(--line-soft); }
.hist li:first-child { color: var(--fg); }
/*
 * A history row is a place you can go, so it has to look like one and answer a keyboard like one.
 */
.hist li[data-step] { cursor: pointer; }
.hist li[data-step]:hover { color: var(--fg); background: var(--panel-2); }
.hist li[data-step]:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
/*
 * The project as it was opened. Italic and quieter than the actions above it, because it is not
 * one of them: every other row is something that happened, and this is the state before anything
 * did. It sits last and carries no size, since nothing was written to get there.
 */
.hist li.origin { font-style: italic; color: var(--dim); border-bottom: none; }
.hist li.origin:hover { color: var(--fg); }
/*
 * Undone steps: still listed, because they are where forward is, but dimmed and marked so the
 * line between past and future is visible without a label saying so.
 */
.hist li.ahead { color: var(--dim); font-style: italic; }
.hist li.ahead::before { content: "↷ "; }
.hist .b { color: var(--dim); float: right; font-size: .68rem; }

/* ---- status ------------------------------------------------------------------------- */
.status {
  padding: .45rem .9rem; border-top: 1px solid var(--line); background: #1a1f21;
  font-size: .76rem; color: var(--muted); min-height: 2rem;
}
/*
 * On a flowing page the status bar is pinned, because otherwise it is not a status bar.
 *
 * In `body.app` it is the last child of a viewport-height column, so it sits at the bottom of the
 * window and is always in view. Reused on `body.page` it became an ordinary `<div>` at the end of a
 * long document: a message written while the user is looking at the middle of the page appeared
 * somewhere below the fold, out of sight and out of context. `body.page` already reserves the room
 * for it (`padding-bottom`), which is what that rule was for.
 */
body.page > .status { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; }
.status.error { color: var(--salmon); }
.status.warn { color: var(--amber); }
.status.ok { color: var(--teal); }
.dropzone { padding: 2.5rem 1rem; text-align: center; color: var(--muted); }
.dropzone strong { color: var(--fg); }
label.file { display: inline-block; cursor: pointer; }
label.file input { display: none; }
.legend { display: flex; gap: .9rem; font-size: .68rem; color: var(--dim); margin-top: .55rem; }
.legend i { display: inline-block; width: .6rem; height: .6rem; border-radius: 2px;
  margin-right: .25rem; vertical-align: -1px; }
/*
 * The chips themselves. `.sw` was in the markup with no rule behind it, so every legend swatch
 * rendered as an empty box — the same silent pairing between markup and stylesheet that the grid
 * class guard in `web.test.ts` now catches. Each one matches the state it stands for.
 */
.legend .sw { border: 1px solid var(--line); background: var(--panel-2); }
.legend .sw.occupied { background: #1d2726; border-color: var(--teal-dim); }
.legend .sw.sel { background: #24322f; border-color: var(--focus); }
.legend .sw.pending { background: transparent; border-color: var(--amber); }

/* ---- a section of a flowing page ------------------------------------------------------ */
.section { margin: 0 0 1.1rem; }
.section > h2 {
  margin: 0 0 .45rem; font-size: .7rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--teal); font-weight: 600;
}
.section > .sub { margin: -.2rem 0 .5rem; color: var(--muted); font-size: .76rem; }

/*
 * A strip of controls that wraps rather than overflowing.
 *
 * Every bar on every page: the input row, the options, the output row. `flex-wrap` is the whole
 * point — a fixed row is what makes a layout fall apart at a width nobody tested.
 */
.bar {
  display: flex; align-items: center; gap: .55rem; flex-wrap: wrap;
  padding: .55rem .7rem; background: var(--panel);
  border: 1px solid var(--line); border-radius: 4px;
}
.bar .sep { width: 1px; align-self: stretch; background: var(--line); margin: 0 .25rem; }

/*
 * A command row directly under the chrome bar, on a full-height app page.
 *
 * `.bar` on its own is a card inside a flowing section, which is right for the expander. Under the
 * chrome it needs to read as another band of the same bar rather than a card that has floated up to
 * the top, so it loses its radius and its side borders and keeps only the rule beneath it.
 */
.bar.toolbar {
  border-radius: 0; border-left: 0; border-right: 0; border-top: 0;
  background: var(--bg); padding-left: .9rem; padding-right: .9rem;
}

/* A file input dressed as a button, since the native control cannot be styled. */
.filebtn { position: relative; overflow: hidden; display: inline-flex; }
.filebtn input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; }

/* ---- the expansion report ------------------------------------------------------------
 *
 * The sounds-on-tracks table and the sounds staying locked. **The most important data any of these
 * tools shows**: it is the answer to "what will this actually do to my project", and the user
 * singled it out as the clearest thing the old UI had.
 *
 * Here rather than on a page because it is data, not decoration — a second tool reporting an
 * expansion should look identical, and the manager will want it the day it can expand.
 */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); gap: .5rem;
  margin: 0 0 .7rem; }
.stats > div { background: var(--panel); border: 1px solid var(--line); border-radius: 4px;
  padding: .5rem .65rem; }
.stats strong { display: block; font-size: 1.35rem; line-height: 1.1; color: var(--fg); }
.stats span { color: var(--muted); font-size: .68rem; text-transform: uppercase; letter-spacing: .07em; }

table { border-collapse: collapse; width: 100%; font-size: .78rem; }
th, td { text-align: left; padding: .3rem .5rem; border-bottom: 1px solid var(--line-soft); }
th { color: var(--muted); font-weight: 600; font-size: .66rem; text-transform: uppercase;
  letter-spacing: .07em; }
td.track { font-weight: 700; color: var(--teal); }
td.num { text-align: right; }
td.from, td.patterns { color: var(--muted); font-size: .74rem; }

h3 { font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted);
  font-weight: 600; margin: 1rem 0 .35rem; }

/* Two columns where there is room: a long list of names reads badly as one tall column. */
ul.overflow { columns: 2; column-gap: 1.4rem; margin: .3rem 0; padding-left: 1.1rem; font-size: .78rem; }
@media (max-width: 46rem) { ul.overflow { columns: 1; } }

.good { color: var(--teal); }
.muted { color: var(--muted); }

/*
 * ---- help ------------------------------------------------------------------------------
 *
 * A page's explanation of itself, on a `?` rather than in the layout. See `web/src/help.ts` for
 * what it cost the probe to keep that explanation on the page: 30% of the results window at
 * 1600x876, and all of it at 900x800.
 *
 * Here rather than in the probe's own `<style>` because it is a component, not a layout. The next
 * page with something to explain should get the same panel, and the alternative is a second copy
 * of these rules written months later against a slightly different set of colours — which is what
 * this whole file exists to stop.
 */
/* The heading becomes a row so the `?` can sit at its end, out of the way of the words. */
[data-help] { display: flex; align-items: center; gap: .5rem; }
.helpq {
  margin-left: auto; flex: none;
  width: 1.15rem; height: 1.15rem; padding: 0; line-height: 1;
  background: none; border: 1px solid var(--line); border-radius: 50%;
  color: var(--muted); font-size: .72rem; font-weight: 600; cursor: pointer;
}
.helpq:hover { border-color: var(--teal); color: var(--teal); }
.helpq:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

/*
 * **`[open]`, not a bare `dialog.help`.** A closed dialog is hidden by a UA rule of
 * `display: none`, which any author rule setting `display` beats — the same fault that left the
 * bank tabs and the pattern grid on screen while marked `hidden`. Without the attribute selector
 * every page's help panel would render inline, permanently, at the bottom of the document.
 */
dialog.help[open] { display: flex; flex-direction: column; }
dialog.help {
  padding: 0;                    /* so a click landing on the dialog itself is really the backdrop */
  border: 1px solid var(--line); border-radius: 4px;
  background: var(--panel); color: var(--fg);
  width: min(46rem, calc(100vw - 2rem));
  max-height: min(38rem, calc(100vh - 4rem));
}
dialog.help::backdrop { background: rgba(10, 13, 14, .66); }
/* `min-height: 0`, or a flex child refuses to shrink below its content and the panel overflows. */
.helpbody { overflow: auto; min-height: 0; padding: 1rem 1.2rem; }
.helpbody > h2 {
  margin: 0 0 .7rem; font-size: .7rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--teal); font-weight: 600;
}
.helpbody [data-topic] {
  padding: .7rem; margin: 0 -.7rem; border-top: 1px solid var(--line-soft); border-radius: 3px;
}
.helpbody [data-topic]:first-of-type { border-top: none; padding-top: 0; }
.helpbody [data-topic] h3 {
  margin: 0 0 .35rem; font-size: .68rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--muted); font-weight: 600;
}
.helpbody [data-topic] p { margin: 0 0 .5rem; color: var(--muted); font-size: .8rem; }
.helpbody [data-topic] p:last-child { margin-bottom: 0; }
.helpbody strong { color: var(--fg); font-weight: 600; }
/*
 * Where the `?` you pressed lands. Marked by lifting it rather than by dimming its neighbours: the
 * sentence somebody needs is often on the card next to the one they asked about, and help you have
 * to squint at is help that did not work.
 */
.helpbody [data-topic][data-current] { background: var(--panel-2); }
.helpbody [data-topic][data-current] h3 { color: var(--teal); }
.helpclose { display: flex; justify-content: flex-end; margin-top: .9rem; }

/* ---- asking something ----------------------------------------------------------------
 *
 * The consent dialogs, replacing twelve `window.confirm` and `prompt` boxes. Same skeleton as
 * `dialog.help` above and deliberately so — one modal shape for the app, differing only in that
 * this one has an answer and help does not.
 *
 * **`[open]`, not a bare `dialog.ask`** — see the note on the help panel. A closed dialog is
 * hidden by a UA `display: none` rule, which any author rule setting `display` beats.
 */
dialog.ask[open] { display: flex; flex-direction: column; }
dialog.ask {
  /* All padding is on `.askbody`, so the scroll container is the full width of the panel. Unlike
     `dialog.help` this is not also load-bearing for hit-testing: a question does not dismiss on a
     backdrop click, because the list inside it is what the answer is decided from. */
  padding: 0;
  border: 1px solid var(--line); border-radius: 4px;
  background: var(--panel); color: var(--fg);
  /* Narrower than help: a question is read once and answered, not browsed. */
  width: min(34rem, calc(100vw - 2rem));
  max-height: min(32rem, calc(100vh - 4rem));
}
dialog.ask::backdrop { background: rgba(10, 13, 14, .66); }
.askbody { overflow: auto; min-height: 0; padding: 1rem 1.2rem; }
.askbody > h2 {
  margin: 0 0 .7rem; font-size: .78rem; letter-spacing: .02em;
  color: var(--fg); font-weight: 600;
}
.askbody > p { margin: 0 0 .6rem; color: var(--muted); font-size: .8rem; line-height: 1.5; }

/*
 * The itemised half — tracks about to be destroyed, findings of a plan. Scrolls on its own so a
 * long list cannot push the buttons off the bottom of the dialog, which is the one thing a modal
 * must never do: an unanswerable question with no visible way out.
 */
.asklist {
  margin: 0 0 .7rem; padding: .5rem .7rem; list-style: none;
  max-height: 14rem; overflow: auto;
  background: var(--panel-2); border: 1px solid var(--line-soft); border-radius: 3px;
  font-size: .75rem; color: var(--muted);
}
.asklist li { padding: .16rem 0; border-bottom: 1px solid var(--line-soft); }
.asklist li:last-child { border-bottom: none; }

.askbuttons { display: flex; justify-content: flex-end; gap: .4rem; margin-top: .9rem; }

/*
 * **The destructive button has to look destructive before it is pointed at.**
 *
 * `.btn.danger` elsewhere only colours on `:hover`, which is fine for a button sitting in a bar
 * you are reading anyway. In a dialog it is not: measured on the manager's own destroy dialog,
 * "Destroy it" and "Cancel" both computed to `rgb(49, 58, 61)` — the same border, the same text,
 * distinguishable only by the words. The one place the app asks somebody to approve losing work
 * is the one place two buttons must not look alike.
 *
 * Scoped to `.askbuttons` deliberately. Restyling every `.btn.danger` in the app is a change to
 * pages this has nothing to do with, and it belongs to whoever is looking at those pages.
 */
.askbuttons .btn.danger { border-color: var(--salmon); color: var(--salmon); }
.askbuttons .btn.danger:hover:not(:disabled) { background: var(--salmon); color: #1a1010; }
.askinput {
  width: 100%; box-sizing: border-box; margin: .2rem 0 0;
  background: var(--panel-2); border: 1px solid var(--line-soft); border-radius: 3px;
  padding: .38rem .5rem; color: var(--fg); font: inherit; letter-spacing: .04em;
}
.askinput:focus { outline: 2px solid var(--focus); outline-offset: -2px; }

/* ---- the +Drive library table --------------------------------------------------------
 *
 * A bank is 256 presets and the question is "where is the kick I made last week". The pool
 * keeps its grid, where the question is "what is in use" — see `librarytable.ts` for why the
 * two are deliberately not one component.
 */
.libfind { display: flex; gap: .4rem; align-items: center; margin: 0 0 .5rem; }
.libfind input[type="search"] {
  flex: 1 1 auto; min-width: 0;
  background: var(--panel-2); border: 1px solid var(--line-soft); border-radius: 3px;
  padding: .34rem .5rem; color: var(--fg); font: inherit;
}
.libfind input[type="search"]:focus { outline: 2px solid var(--focus); outline-offset: -2px; }
.libfind .lbl { display: flex; align-items: center; gap: .3rem; white-space: nowrap; }

.tagchips { display: flex; flex-wrap: wrap; gap: .3rem; margin: 0 0 .55rem; }
.tagchip {
  background: #1b2123; border: 1px solid var(--line-soft); border-radius: 999px;
  padding: .18rem .55rem; cursor: pointer; color: var(--muted);
  font-size: .68rem; letter-spacing: .06em; display: inline-flex; align-items: center; gap: .3rem;
}
.tagchip:hover { border-color: var(--teal); color: var(--fg); }
.tagchip:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.tagchip .n { color: var(--dim); font-size: .64rem; }
/* Pressed is a state the button reports, so the styling hangs off the same attribute a screen
   reader uses rather than a class that could disagree with it. */
.tagchip[aria-pressed="true"] {
  background: var(--teal-dim); border-color: var(--teal); color: #eafffb;
}
.tagchip[aria-pressed="true"] .n { color: #eafffb; opacity: .75; }
/* Unreachable: no row left carries it as well as the tags already chosen. Dimmed and disabled
   rather than removed, because a chip vanishing under the pointer moves the ones beside it, and
   the cloud is also a map of what this bank holds. Disabled for real, so a press that would do
   nothing does not look like a press the page ignored. A selected chip is never in this state:
   dropping it always widens the result. */
.tagchip:disabled { opacity: .35; cursor: not-allowed; }
.tagchip:disabled:hover { border-color: var(--line-soft); color: var(--muted); }

/* The rows scroll, the header does not — a 256-row bank is exactly when you stop knowing which
   column is which. `overflow-x: auto` keeps a narrow pane from pushing the page sideways. */
.libtablewrap { max-height: 30rem; overflow: auto; border: 1px solid var(--line-soft); border-radius: 3px; }
.libtable { width: 100%; border-collapse: collapse; font-size: .75rem; }
.libtable thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--panel-2); border-bottom: 1px solid var(--line);
  padding: .35rem .5rem; text-align: left; font-weight: 600;
  font-size: .66rem; text-transform: uppercase; letter-spacing: .09em; color: var(--muted);
}
.libtable td { padding: .28rem .5rem; border-bottom: 1px solid var(--line-soft); }
.libtable tbody tr.occupied { cursor: grab; }
.libtable tbody tr.occupied:hover { background: #1d2726; }
.libtable tbody tr.selected,
.libtable tbody tr.selected:hover { background: #20312f; box-shadow: inset 3px 0 0 var(--teal); }
.libtable tbody tr.selected td.name { color: var(--text, #e6f2ef); font-weight: 600; }
.libtable tbody tr.free td { color: var(--dim); }
.libtable td.n { color: var(--dim); text-align: right; width: 3rem; font-variant-numeric: tabular-nums; }
.libtable tr.occupied td.name { color: var(--teal); }
.libtable td.machine, .libtable td.state { color: var(--muted); white-space: nowrap; }
.libtable td.tags { line-height: 1.7; }
.libtable .tag {
  background: #232a2c; border: 1px solid var(--line-soft); border-radius: 2px;
  padding: .04rem .28rem; font-size: .64rem; letter-spacing: .05em; color: var(--muted);
}
/* Three states, not two: nobody has read this slot is not the same as this slot has no tags. */
.libtable .unread { color: var(--dim); font-style: italic; }
.libtable .untagged { color: var(--dim); }

/* ---- progress, in the status bar ------------------------------------------------------
 *
 * Inserted into `#status` by `progress.ts` and removed when the work finishes, so no page needed
 * markup for it and a page at rest looks exactly as it did.
 */
/*
 * Its own element at the foot of the window, **not** a child of `#status`.
 *
 * That element's text is written with `textContent`, which replaces every child — a bar appended
 * inside it was deleted by the next message about the operation it was drawing. See `progress.ts`.
 */
.progress {
  position: fixed; right: 1rem; bottom: .62rem; z-index: 30;
  /* Fixed width, so the number beside it cannot shuffle the bar sideways as it changes. */
  width: 12rem;
  height: .5rem; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--line-soft);
  overflow: hidden;
}
.progress > i {
  display: block; height: 100%; width: 0;
  background: var(--teal);
  transition: width .12s linear;
}
/* The percentage sits outside the track, or a bar under 20% has nowhere to put its own label. */
.progress > b {
  position: absolute; right: 12.6rem; bottom: -.3rem;
  font-size: .66rem; font-weight: 600; color: var(--muted); font-variant-numeric: tabular-nums;
}

/*
 * The indeterminate mode: a stripe that travels, promising nothing.
 *
 * Used where the total genuinely is not known — a +Drive read, where the listing reports a flat
 * 4 MiB allocation rather than the file's size. See `progress.ts`.
 */
.progress.working { background: var(--panel-2); }
.progress.working::after {
  content: ""; position: absolute; inset: 0;
  width: 35%;
  background: linear-gradient(90deg, transparent, var(--teal-dim), var(--teal), var(--teal-dim), transparent);
  animation: progress-sweep 1.1s ease-in-out infinite;
}
@keyframes progress-sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(340%); }
}
/* Somebody who asked for less motion gets a still bar rather than none — it still says "working". */
@media (prefers-reduced-motion: reduce) {
  .progress.working::after { animation: none; opacity: .5; width: 100%; }
}

/* ---- songs ---------------------------------------------------------------------------
 *
 * The song panel sits in the main column below the pattern grid, because `main.split` places a
 * third child at column 1, row 2. Stated rather than left to flow, so a fourth panel added later
 * cannot silently push this one into the side column.
 *
 * It reuses `.libtable` from the +Drive library: same question shape — a list of rows you scan for
 * one you want — so it gets the same answer rather than a second table style.
 */
/*
 * First, and **the same width as the patterns below it**.
 *
 * It spanned both columns for a while. Aligning it to the main column instead lines the song table
 * up with the pattern grid it refers to — a row saying `A5` sits directly above the cell called A5 —
 * and it lets the side column own its full height rather than starting halfway down the window.
 */
.panel.songs { grid-column: 1; }
/*
 * The side column spans both rows and **stays put while the main column scrolls**.
 *
 * `align-self: start` stops it stretching to the height of two rows of content, and `sticky` keeps
 * Operations, Selection and History reachable when the song table and the pattern grid together run
 * past the bottom of the window. Without it, the controls for a selection scroll away from the
 * selection they act on.
 */
body.app > main.split > .side {
  grid-column: 2;
  grid-row: 1 / -1;
  align-self: start;
  position: sticky;
  top: 0;
  max-height: 100vh;
}
/*
 * The heading *is* the button. Transparent and inheriting the heading's type, so it reads as a
 * heading and behaves as a disclosure — the first attempt was a 10px chevron in muted grey at the
 * far end of the strip, which is not a control anybody would find.
 */
.songfold {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  width: 100%; background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; color: inherit; letter-spacing: inherit; text-align: left;
}
.songfold:hover { color: var(--fg); }
.songfold .caret { color: var(--teal); font-size: .9rem; }
/*
 * Shorter than the library's 30rem. This panel is *above* the pattern grid, so every pixel it takes
 * pushes the grid down — at 30rem the patterns were off the bottom of a 1512px-tall window, which
 * defeats having both on screen at once. It scrolls inside itself and folds away, so it does not
 * need to be tall to be useful.
 */
.songs .libtablewrap { max-height: 18rem; }
/* Collapsed is a strip, not a hidden panel: the heading still says which song and how many rows. */
.panel.songs.folded > .body { display: none; }
.songtabs { padding: 0 0 .5rem; background: transparent; flex-wrap: wrap; }
/*
 * Numbers right, and their headings with them. A right-aligned column under a left-aligned heading
 * reads as two columns, which is what the first draft looked like.
 */
.songtable td.num, .songtable th:nth-child(n+4):nth-child(-n+7) { text-align: right; }
.songtable td.num { font-variant-numeric: tabular-nums; }
/*
 * The slack goes to **mutes**, which is sixteen chips and the only column that can use it.
 *
 * It went to `:last-child` while the table had eight columns and mutes was last. Adding the actions
 * column moved the target: 34% of the table went to two small buttons, and the label picker was
 * squeezed until `BRIDGE` read as `BRIDGI`. A width tied to a position breaks the moment a column
 * is added, so this names the column instead.
 */
/*
 * Sized to what each column holds. The numeric ones were taking a share of the table proportional to
 * nothing — `2` and `60` sat in columns wide enough for a name — while the pattern column had a slot
 * label and now has a name beside it too.
 */
.songtable th:nth-child(1), .songtable td:nth-child(1) { width: 3rem; }   /* Row */
.songtable th:nth-child(2), .songtable td:nth-child(2) { width: 12rem; }  /* Pattern + name */
.songtable th:nth-child(3), .songtable td:nth-child(3) { width: 9rem; }   /* Label */
.songtable th:nth-child(4), .songtable td:nth-child(4) { width: 4.5rem; } /* Plays */
.songtable th:nth-child(5), .songtable td:nth-child(5) { width: 5rem; }   /* Length */
.songtable th:nth-child(6), .songtable td:nth-child(6) { width: 5.5rem; } /* Tempo */
.songtable th:nth-child(7), .songtable td:nth-child(7) { width: 5rem; }   /* Swing */
/* Mutes has no width: every other column is sized to its content, so the slack lands here — which
 * is the only column that can use it, being sixteen chips. */
.songtable th:last-child, .songtable td:last-child { width: 4rem; }
/* Wide enough for the longest label the device offers, so none is shown cut off. */
.songtable select.rowedit { min-width: 6.5rem; }
.songtable td.dim { color: var(--dim); }
.songtable tbody tr:hover { background: #1d2726; }

/* ---- song row editing ----------------------------------------------------------------
 *
 * The controls sit inside the table rather than in a separate form, because a song row is a row of
 * a table and an editor beside it would mean holding "which row am I changing" in your head.
 */
.songtable input.rowedit, .songtable select.rowedit {
  background: #171c1e; color: var(--fg); border: 1px solid var(--line-soft);
  border-radius: 2px; padding: .1rem .25rem; font: inherit; font-size: .75rem; width: 100%;
}
.songtable input.rowedit { text-align: right; max-width: 5.5rem; }
.songtable select.rowedit { max-width: 8rem; }
.songtable input.rowedit:focus, .songtable select.rowedit:focus { border-color: var(--teal); outline: none; }
/* The slot stays scannable down the column; the name reads as prose beside it. */
.songtable td.ptn { white-space: nowrap; }
.songtable .ptnid { font-weight: 600; color: var(--teal); }
.songtable .ptnname { color: var(--muted); margin-left: .45rem; font-size: .72rem; }
/* Sixteen chips, so muting is a click on a numbered track exactly as it is on the instrument. */
.songtable td.mutes { white-space: nowrap; line-height: 1; }
.mutechip {
  background: #171c1e; color: var(--dim); border: 1px solid var(--line-soft);
  border-radius: 2px; width: 1.35rem; padding: .1rem 0; margin-right: 1px;
  font-size: .62rem; cursor: pointer;
}
.mutechip:hover { color: var(--fg); border-color: var(--line); }
.mutechip.on { background: var(--amber); color: #14181a; border-color: var(--amber); font-weight: 700; }
.songtable td.rowacts { white-space: nowrap; text-align: right; }
.btn.tiny { padding: .05rem .3rem; font-size: .7rem; min-width: 0; }
/* A row being dragged onto, using the same marks the pattern grid uses. */
.songtable tbody tr.target { outline: 1px solid var(--teal); outline-offset: -1px; }
.songtable tbody tr.dragging { opacity: .5; }

/* The song-level controls: rename, end mode, tempo, clear. */
.songcontrols { display: flex; align-items: center; gap: .4rem; padding: 0 0 .55rem; flex-wrap: wrap; }
.songcontrols .songtempo {
  display: flex; align-items: center; gap: .35rem;
  font-size: .7rem; letter-spacing: .04em; color: var(--muted); text-transform: uppercase;
}
.songcontrols .songtempo input { max-width: 5rem; }

/* ---- insights mode --------------------------------------------------------------------
 *
 * The manager, reading rather than editing. The bank tabs and the pattern grid stay — they are the
 * selector, and clicking between patterns to compare them on the same chart is the whole point —
 * and everything that edits goes away.
 *
 * **The side column goes, so the column itself goes.** Hiding `.side` while leaving a 20rem track
 * in the grid would leave the charts drawing into two thirds of the window with an empty third
 * beside them. One column, full width.
 */
/*
 * **The page has to be allowed to grow, and by default it is not.**
 *
 * `body.app` is `height: 100%` and `body.app > main` is `flex: 1; min-height: 0` — a workspace
 * sized to the window, which is right for a grid and a side column that each scroll inside
 * themselves. The analysis is a *document*: several thousand pixels of cards that the page is
 * meant to scroll. Left capped, `main` stayed one viewport tall and its content simply overflowed,
 * so the status bar — an ordinary last child, not a fixed one on this page — was laid out where
 * `main`'s box ended and ended up sitting **in the middle of the charts**.
 *
 * `min-height: auto` is the flex default that `min-height: 0` was overriding: it stops the item
 * shrinking below its content, so `main` grows, the body grows with it, and the status bar goes
 * back to the bottom. The same family as #215 — a row sized to the window rather than to what is
 * in it, discovered by looking at the page rather than by anything failing.
 */
body.app.insights { height: auto; }
body.app.insights > main { min-height: auto; }
body.app.insights > main.split { grid-template-columns: minmax(0, 1fr); }
body.app.insights > main.split > .side { display: none; }
/*
 * The drill-down is hidden too. It answers "what is on this pattern's tracks", which is the
 * question the charts below answer in more detail — two answers to one question, one of them
 * worse, competing for the same screen.
 */
body.app.insights #trackSection { display: none; }
/* Operations are unreachable in this mode; the grid declines drops, and its cursor should agree. */
body.app.insights .slot { cursor: pointer; }

#insightsPanel { grid-column: 1; display: flex; flex-direction: column; gap: .7rem; }
/*
 * A pressed toggle has to look pressed. The tool row uses the accent for "you are here"; this is
 * the same idea one level down, so it borrows the same colour rather than inventing a second
 * vocabulary for the same meaning.
 */
.bar .btn[aria-pressed="true"] {
  border-color: var(--teal-dim); color: var(--teal); background: #1d2726;
}
