/* Black ground, silver controls, no ornament.
 *
 * Three targets share this sheet and each pulls it a different way: a plugin
 * window a host may size to anything, a resizable desktop window, and a phone held
 * in one hand. So there is no fixed pixel layout anywhere in here -- panels flow
 * into however many columns fit, and the one hard rule is that a control never
 * gets smaller than a finger.
 *
 * The palette is deliberately tiny. Everything is a grey; the only colour in the
 * interface is the faint blue cast on an active control, which is what makes it
 * readable at a glance against a panel of forty knobs.
 */

:root {
  /* Stated in the sheet as well as in the meta tag in index.html, because this
   * is what decides the colour of the parts of the window the sheet cannot
   * reach -- the scrollbars above all. A pale scrollbar down the edge of a
   * black panel does not read as a scrollbar; it reads as a border. */
  color-scheme: dark;

  --bg: #08080a;
  --panel: #0e0e11;
  --panel-edge: #1c1c21;
  /* Three tiers of text, measured against --panel rather than chosen by eye:
   * 15.8:1, 7.4:1 and 4.7:1. The faint one used to be #55555e, which is
   * 2.6:1 -- under half of what WCAG asks of normal text, and it read as
   * unreadable on a phone in daylight rather than as quiet. The dim one was
   * passing at 5.6:1 but sits under small labels, so it was lifted with it to
   * keep the three tiers evenly spaced. */
  --ink: #e8e8ec;
  --ink-dim: #a0a0aa;
  --ink-faint: #7d7d87;

  /* The silver a knob is made of: a lit top edge falling to a dark underside. */
  --metal-lit: #dcdce2;
  --metal: #9a9aa4;
  --metal-dark: #4a4a52;
  --track: #24242a;
  --accent: #cfd6e6;

  --gap: 14px;
  --radius: 10px;
  --type-lead: 18px;
  --type-title: 15px;
  --type-read: 13px;
  --type-body: 12px;
  --type-micro: 11px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  /* The platform UI face. Nothing here is downloaded: this UI ships no webfont
   * and has no network, so naming a hosted family would only have described a
   * font that never arrives. `system-ui` resolves to Segoe UI, San Francisco,
   * or the desktop's own UI face, which is what a plugin editor should wear
   * inside its host anyway. */
  font-family: system-ui, "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
  /* A knob drag must not scroll the page underneath it on a touch screen. */
  overscroll-behavior: none;
}

html {
  /* No scroll snapping. It was tried and taken out: sections vary from four
   * controls to seventeen, so a snap point is as often in the middle of what is
   * being read as at the top of it, and the strip navigates well enough without
   * the scroll being taken over. */
  scroll-behavior: smooth;
}

/* Anyone who has asked for less motion gets the jump without the travel. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body { padding: var(--gap); }

/* -------------------------------------------------------------- navigation */

nav {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: stretch;
  gap: 12px;
  margin: 0 calc(var(--gap) * -1) var(--gap);
  padding: 0 var(--gap);
  background: rgba(8, 8, 10, 0.92);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--panel-edge);
}

/* The whole of the branding, and the whole of the chrome with it: the name and
 * nothing else.
 *
 * It used to carry the loaded patch and the engine's sample rate too. The patch
 * is already in the bank bar at the foot, and a sample rate is a developer's
 * diagnostic that had taken up residence in the one piece of chrome on screen at
 * all times. Losing both also loses a strip whose width changed as patches were
 * stepped through. */
.brand {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  padding-right: 4px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
}

.nav-inner {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  gap: 2px;
  overflow-x: auto;
  scrollbar-width: none;
  /* The strip scrolls sideways on a narrow window; it must not drag the page
   * with it, and it must not show a scrollbar over a 34px tall bar. */
  overscroll-behavior-x: contain;
}

.nav-inner::-webkit-scrollbar { display: none; }

.nav-inner button {
  flex: 0 0 auto;
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--ink-faint);
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 11px 10px 9px;
  cursor: pointer;
  white-space: nowrap;
  transition: color 120ms ease, border-color 120ms ease;
}

.nav-inner button:hover { color: var(--ink-dim); }

.nav-inner button[aria-current="true"] {
  color: var(--ink);
  border-bottom-color: var(--metal);
}

/* ------------------------------------------------------------------ panels */

/* One section per row. A wide window then reads as a stack of labelled rows of
 * controls rather than as columns the eye has to track up and down, and it is
 * what makes snapping to a section mean anything. */
main {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

.panel {
  background: var(--panel);
  border: 1px solid var(--panel-edge);
  border-radius: var(--radius);
  padding: 14px 14px 16px;
}

.panel > h2 {
  margin: 0 0 14px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* ------------------------------------------------------------------ groups */

/* Controls that act on the same thing, boxed together and separated by a rule.
 * Without this a section is one undifferentiated run of knobs and the reader has
 * to know already that three of the seventeen belong to the sub oscillator. */
.groups {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 18px 0;
}

/* Every group is ruled off, the first one included. Leaving the first without a
 * rule made the set look unfinished on the left rather than deliberately open. */
.group {
  padding: 0 18px;
  border-left: 1px solid var(--panel-edge);
}

.group-label {
  font-size: 9px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 12px;
}

/* Packed left at a fixed width rather than stretched to fill.
 *
 * A row is as wide as the window, and a section with four controls in it should
 * not spread those four across fourteen columns' worth of space just because a
 * section with seventeen would fill them. */
.controls {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 20px 14px;
}

.controls > .control { width: 92px; }
.controls > .control.wide { width: 120px; }

/* A dropdown needs room its label does not. The option names are spelled out --
 * "Low Pass 24 dB/oct", "Both Oscillator Pitches" -- and a knob's width truncates
 * them to the point of being unreadable, which defeats having written them out.
 * Sized to the longest of them, which is the LFO destination's. */
.controls > .control.has-options { width: 152px; }

/* A list and a stepper are sized by what is in them: the filter's five responses
 * need room for "Low Pass 24 dB/oct" and the arpeggiator's four need room for
 * "1 Octave", and holding both to one width would waste the difference on every
 * short one. */
.controls > .control.has-radio,
.controls > .control.has-stepper { width: auto; min-width: 88px; }

/* Their options are left-aligned, so the label above them should be too --
 * centring a heading over a left-hand column reads as a mistake. */
.controls > .control.has-radio .label { text-align: left; align-self: flex-start; }

/* ----------------------------------------------------------------- control */

.control {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  position: relative;
}

/* Always one line.
 *
 * Every control sits under a group heading that supplies the context, so the
 * label can be short without being abbreviated -- "Fine Tune" under "Oscillator
 * 2" says as much as "Oscillator 2 Fine Tune" did over three ragged lines. The
 * ellipsis is a backstop for a label that still does not fit; the full name is
 * always in the information popover and in the hover title, so nothing that gets
 * clipped here is lost. */
.control .label {
  font-size: 10px;
  line-height: 1.25;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-dim);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.control .value {
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  text-align: center;
  line-height: 1.2;
  /* Readings are upper case with the rest of the interface; the unit inside them
   * opts back out below, because "kHz" and "ms" are not words to be styled. */
  text-transform: uppercase;
}

/* Every unit, wherever it came from -- the reference's own display string or the
 * generated table's suffix -- is grey and keeps its own capitalisation.
 *
 * Deliberately well under the reading it belongs to. The number is what is being
 * read; "msec" and "cent" only say what it is measured in, and at the same
 * weight they compete with it. Set here rather than per breakpoint because the
 * relationship holds at every width. */
.control .value .unit {
  color: var(--ink-faint);
  font-size: 8px;
  margin-left: 3px;
  text-transform: none;
}

/* ------------------------------------------------------------------- radio */

.radio {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
  align-items: stretch;
}

.radio button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 3px 0;
  appearance: none;
  background: none;
  border: 0;
  color: var(--ink-faint);
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-align: left;
  cursor: pointer;
  transition: color 120ms ease;
}

.radio button:hover { color: var(--ink-dim); }
.radio button[aria-checked="true"] { color: var(--ink); }

/* The option mark uses the same silver state as its text, without a glow. */
.radio .dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1px solid #3a3a42;
  background: #0c0c0f;
  transition: background 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
}

.radio button[aria-checked="true"] .dot {
  background: var(--metal-lit);
  border-color: var(--metal-lit);
}

.radio button:focus-visible { outline: 1px solid #3a4150; outline-offset: 2px; }

/* ----------------------------------------------------------------- stepper */

.stepper {
  display: flex;
  align-items: center;
  gap: 6px;
}

.stepper button {
  width: 24px;
  height: 24px;
  flex: 0 0 auto;
  border-radius: 6px;
  border: 1px solid var(--panel-edge);
  background: #131317;
  color: var(--ink-dim);
  font: inherit;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}

.stepper button:hover:not(:disabled) { color: var(--ink); border-color: #3a4150; }
.stepper button:disabled { opacity: 0.3; cursor: default; }

.stepper .num {
  min-width: 38px;
  text-align: center;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* ---------------------------------------------------------------- disabled */

/* A control switched off by its section's enable, or by an effect type that does
 * not read it. Dimmed rather than hidden: what a section contains should not
 * change shape when it is switched off, or finding the switch again means
 * remembering where the panel used to be. */
.control.off {
  opacity: 0.28;
  transition: opacity 140ms ease;
}

.control.off .knob,
.control.off .toggle,
.control.off .radio,
.control.off .stepper,
.control.off .options { pointer-events: none; }

/* The mark stays readable and clickable: what a control does is worth knowing
 * whether or not it is currently doing it. */
.control.off .info { opacity: 0.9; pointer-events: auto; }
.control.off .label,
.control.off .value { pointer-events: none; }

/* -------------------------------------------------------------------- info */

.control .info {
  position: absolute;
  top: -3px;
  right: -1px;
  width: 15px;
  height: 15px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--panel-edge);
  background: none;
  color: var(--ink-faint);
  font: inherit;
  font-size: 9px;
  font-style: italic;
  line-height: 1;
  cursor: pointer;
  opacity: 0.5;
  transition: opacity 120ms ease, color 120ms ease;
}

.control:hover .info, .control .info:focus-visible { opacity: 1; color: var(--ink-dim); }

/* On a touch screen there is no hover to reveal it, so it stays visible. */
@media (hover: none) {
  .control .info { opacity: 0.8; }
}

.popover {
  position: fixed;
  z-index: 40;
  width: 260px;
  max-width: calc(100vw - 16px);
  background: #16161b;
  border: 1px solid #2a2a31;
  border-radius: 8px;
  padding: 12px 13px;
  box-shadow: none;
  display: none;
}

.popover.open { display: block; }

.popover-title {
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 7px;
}

.popover p {
  margin: 0 0 9px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink-dim);
}

.popover-meta {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* -------------------------------------------------------------------- knob */

.knob {
  width: 58px;
  height: 58px;
  touch-action: none;          /* the drag is ours, not the scroller's */
  cursor: ns-resize;
  display: block;
  user-select: none;
  -webkit-user-select: none;
}

.knob .track { stroke: var(--track); }
.knob .arc { stroke: var(--accent); transition: stroke 120ms ease; }
.knob .pointer { stroke: var(--metal-lit); }
.knob:focus { outline: none; }
.knob:focus-visible .arc, .knob.active .arc { stroke: #ffffff; }

/* ------------------------------------------------------------------ toggle */

/* 28 tall, not 26. The switch is an ordinary target and the approved ledger puts
 * the floor at 28px everywhere, so the two pixels it was short of it are added
 * here rather than only at the phone breakpoint below, which was already over.
 * The pill's proportions are kept: the radius and the thumb grow with the track,
 * and the travel is unchanged because the width did not move. */
.toggle {
  width: 52px;
  height: 28px;
  border-radius: 14px;
  border: 1px solid var(--panel-edge);
  background: #131317;
  position: relative;
  cursor: pointer;
  padding: 0;
  touch-action: manipulation;
  transition: background 120ms ease, border-color 120ms ease;
}

.toggle::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: linear-gradient(180deg, var(--metal-lit), var(--metal-dark));
  transition: transform 130ms cubic-bezier(0.3, 0.7, 0.4, 1);
}

.toggle[aria-checked="true"] { background: #2a2f3a; border-color: #3a4150; }
.toggle[aria-checked="true"]::after { transform: translateX(26px); }

/* ----------------------------------------------------------------- options */

.options {
  width: 100%;
  background: #131317;
  color: var(--ink);
  border: 1px solid var(--panel-edge);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 10px;
  font-family: inherit;
  /* Upper case with everything else. A native dropdown draws its own list, so
   * this reaches the closed control; the options carry it themselves below. */
  text-transform: uppercase;
  letter-spacing: 0.04em;
  /* 16px on a phone, or iOS zooms the whole page when the select is tapped. */
  cursor: pointer;
  touch-action: manipulation;
}

.options:focus { outline: 1px solid #3a4150; }

/* The open list is drawn by the platform and inherits almost nothing, so the
 * casing has to be set on the options as well as on the control. */
.options option {
  text-transform: uppercase;
  background: #16161b;
  color: var(--ink);
}

/* -------------------------------------------------------------------- bank */

/* Fixed at the foot, and moved up by exactly the keyboard's height when the
 * keyboard is out. The offset is a variable rather than a second fixed number so
 * the two cannot disagree: the script measures the keyboard once and writes it
 * here. */
.bank {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--keys-h, 0px);
  z-index: 15;
  display: flex;
  align-items: stretch;
  gap: 10px;
  padding: 0 10px;
  background: rgba(8, 8, 10, 0.96);
  backdrop-filter: blur(10px);
  border-top: 1px solid var(--panel-edge);
  transition: bottom 140ms ease;
}

/* Only when the keyboard is away; with it out, the keyboard owns the inset. */
body:not(.keys-open) .bank { padding-bottom: env(safe-area-inset-bottom, 0px); }

.bank-read {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 14px;
  padding: 9px 0;
}

/* Which of the two gives up its width first, when there is not enough.
 *
 * It used to be decided by accident, and decided backwards. `overflow: hidden`
 * sets a flex item's automatic minimum size to zero, so the patch -- the only
 * one of the pair with it -- was the one flexbox was free to crush, while the
 * bank name had no `overflow` at all and so refused to shrink below its full
 * width. On a phone the strip read "0.. QUESYNTH FACTORY": the name of the
 * sound gone, the name of the bank it came from intact.
 *
 * So the shrink factors say it outright instead. The bank is context and the
 * patch is the thing itself, so the bank yields first and by a long way, and
 * the patch only starts to lose characters once the bank has none left. */
.bank-patch {
  flex: 0 1 auto;
  min-width: 0;
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bank-name {
  flex: 0 100 auto;
  min-width: 0;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bank-step {
  flex: 0 0 auto;
  appearance: none;
  background: none;
  border: 0;
  color: var(--ink-faint);
  font: inherit;
  font-size: 17px;
  line-height: 1;
  padding: 0 12px;
  cursor: pointer;
  transition: color 120ms ease;
}

.bank-step:hover { color: var(--ink); }

/* The file button, beside the step arrows. Lettered rather than an icon, for
 * the reason the MIDI and Keys buttons are: this panel has no icon set and one
 * glyph nobody recognises is worse than four characters everybody reads. */
.bank-write {
  flex: 0 0 auto;
  appearance: none;
  background: none;
  border: 1px solid var(--panel-edge);
  border-radius: 5px;
  color: var(--ink-faint);
  font: inherit;
  font-size: 9px;
  letter-spacing: 0.12em;
  padding: 4px 8px;
  margin-left: 10px;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}

.bank-write:hover { color: var(--ink); border-color: var(--metal-dark); }

.bank-read.pickable { cursor: pointer; }
.bank-read.pickable:hover .bank-patch { color: #ffffff; }

/* The patch list and the MIDI input list. Same object, two anchors: one opens
 * upward off the bar at the foot, the other downward off the strip at the top. */
.bank-list,
.midi-list {
  position: fixed;
  z-index: 40;
  max-height: min(60vh, 420px);
  overflow-y: auto;
  min-width: 220px;
  background: #16161b;
  border: 1px solid #2a2a31;
  border-radius: 8px;
  padding: 6px;
  box-shadow: none;
  display: none;
}

.bank-list.open,
.midi-list.open { display: block; }

.bank-list button,
.midi-list button {
  display: block;
  width: 100%;
  text-align: left;
  appearance: none;
  background: none;
  border: 0;
  border-radius: 5px;
  color: var(--ink-dim);
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 6px 9px;
  cursor: pointer;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.bank-list button:hover,
.midi-list button:hover { color: var(--ink); background: #1e1e25; }

.bank-list button[aria-current="true"],
.midi-list button[aria-current="true"] { color: var(--ink); background: #262b35; }

.midi-none {
  padding: 8px 9px;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ---------------------------------------------------------------- keyboard */

.keys-toggle {
  flex: 0 0 auto;
  align-self: center;
  appearance: none;
  background: none;
  border: 1px solid var(--panel-edge);
  border-radius: 6px;
  color: var(--ink-faint);
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 6px 11px;
  cursor: pointer;
  white-space: nowrap;
  transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
}

.keys-toggle:hover { color: var(--ink-dim); }

.keys-toggle[aria-expanded="true"] {
  color: var(--ink);
  border-color: #3a4150;
  background: #1b1f27;
}

.keyboard {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  background: rgba(8, 8, 10, 0.96);
  backdrop-filter: blur(10px);
  border-top: 1px solid var(--panel-edge);
  /* Clear of the home indicator on a phone held upright. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.keyboard[hidden] { display: none; }

/* The octave controls at the two ends, not huddled in the middle: on a phone held
 * in two hands that is where the thumbs already are. */
.keys-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 5px 10px 4px;
}

.keys-row {
  display: flex;
  align-items: stretch;
  gap: 10px;
  padding: 0 0 6px 10px;
}

.oct {
  appearance: none;
  width: 26px;
  height: 22px;
  border-radius: 5px;
  border: 1px solid var(--panel-edge);
  background: #131317;
  color: var(--ink-dim);
  font: inherit;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

.oct:hover:not(:disabled) { color: var(--ink); border-color: #3a4150; }
.oct:disabled { opacity: 0.3; cursor: default; }

.oct-label {
  min-width: 92px;
  text-align: center;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* The strip scrolls, but only under the octave buttons and a trackpad -- a drag
 * that starts on a key plays it, because that is what dragging across a keyboard
 * means. Reaching another octave by swiping would make a glissando impossible,
 * so the two gestures are not allowed to compete. */
.keys-scroll {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}

.keys-scroll::-webkit-scrollbar { display: none; }

.keys {
  position: relative;
  height: 104px;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.key {
  position: absolute;
  top: 0;
  border: 0;
  padding: 0;
  cursor: pointer;
  font: inherit;
}

/* Not an ivory keyboard.
 *
 * A real piano's colours are the one place this interface would have gone loud:
 * a bank of white keys under a black panel is the brightest thing on the screen
 * and pulls the eye away from the controls. So the keys are the panel's own greys
 * -- the naturals a raised surface, the accidentals nearly the background -- and
 * what lights up is the key being *played*, in the same silver the lit options and
 * the knob pointers use. The instrument reads as one object, and the only bright
 * thing on it is the note sounding. */
/* Flat fills and one border each. No gradients, no bevels, no drop shadows: a key
 * is a rectangle, its border says where it ends, and the only state it has is
 * whether it is sounding. */
.key.natural {
  height: 100%;
  background: #1b1b21;
  border: 1px solid #2c2c34;
  border-radius: 0 0 3px 3px;
}

.key.accidental {
  height: 60%;
  z-index: 2;
  background: #0b0b0e;
  border: 1px solid #26262d;
  border-radius: 0 0 3px 3px;
}

.key.natural.down,
.key.accidental.down {
  background: var(--metal-lit);
  border-color: var(--metal-lit);
}

/* Middle C, so a hand knows where it is without counting from the end. */
.key.natural.anchor::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 8px;
  width: 4px;
  height: 4px;
  margin-left: -2px;
  border-radius: 50%;
  background: #6a6a74;
}

.key.natural.anchor.down::after { background: #33333c; }

/* ------------------------------------------------------------------ wheels */

.wheels {
  flex: 0 0 auto;
  display: flex;
  gap: 8px;
}

.wheel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

.wheel-track {
  position: relative;
  width: 26px;
  flex: 1 1 auto;
  border-radius: 13px;
  background: #0b0b0e;
  border: 1px solid var(--panel-edge);
  touch-action: none;
  cursor: ns-resize;
  overflow: hidden;
}

/* The centre detent, drawn rather than felt: a bend wheel that springs back needs
 * somewhere visible to spring back to. */
.wheel:first-child .wheel-track::before {
  content: "";
  position: absolute;
  left: 4px;
  right: 4px;
  top: 50%;
  height: 1px;
  background: #2e2e36;
}

/* Positioned from one custom property so the script never has to know which way
 * the wheel is lying. `--t` runs 0 to 1 from the bottom, or from the left once the
 * phone layout turns the track on its side. */
.wheel-thumb {
  position: absolute;
  left: 2px;
  right: 2px;
  height: 20px;
  bottom: calc(2px + var(--t, 0.5) * (100% - 24px));
  border-radius: 10px;
  background: var(--metal);
  pointer-events: none;
}

.wheel-label {
  font-size: 8px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ------------------------------------------------------------------ phones */

@media (max-width: 560px) {
  :root { --gap: 10px; }
  body { padding: 10px 8px 24px; }
  /* The phone body uses 8px side padding while --gap is 10px. Keep the sticky
   * strip on that actual edge; using the desktop negative gap made the document
   * two pixels wider than the viewport and exposed a horizontal scrollbar. */
  nav { margin: 0 -8px var(--gap); padding-inline: 8px; }

  /* No snapping on a phone. Sections are far taller than the screen here, so a
   * snap has nothing useful to land on and only takes the scroll away from the
   * thumb. The navigation strip stays, because thirteen sections is a long way
   * to flick past. */
  html { scroll-snap-type: none; }

  /* Groups stack instead of sitting side by side, with the rule moved from their
   * left edge to their top -- at 375px there is no room for two of them abreast
   * and a left rule on a full-width block reads as an indent rather than a
   * division.
   *
   * The rule needs air on both sides. Padding alone only pushes the heading down
   * from it and leaves the previous group's controls sitting on the line. */
  .groups { display: block; }
  .group {
    margin-top: 18px;
    padding: 16px 0 0;
    border-left: 0;
    border-top: 1px solid var(--panel-edge);
  }

  /* Back to a filling grid: at 375px the fixed-width row leaves a ragged margin
   * where stretching to the full width does not. */
  .controls {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(98px, 1fr));
    gap: 18px 8px;
  }
  /* Every fixed width from the desktop row layout is released here, the dropdown
   * one included -- a 150px control does not fit a 375px grid cell and pushes the
   * column out from under its own label. */
  .controls > .control,
  .controls > .control.wide,
  .controls > .control.has-options,
  .controls > .control.has-radio,
  .controls > .control.has-stepper { width: auto; min-width: 0; }

  /* A list of spelled-out options does not fit a 98px column, so it takes two
   * like the dropdowns do. */
  .controls > .control.has-radio { grid-column: span 2; }

  /* A dropdown takes two columns instead of one: enough for the longest option
   * name this instrument has ("Low Pass 24 dB/oct") without giving each one a
   * whole row of its own, which would make a section of four dropdowns four
   * screens long. */
  .controls > .control.has-options { grid-column: span 2; }
  .knob { width: 66px; height: 66px; }
  /* Smaller text, but the same generous box: the padding is what makes this a
   * comfortable tap target, not the type size, so shrinking the glyphs costs
   * nothing in reachability.
   *
   * The 16px this replaces was there for one reason -- iOS zooms the page when a
   * form control under 16px is focused, and the viewport line deliberately does
   * not set `maximum-scale`, because forbidding zoom to win this argument would
   * take pinch-to-zoom away from everything else on the page. So on iOS tapping
   * a dropdown may now zoom in and need a pinch back out; that is the price of
   * the smaller type, and it is paid only on iOS and only on dropdowns. */
  .options { font-size: 12px; padding: 9px 8px; }
  .toggle { width: 58px; height: 30px; }
  .toggle::after { width: 24px; height: 24px; }
  .toggle[aria-checked="true"]::after { transform: translateX(28px); }

  /* Big enough to play, since on a phone this is the whole instrument and not an
   * accessory to a mouse. The octave buttons are how another register is reached
   * here, because a swipe across the keys has to stay a glissando. */
  .keys { height: 172px; }
  .oct { width: 52px; height: 36px; font-size: 17px; }
  .keys-bar { padding: 6px 10px 5px; }

  /* The wheels lie down and move above the keys.
   *
   * Standing beside them they cost about eighty pixels of a three-hundred-and
   * -seventy-five pixel screen, which is a whole octave of reach; lying across
   * the top they cost a row barely taller than the label they already needed.
   * Both gestures still work, because the track reads its own orientation from
   * its shape rather than being told which way it is pointing. */
  .keys-row {
    flex-direction: column;
    gap: 8px;
    padding: 0 8px 6px;
  }

  .wheels { width: 100%; gap: 12px; }

  /* Reversed, so the label leads its own track. In source order the track comes
   * first, which laid out as a row put "Pitch" between the two wheels where it
   * reads as the label of the one after it. */
  .wheel {
    flex: 1 1 0;
    flex-direction: row-reverse;
    justify-content: flex-end;
    align-items: center;
    gap: 7px;
  }

  .wheel-track {
    width: auto;
    height: 22px;
    flex: 1 1 auto;
    border-radius: 11px;
    cursor: ew-resize;
  }

  .wheel-thumb {
    left: calc(2px + var(--t, 0.5) * (100% - 24px));
    right: auto;
    top: 2px;
    bottom: auto;
    width: 20px;
    height: calc(100% - 4px);
  }

  /* The detent turns with the track. */
  .wheel:first-child .wheel-track::before {
    left: 50%;
    right: auto;
    top: 4px;
    bottom: 4px;
    width: 1px;
    height: auto;
  }

  .wheel-label { font-size: 8px; }
  .keys-scroll { width: 100%; }
  .brand { font-size: 11px; letter-spacing: 0.14em; }
}

/* Master volume, in the bank bar.
 *
 * Not a patch parameter: the reference's own "vol" knob sits beside the patch
 * name rather than in the panel, and nothing in the .sy1 format carries it, so
 * it is deliberately outside the engine. It leaves as its own message and each
 * host scales its own output, which leaves every measured parameter mapping
 * untouched -- a master volume that fed the engine would show up in the null
 * test as a level error on every patch at once. */
.bank-vol {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  padding-right: 4px;
}

.bank-vol label {
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.bank-vol-read {
  font-size: 11px;
  color: var(--ink-faint);
  min-width: 2.2em;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* The one slider this interface has, wherever it appears: the master volume in
 * the strip at the foot and the note a pad plays. Written once for both rather
 * than copied, because a second copy of a control is how two of them end up
 * looking almost the same. The width is the only thing that differs. */
.bank-vol input[type="range"],
.pad-note input[type="range"] {
  appearance: none;
  -webkit-appearance: none;
  width: 84px;
  height: 18px;
  background: none;
  cursor: pointer;
}

.bank-vol input[type="range"]::-webkit-slider-runnable-track,
.pad-note input[type="range"]::-webkit-slider-runnable-track {
  height: 2px;
  background: var(--panel-edge);
  border-radius: 1px;
}

.bank-vol input[type="range"]::-moz-range-track,
.pad-note input[type="range"]::-moz-range-track {
  height: 2px;
  background: var(--panel-edge);
  border-radius: 1px;
}

.bank-vol input[type="range"]::-webkit-slider-thumb,
.pad-note input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 11px;
  height: 11px;
  margin-top: -4.5px;
  border-radius: 50%;
  background: linear-gradient(#c9c9d1, #7e7e88);
  border: 0;
}

.bank-vol input[type="range"]::-moz-range-thumb,
.pad-note input[type="range"]::-moz-range-thumb {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: linear-gradient(#c9c9d1, #7e7e88);
  border: 0;
}

.bank-vol input[type="range"]:focus-visible,
.pad-note input[type="range"]:focus-visible { outline: 1px solid var(--ink-faint); }

/* On a phone the bar is tight; the readout is the first thing to go. */
@media (max-width: 560px) {
  .bank-vol label,
  .bank-vol-read { display: none; }
  .bank-vol input[type="range"] { width: 60px; }
}

/* A key lit by an incoming MIDI note, as distinct from one held by a pointer.
 * Same treatment, because to the player they are the same event. */

/* ------------------------------------------------------------------- modal */

/* One dialog shell, used by the patch browser and by whatever comes after it.
 * See ui/modal.js for why this is a plain overlay and not <dialog>. */

body.modal-open { overflow: hidden; }

.modal-back {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--gap);
  background: rgba(4, 4, 6, 0.72);
  backdrop-filter: blur(3px);
}

.modal {
  display: flex;
  flex-direction: column;
  width: min(760px, 100%);
  max-height: min(80vh, 620px);
  background: var(--panel);
  border: 1px solid var(--panel-edge);
  border-radius: var(--radius);
  box-shadow: none;
  overflow: hidden;
}

.modal-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--panel-edge);
}

.modal-title {
  flex: 1 1 auto;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
}

.modal-close {
  flex: 0 0 auto;
  appearance: none;
  background: none;
  border: 0;
  padding: 0 4px;
  font-size: 20px;
  line-height: 1;
  color: var(--ink-dim);
  cursor: pointer;
}
.modal-close:hover { color: var(--ink); }

.modal-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  overflow: hidden;
}

.modal-foot {
  flex: 0 0 auto;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 10px 14px;
  border-top: 1px solid var(--panel-edge);
}

.modal-action {
  appearance: none;
  padding: 7px 14px;
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-dim);
  background: none;
  border: 1px solid var(--panel-edge);
  border-radius: 6px;
  cursor: pointer;
}
.modal-action:hover { color: var(--ink); border-color: var(--metal-dark); }
.modal-action.primary { color: var(--ink); border-color: var(--metal-dark); }

/* ----------------------------------------------------------- bank browser */

.browser {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  width: 100%;
}

/* The two panes. Their own row, because the save field sits under both. */
.browser-panes {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  width: 100%;
}

/* Naming a sound on its way into a slot. Hidden until asked for, so the
 * browser is a list until the moment it is a form. */
.browser-save {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-top: 1px solid var(--panel-edge);
  background: #121216;
}

.browser-save-slot {
  flex: 0 0 auto;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.browser-save-name {
  flex: 1 1 auto;
  min-width: 0;
  appearance: none;
  padding: 7px 9px;
  font: inherit;
  font-size: 12px;
  color: var(--ink);
  background: #0a0a0d;
  border: 1px solid var(--panel-edge);
  border-radius: 5px;
}
.browser-save-name:focus {
  outline: none;
  border-color: var(--metal-dark);
}

.browser-sources {
  flex: 0 0 190px;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px;
  overflow-y: auto;
  border-right: 1px solid var(--panel-edge);
}

.browser-heading {
  padding: 2px 6px 6px;
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.browser-source,
.browser-open {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  appearance: none;
  padding: 7px 8px;
  font: inherit;
  font-size: 12px;
  text-align: left;
  color: var(--ink-dim);
  background: none;
  border: 0;
  border-radius: 5px;
  cursor: pointer;
}
.browser-source:hover, .browser-open:hover { background: #16161b; color: var(--ink); }
.browser-source.on { background: #1b1b22; color: var(--ink); }
.browser-source-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.browser-source-count { flex: 0 0 auto; font-size: 10px; color: var(--ink-faint); }

.browser-open { margin-top: 6px; color: var(--ink-faint); }

.browser-slots {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.browser-slots-head {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--panel-edge);
}
.browser-bank-name {
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.browser-bank-count { flex: 0 0 auto; font-size: 10px; color: var(--ink-faint); }

/* Numbered columns, the way a hardware patch list is printed: down one column
 * and on to the next, so a run of numbers stays vertically adjacent and the
 * eye can go straight to "somewhere in the forties".
 *
 * The column count and the row count are set from ui/browser.js rather than
 * here. `auto-fill` can do the columns but not this order: filling downward
 * needs `grid-auto-flow: column`, and that needs an explicit row count, which
 * depends on how many columns fitted. One measurement settles both. */
.browser-grid {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 8px;
  display: grid;
  grid-auto-flow: column;
  gap: 1px 8px;
  align-content: start;
}

.browser-slot {
  display: flex;
  align-items: baseline;
  gap: 8px;
  appearance: none;
  padding: 5px 7px;
  font: inherit;
  font-size: 12px;
  text-align: left;
  color: var(--ink-dim);
  background: none;
  border: 0;
  border-radius: 4px;
  cursor: pointer;
}
.browser-slot:hover { background: #16161b; color: var(--ink); }
.browser-slot.on { background: #23232b; color: var(--ink); }
.browser-slot.empty .browser-slot-name { color: var(--ink-faint); }

.browser-slot-num {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--ink-faint);
}
.browser-slot.on .browser-slot-num { color: var(--ink-dim); }
.browser-slot-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* On a phone the two panes stack: sources across the top, slots below. */
@media (max-width: 620px) {
  .modal { max-height: 88vh; }
  .browser-panes { flex-direction: column; }
  .browser-sources {
    flex: 0 0 auto;
    max-height: 30vh;
    border-right: 0;
    border-bottom: 1px solid var(--panel-edge);
  }
}

/* New and Add, beside the "Banks" heading. Small, because they are the two
 * least frequent things in the dialog and the list is the point. */
/* The label on one line and the three actions on the next.
 * Side by side they did not fit: the pane is 190px, and BANKS plus New, Add
 * and Folder overflowed it and put a horizontal scrollbar under a list that
 * scrolls vertically. */
.browser-heading {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

.browser-heading-actions {
  display: flex;
  gap: 4px;
}

.browser-heading-actions .browser-mini {
  flex: 1 1 0;
  text-align: center;
  padding-left: 2px;
  padding-right: 2px;
}

.browser-mini {
  appearance: none;
  padding: 3px 7px;
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-dim);
  background: none;
  border: 1px solid var(--panel-edge);
  border-radius: 4px;
  cursor: pointer;
}
.browser-mini:hover { color: var(--ink); border-color: var(--metal-dark); }

/* Where the sound is about to go. Marked differently from the slot you are
 * playing, because in write mode they are not the same thing and confusing
 * them is how a patch gets overwritten by accident. */
.browser-slot.target {
  background: #2a2118;
  color: var(--ink);
  outline: 1px solid #6b5334;
}
.browser-slot.target .browser-slot-num { color: #c39a5e; }

/* A bank that is listed but not read yet -- one row of a zip of banks. Dimmer,
 * because its name is all that is known about it until it is opened. */
.browser-source.unopened .browser-source-name { color: var(--ink-faint); }
.browser-source.unopened:hover .browser-source-name { color: var(--ink-dim); }

/* Searching the bank list. Bank names only -- see the note in ui/browser.js
 * about why the patches inside an unopened bank are not searchable. */
.browser-search,
.browser-archive-path {
  flex: 0 0 auto;
  width: 100%;
  appearance: none;
  margin-bottom: 4px;
  padding: 6px 8px;
  font: inherit;
  font-size: 12px;
  color: var(--ink);
  background: #0a0a0d;
  border: 1px solid var(--panel-edge);
  border-radius: 5px;
}
.browser-search::placeholder { color: var(--ink-faint); }
.browser-search:focus { outline: none; border-color: var(--metal-dark); }

/* The rows get their own box so the field above them is never rebuilt. */
.browser-source-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.browser-source-empty {
  padding: 10px 8px;
  font-size: 11px;
  color: var(--ink-faint);
}

/* What the Add/Folder pickers report. An error reads in the brightest ink tier
   so it is not mistaken for the faint labels around it; a success stays quiet.
   No new colour: the message text carries the meaning. */
.browser-status {
  padding: 8px;
  font-size: 12px;
  line-height: 1.35;
}
.browser-status.error { color: var(--ink); }
.browser-status.ok { color: var(--ink-faint); }

/* The archive is a source beside the ordinary bank, not the playing patch.
 * Paths can be far wider than the narrow source pane; never size it by one. */
.browser-archive { min-width: 0; padding: 4px 0 8px; }
.browser-archive > label {
  display: block;
  font-size: 11px;
  color: var(--ink-faint);
}
.browser-archive-path { min-width: 0; margin-top: 4px; }
.browser-archive-path:focus { outline: 2px solid var(--accent); }
.browser-archive .browser-status { padding: 6px 0; overflow-wrap: anywhere; }
.browser-playing {
  flex: 0 0 auto;
  padding: 8px 14px;
  font-size: 11px;
  color: var(--ink-dim);
  overflow-wrap: anywhere;
  border-bottom: 1px solid var(--panel-edge);
}

/* --------------------------------------------------------------- configure */

.options {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.options-block { display: flex; flex-direction: column; gap: 8px; }

.options-heading-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.options-heading {
  margin: 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.options-note {
  margin: 0;
  max-width: 62ch;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ink-faint);
}

.options-fixed {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-top: 2px;
}

.options-fixed-row {
  display: flex;
  gap: 12px;
  padding: 5px 8px;
  border-radius: 4px;
  background: #101014;
}

.options-fixed-what {
  flex: 0 0 130px;
  font-size: 12px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.options-fixed-does {
  flex: 1 1 auto;
  font-size: 12px;
  color: var(--ink-faint);
}

.options-rows { display: flex; flex-direction: column; gap: 4px; }

.options-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.options-cc {
  flex: 0 0 56px;
  font-size: 12px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.options-target {
  flex: 1 1 auto;
  min-width: 0;
  appearance: none;
  padding: 6px 8px;
  font: inherit;
  font-size: 12px;
  color: var(--ink);
  background: #0a0a0d;
  border: 1px solid var(--panel-edge);
  border-radius: 5px;
}
.options-target:focus { outline: none; border-color: var(--metal-dark); }

.options-empty { padding: 8px; font-size: 12px; color: var(--ink-faint); }
.options-add { align-self: flex-start; margin-top: 4px; }

/* Sentences, not labels. The panel uppercases almost everything, which is right
 * for a control's name and wrong for a paragraph explaining one. */
.options-note,
.options-fixed-does {
  text-transform: none;
  letter-spacing: 0;
}

/* The wiki's address, shown when a web view refuses to open a window for it.
 * Selectable, because the only thing left to do with it is copy it. */
.options-address {
  user-select: text;
  color: var(--ink-dim);
  font-size: 12px;
  word-break: break-all;
}


/* ---------------------------------------------------------------- the pad --- */
.pad-body { --rack-accent-soft: color-mix(in srgb, var(--accent) 20%, transparent); }
.pads { display: grid; gap: 12px; margin-bottom: 18px; }
.rack-toolbar,
.pad-inspector,
.rack-editor-title {
  background: var(--panel);
  border: 1px solid var(--panel-edge);
  border-radius: var(--radius);
}
.rack-toolbar { display: flex; align-items: end; gap: 12px; padding: 12px; }
.rack-kit-name { flex: 1 1 240px; min-width: 150px; }
.rack-compact { flex: 0 1 170px; }
.rack-toolbar label,
.rack-field,
.rack-slider { display: grid; gap: 6px; }
.rack-toolbar label > span,
.rack-field > span,
.rack-slider > span,
.eyebrow {
  color: var(--ink-faint); font-size: 8px; letter-spacing: .13em; text-transform: uppercase;
}
.rack-toolbar input,
.rack-toolbar select,
.rack-field input,
.rack-field select {
  width: 100%; min-width: 0; box-sizing: border-box; padding: 7px 8px;
  border: 1px solid var(--panel-edge); border-radius: 5px; background: #0e0e11;
  color: var(--ink); font: inherit; font-size: 11px;
}
.rack-kit-name input { font-size: 14px; font-weight: 650; letter-spacing: .02em; }
.rack-file-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.rack-workspace { display: grid; grid-template-columns: minmax(360px, 1.45fr) minmax(290px, .85fr); gap: 12px; align-items: stretch; }
.pad-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; min-width: 0; }
.pad-cell {
  position: relative; appearance: none; aspect-ratio: 1 / .78; min-height: 76px;
  display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-end;
  gap: 3px; padding: 9px; overflow: hidden; background: var(--panel);
  border: 1px solid var(--panel-edge); border-radius: var(--radius); color: var(--ink-dim);
  font: inherit; text-align: left; cursor: pointer; touch-action: none; user-select: none;
  -webkit-user-select: none; -webkit-tap-highlight-color: transparent;
  transition: transform 80ms ease, border-color 120ms ease, background 120ms ease, opacity 120ms ease;
}
.pad-cell::before { display: none; }
.pad-cell:hover { border-color: var(--metal-dark); transform: translateY(-1px); color: var(--ink); }
.pad-cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pad-cell.sel { border-color: var(--accent); color: var(--ink); }
.pad-cell.lit { background: var(--metal); border-color: var(--metal-lit); color: #09090b; transform: translateY(1px) scale(.985); transition: none; }
.pad-cell.muted { opacity: .42; }
.pad-cell.conflict .pad-cell-note::after { content: "  layered"; color: #c39a5e; }
.pad-cell-top { position: absolute; inset: 8px 8px auto; display: flex; justify-content: space-between; align-items: center; }
.pad-cell-index { color: var(--ink-faint); font-size: 8px; letter-spacing: .16em; }
.pad-cell-mode { color: var(--ink-faint); font-size: 7px; letter-spacing: .11em; }
.pad-cell-name { position: relative; max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 11px; letter-spacing: .025em; }
.pad-cell-note { position: relative; color: var(--ink-faint); font-size: 8px; letter-spacing: .1em; }
.pad-cell.lit .pad-cell-note, .pad-cell.lit .pad-cell-index, .pad-cell.lit .pad-cell-mode { color: #2a2a30; }
.pad-inspector { display: flex; flex-direction: column; gap: 12px; padding: 14px; min-width: 0; }
.pad-inspector-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.pad-inspector-head > div { display: grid; gap: 4px; min-width: 0; }
.pad-inspector-head strong { overflow: hidden; color: var(--ink); font-size: 14px; font-weight: 600; letter-spacing: .04em; text-overflow: ellipsis; white-space: nowrap; }
.rack-fields { display: grid; gap: 10px; }
.rack-fields.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.rack-field { position: relative; }
.rack-field output { position: absolute; right: 7px; bottom: 8px; color: var(--ink-faint); font-size: 8px; pointer-events: none; }
.rack-field input[type="number"] { padding-right: 66px; }
.rack-sliders { display: grid; gap: 10px; padding-top: 2px; }
.rack-slider { grid-template-columns: 58px minmax(70px, 1fr) 58px; align-items: center; }
.rack-slider input { width: 100%; min-width: 0; }
.rack-slider output { color: var(--ink-dim); font-size: 9px; text-align: right; }
.rack-toggles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.rack-toggles button {
  appearance: none; padding: 7px 5px; border: 1px solid var(--panel-edge); border-radius: 5px;
  background: transparent; color: var(--ink-faint); font: inherit; font-size: 8px; letter-spacing: .1em; cursor: pointer;
}
.rack-toggles button.on { border-color: var(--accent); background: var(--rack-accent-soft); color: var(--ink); }
.pad-inspector-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: auto; }
.pad-inspector-actions #pad-edit { margin-left: auto; }
.rack-hint { margin: 0; color: var(--ink-faint); font-size: 9px; line-height: 1.5; text-transform: none; letter-spacing: .02em; }
.pad-action {
  flex: 0 0 auto; appearance: none; padding: 6px 9px; border: 1px solid var(--panel-edge);
  border-radius: 5px; background: none; color: var(--ink-faint); font: inherit;
  font-size: 8px; letter-spacing: .11em; cursor: pointer; transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
}
.pad-action.primary { border-color: color-mix(in srgb, var(--accent) 65%, var(--panel-edge)); color: var(--ink); }
.pad-action:hover:not(:disabled), .pad-action.learning { color: var(--ink); border-color: var(--accent); background: var(--rack-accent-soft); }
.pad-action:disabled { opacity: .38; cursor: default; }
.rack-editor-title { display: flex; justify-content: space-between; padding: 8px 12px; margin-bottom: var(--gap); color: var(--ink-faint); font-size: 8px; letter-spacing: .13em; text-transform: uppercase; }
.rack-editor-title strong { color: var(--ink); font-weight: 500; }

@media (max-width: 820px) {
  .rack-toolbar { flex-wrap: wrap; }
  .rack-file-actions { width: 100%; margin-left: 0; }
  .rack-workspace { grid-template-columns: 1fr; }
  .pad-cell { aspect-ratio: 1 / .82; }
}
@media (max-width: 520px) {
  .pads { gap: 8px; }
  .rack-toolbar { gap: 8px; padding: 10px; }
  .rack-kit-name { flex-basis: 100%; }
  .rack-compact { flex: 1 1 42%; }
  .rack-file-actions { overflow-x: auto; }
  .pad-grid { gap: 5px; }
  .pad-cell { min-height: 68px; padding: 7px; }
  .pad-cell-top { inset: 6px 6px auto; }
  .pad-cell-name { font-size: 9px; }
  .pad-cell-note, .pad-cell-mode { font-size: 7px; }
  .pad-cell.conflict .pad-cell-note::after { content: " layered"; }
  .pad-inspector { padding: 12px; }
  .rack-fields.two { grid-template-columns: 1fr 1fr; }
}

/* ----------------------------------------------------- shell / viewport refit
 * Identity and global actions live in the fixed shell header. The navigator is
 * owned by the editor it moves through. Both pages use the same viewport grid;
 * pad.html swaps the inline editor for a drawer over its rack. */
:root {
  --head-h: 40px;
  --bank-h: 46px;
  --rail-w: 150px;
}

[hidden] { display: none !important; }

html, body { width: 100%; height: 100%; overflow: hidden; }
body,
body.pad-body {
  padding: 0;
  display: grid;
  grid-template-rows: var(--head-h) minmax(0, 1fr);
}

#app-header {
  position: relative;
  z-index: 30;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 10px;
  background: rgba(8, 8, 10, 0.96);
  border-bottom: 1px solid var(--panel-edge);
}
#app-header .brand { padding: 0; }
.head-actions {
  min-width: 0;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 5px;
}
#app-header .bank-write { margin-left: 0; align-self: center; }

#surface {
  position: relative;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
  /* The keyboard is a fixed foot that shows and hides instantly (display, not a
   * slide), and `--keys-h` is added to the padding so the surface never runs
   * under it. The padding is not animated: transitioning padding thrashes layout
   * every frame, and with the keyboard itself popping in there is nothing for a
   * smoothed pad to stay in step with. Both move at once instead. */
  padding: 10px 10px calc(10px + var(--bank-h) + var(--keys-h, 0px));
}

.rack-editor {
  min-height: 0;
  min-width: 0;
  background: var(--bg);
}
.rack-editor[data-mode="inline"] { height: 100%; }
.rack-editor[data-mode="drawer"] {
  position: absolute;
  inset: 0 0 calc(var(--bank-h) + var(--keys-h, 0px)) 0;
  z-index: 20;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  border-top: 1px solid var(--panel-edge);
}
.rack-editor-title {
  margin: 0;
  border-radius: 0;
  align-items: center;
}
.rack-editor-title strong { margin-left: auto; }
.rack-editor-title #rack-editor-close { margin-left: 8px; }
.editor-body {
  height: 100%;
  min-height: 0;
  min-width: 0;
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr);
}

/* The editor navigator is independently scrollable. Its last entry therefore
 * remains reachable even when the keyboard reduces the editor's height. */
#navigator {
  position: relative;
  inset: auto;
  z-index: 1;
  min-height: 0;
  min-width: 0;
  margin: 0;
  padding: 6px;
  display: block;
  overflow: hidden;
  background: var(--panel);
  border-right: 1px solid var(--panel-edge);
  border-bottom: 0;
  backdrop-filter: none;
}
#navigator .nav-inner {
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: 1px;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}
#navigator .nav-inner button {
  width: 100%;
  min-height: 28px;
  padding: 6px 8px;
  border: 0;
  border-left: 2px solid transparent;
  text-align: left;
}
#navigator .nav-inner button[aria-current="true"] {
  border-left-color: var(--metal);
  border-bottom-color: transparent;
}

#panels {
  min-height: 0;
  min-width: 0;
  padding: 0 0 8px 10px;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  overflow: auto;
  overscroll-behavior: contain;
  /* The sole scroll offset in the file, and it is zero on purpose: the
   * navigator sits beside this scroller rather than over it, so nothing covers
   * the top of a section. A `scroll-margin-top` on the sections would be a
   * second source, the two would add, and every jump would land too low. */
  scroll-padding-top: 0;
}

.bank {
  height: var(--bank-h);
  min-height: var(--bank-h);
}

/* The pad surface always consumes the space between header and bank/keyboard.
 * Cells have no aspect ratio: four equal rows divide whatever height remains. */
.pad-body .pads {
  height: 100%;
  min-height: 0;
  margin: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 8px;
}
.pad-body .rack-toolbar {
  padding: 8px 10px;
  gap: 10px;
  background: var(--panel);
}
.pad-body .rack-workspace {
  min-height: 0;
  grid-template-columns: minmax(0, 1.5fr) minmax(280px, .8fr);
  gap: 8px;
}
.pad-body .pad-grid {
  min-height: 0;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-template-rows: repeat(4, minmax(0, 1fr));
  gap: 6px;
}
.pad-body .pad-cell {
  min-height: 0;
  min-width: 0;
  aspect-ratio: auto;
}
.pad-body .pad-inspector {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}
.rack-views { display: none; gap: 5px; }
.rack-views .pad-action { flex: 1 1 0; }

@media (max-width: 820px) {
  .pad-body .pads { grid-template-rows: auto auto minmax(0, 1fr); }
  .rack-views { display: flex; }
  .pad-body .rack-workspace { grid-template-columns: minmax(0, 1fr); }
  .pad-body .pads[data-view="grid"] .pad-inspector { display: none; }
  .pad-body .pads[data-view="inspector"] .pad-grid { display: none; }

  .editor-body {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
  }
  #navigator {
    padding: 5px 6px;
    border-right: 0;
    border-bottom: 1px solid var(--panel-edge);
  }
  #navigator .nav-inner {
    height: auto;
    flex-direction: row;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
  }
  #navigator .nav-inner::-webkit-scrollbar { display: none; }
  #navigator .nav-inner button {
    width: auto;
    flex: 0 0 auto;
    border-left: 0;
    border-bottom: 2px solid transparent;
  }
  #navigator .nav-inner button[aria-current="true"] {
    border-left-color: transparent;
    border-bottom-color: var(--metal);
  }
  #panels { padding-left: 0; padding-top: 8px; }
}

@media (max-width: 560px) {
  body, body.pad-body { padding: 0; }
  #surface { padding: 7px 7px calc(7px + var(--bank-h) + var(--keys-h, 0px)); }
  #app-header { padding-inline: 8px; }
  #app-header .brand { font-size: 11px; letter-spacing: .14em; }
  .pad-body .rack-toolbar { padding: 7px; gap: 7px; }
  .pad-body .rack-kit-name { flex-basis: 100%; }
  .pad-body .rack-compact { flex: 1 1 40%; }
  .pad-body .rack-file-actions { width: 100%; margin-left: 0; }
  .pad-body .pad-cell { min-height: 0; aspect-ratio: auto; }
}

/* ------------------------------------------------ visual/accessibility refit */
/* Five approved roles. The explicit selector ledger prevents inherited legacy
 * 7–10px rules from slipping below the 11px floor.
 *
 * The micro role is for furniture only: labels, readouts, and button captions
 * that are scanned, not read. `.rack-hint` is the one sentence of prose down
 * here, so it takes the body role instead; a line you actually read does not
 * belong at label size. */
html, body { font-size: var(--type-body); }
.rack-hint { font-size: var(--type-body); }
.brand, .rack-kit-name input { font-size: var(--type-lead); }
.panel > h2, .pad-inspector-head strong { font-size: var(--type-title); }
.control .value, .stepper .num, .pad-cell-name { font-size: var(--type-read); }
.nav-inner button, .group-label, .control .label, .control .value .unit,
.radio button, .control .info, .popover-meta, .options, .bank-name,
.bank-write, .keys-toggle, .oct-label, .wheel-label, .bank-vol label,
.browser-save-label, .browser-group-label, .browser-source-count,
.browser-bank-count, .browser-mini, .rack-toolbar label > span, .rack-field > span,
.rack-slider > span, .pad-cell-index, .pad-cell-mode, .pad-cell-note,
.rack-field output, .rack-slider output, .rack-toggles button,
.pad-action, .rack-editor-title { font-size: var(--type-micro); }
.control .label, .group-label, .rack-toolbar label > span,
.rack-field > span, .rack-slider > span { white-space: nowrap; }
.control .value .unit, .bank-name, .wheel-label, .pad-cell-index,
.pad-cell-mode, .pad-cell-note, .rack-field output, .rack-hint { color: var(--ink-dim); }

/* Inline SVG is presentation only. State and focus remain on the button. */
.icon {
  width: 16px;
  height: 16px;
  flex: 0 0 16px;
  overflow: visible;
  fill: none;
  stroke: currentColor;
}
.icon-stroke { fill: none; stroke: currentColor; stroke-width: 5; stroke-linecap: round; stroke-linejoin: round; }
[data-icon] { display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap; }
.radio .dot.has-glyph { width: 28px; height: 20px; border: 0; background: none; }
.radio .dot.has-glyph .option-icon { width: 28px; height: 20px; }
.radio button[aria-checked="true"] .dot { background: var(--metal-lit); border-color: var(--metal-lit); box-shadow: none; }
.radio button[aria-checked="true"] .dot.has-glyph { background: none; color: var(--metal-lit); }

/* The approved finger ledger: ordinary targets 28px, primary actions and
 * steppers 34px. Padding is reduced where type grew so rows do not grow twice. */
.radio button { min-height: 28px; padding-block: 2px; }
.control .info { top: -7px; right: -7px; width: 28px; height: 28px; }
.pad-action, .rack-toggles button, .keys-toggle, .bank-write { min-height: 28px; padding-block: 3px; }
.pad-action.primary, .bank-step, .oct, .stepper button { min-height: 34px; }
/* The stepper's two are square: the ledger's 34px applies across as well as
 * down, or a 24px-wide mark misses the floor in the one dimension nothing
 * measured. Centred as a box, because the mark inside them is now an SVG. */
.stepper button {
  min-width: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.bank-step { font-size: var(--type-micro); padding-inline: 8px; }
.oct { width: auto; min-width: 52px; height: 34px; font-size: var(--type-micro); }
input[type="range"] { min-height: 28px; }
.modal-close { min-height: 28px; font-size: var(--type-micro); letter-spacing: .1em; }
.modal-action { min-height: 34px; }

button:focus-visible, input:focus-visible, select:focus-visible,
.knob:focus-visible, .options:focus, .browser-save-name:focus,
.browser-search:focus, .options-target:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Flat selection: one border and colour, never a glow, shadow, or ornamental
 * radial layer. The lit pad uses one solid metal tone. */
.popover, .modal, .bank-list, .midi-list { box-shadow: none; }
.pad-cell::before { display: none; }
.pad-cell.sel { box-shadow: none; border-color: var(--accent); }
.pad-cell.lit { background: var(--metal); }
.pad-cell.conflict .pad-cell-note::after { content: "  layered"; color: var(--warn, #c39a5e); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    scroll-behavior: auto !important;
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* On a phone the single transport moves to the top. Its identity is therefore
 * present once, above the instrument; the keyboard alone owns the bottom edge. */
@media (max-width: 560px) {
  :root { --head-h: 86px; }
  body, body.pad-body { grid-template-rows: var(--head-h) minmax(0, 1fr); }
  #app-header .brand { font-size: var(--type-micro); }
  #app-header [data-icon] { gap: 3px; padding-inline: 4px; }
  #app-header .icon { width: 13px; height: 13px; flex-basis: 13px; }
  #app-header { height: 40px; align-self: start; }
  .head-actions { overflow-x: auto; scrollbar-width: none; }
  .head-actions::-webkit-scrollbar { display: none; }
  .bank {
    top: 40px;
    bottom: auto;
    z-index: 29;
    border-top: 0;
    border-bottom: 1px solid var(--panel-edge);
    padding-bottom: 0;
  }
  body:not(.keys-open) .bank { padding-bottom: 0; }
  #surface { padding-bottom: calc(7px + var(--keys-h, 0px)); }
  .rack-editor[data-mode="drawer"] { bottom: var(--keys-h, 0px); }
  .bank-read { justify-content: flex-start; gap: 7px; padding-inline: 4px; }
  .bank-name { flex-basis: auto; }
  .bank-step { padding-inline: 5px; }
  .bank-vol input[type="range"] { width: 52px; }
  .keyboard { bottom: 0; }
}
