/* AURACLE — a full instrument: menu bar, bank, staged views, keyboard dock.
   Two-phosphor language: green = audio, amber = the model's mind.

   The token layer below is load-bearing and has three laws:

   1. TEXT TIER vs STROKE TIER. Each phosphor exists twice. `--phos-*-dim` is
      the *text* tier and clears 4.5:1 on the rack; `--phos-*-deep` is the
      *stroke* tier for wire glow and jack rings, where contrast rules don't
      apply and the darker value looks right. One token cannot do both jobs —
      trying to made every dimmed label in the app fail AA, including the
      lineage log, which is the copy that explains what evolution actually did.
   2. TRACKING LAW. Uppercase gets tracking. Lowercase and mono get none.
   3. NO OFF-SYSTEM COLOUR. Every accent is signal-green or mind-amber. Slate
      blues and pinks read as "generic dark app" and dissolve the phosphor law. */

/* Self-hosted so the identity isn't a coin flip on the user's OS — see
   fonts/README.md for why the old local-only stack silently failed. */
@font-face {
  font-family: "Jost";
  src: url("fonts/jost-var.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("fonts/plexmono-400.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("fonts/plexmono-600.woff2") format("woff2");
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: "Newsreader";
  src: url("fonts/newsreader-it.woff2") format("woff2-variations");
  font-weight: 300 600;
  font-style: italic;
  font-display: swap;
}

/* The colors, font families, type scale, spacing, radii and motion come from
   www/brand/tokens.json, one source for every surface (`make tokens`). `make
   dev-check` fails on a color written anywhere else in this file, and on a
   rise in its count of literal font sizes, spacings, radii and durations (a
   literal the scale has no step for says why, with `token-exempt:`). */
/* tokens:begin: generated from www/brand/tokens.json by `make tokens`. Edit the JSON, not this block. */
:root {
  /* grounds */
  --rack: #0c0d10;     /* the void behind the rack */
  --panel: #171a1f;    /* graphite faceplate */
  --panel-hi: #1d2127; /* raised surface */
  --bezel: #07080a;    /* screen bezels */
  --hairline: #292e36;

  /* machined graphite: the two faces of every raised control and the ink of
     the shadow under it, one literal each, so a retune is one line */
  --plate-hi: #232830;     /* util buttons, selects: top of the face */
  --plate-lo: #181b21;     /*   …and the bottom */
  --btn-hi: #2a2f37;       /* hardware buttons and pads: top of the face */
  --btn-lo: #1a1d23;       /*   …and the bottom */
  --btn-hover-hi: #333944; /* the same face under a pointer: one step up */
  --btn-hover-lo: #1e2228;
  --shadow-ink: #0a0b0d;   /* the hard shadow line under a raised control */
  --knob-body: #22262d;    /* PERFORM's knob caps */

  /* shared shades: one value, used on more than one surface */
  --rack-lo: #0a0b0e;   /* below the rack: the landing page's foot, the docs' sidebar */
  --panel-lo: #12151a;  /* the foot of a raised panel; the docs' quotes and search results */
  --pop-lo: #14161a;    /* the foot of a floating surface and of the menu bar; the docs' pop-ups */
  --recess-hi: #14171b; /* the top of a recessed panel (bank, node bank); a screw's shadow; an empty tile */
  --lifted: #1a1e24;    /* a lifted strip: a held tray item's top, a table's header row */
  --amber-off: #1a1410; /* an unlit amber cell: pips, lock dots */
  --steel: #3a3f47;     /* bare steel: a screw head's highlight; the docs' scrollbar */

  /* silk: warm silkscreen ink */
  --silk: #d9d4c8;      /* 13.1:1 on rack */
  --silk-dim: #9a958a;  /*  5.9:1: all dimmed TEXT */
  --silk-mute: #6f6c63; /*  non-text only: hairlines, disabled glyphs */

  /* green phosphor: sound. `-dim` is the text tier, `-deep` the stroke tier */
  --phos-a: #8ef0b1;      /* 14.1:1 */
  --phos-a-dim: #63a97c;  /*  5.2:1: knob values, readouts */
  --phos-a-deep: #3d6a4d; /*  strokes and fills only */

  /* amber phosphor: the model's mind */
  --phos-b: #ffb454;      /* 11.0:1 */
  --phos-b-dim: #b8823c;  /*  5.4:1: lineage log, notes */
  --phos-b-deep: #7a5526; /*  strokes and fills only */

  /* danger */
  --led-red: #ff5747;

  /* light and shadow: never ink */
  --black: #000000; /* shadow lines, the edge under a raised control, masks */
  --white: #ffffff; /* highlights only, at an opacity; as ink it reads cold against silk */

  /* app: shades only this surface uses */
  --jack-bore: #050607;           /* the hole in a jack */
  --well: #0a0c0e;                /* a recessed SVG well: an enum's body, a step's track */
  --tray-jack: #0a0c0a;           /* a jack's face in the rack */
  --scrim: #0a0c0f;               /* the veil under an overlay; only at an opacity */
  --rack-map-glass: rgba(6, 8, 10, 0.82); /* the rack's minimap */
  --mod-tab-fill: rgba(10, 7, 3, 0.55); /* a module's modulation tab */
  --mod-tab-filled: rgba(24, 16, 5, 0.75); /*   …with a source in it */
  --mm-pip-rim: rgba(10, 8, 5, 0.85); /* the minimap's pip */
  --mm-pip-ink: #120c04;          /*   …and its number */
  --rack-bed-hi: #0b0c0f;         /* the rack's ground: top (also the exported rack's) */
  --rack-bed-lo: #0e1013;         /*   …and foot; the XY field's */
  --xport-hi: #1a2620;            /* the transport button: lit side */
  --xport-lo: #0b0f0d;            /*   …shadow side, and its ink when playing */
  --recess-lo: #101216;           /* the foot of a recessed panel; a knob's shadow side */
  --spec-dock-lo: #101317;        /* the spec dock's foot */
  --ghost-plate: #10131a;         /* a module plate being placed */
  --crt-green: #10160f;           /* the green glow at a scope's centre; a tray jack's bore */
  --crt-amber: #171107;           /* the amber glow at an amber screen's centre */
  --keybar-lo: #121419;           /* the keyboard dock's foot */
  --module-hi: #22272f;           /* a module plate in the rack: top */
  --module-lo: #13161a;           /*   …and foot; a tray item's foot */
  --knob-hi: #3b414c;             /* a rack knob: lit side */
  --knob-mid: #20242b;            /*   …and middle (the foot is --recess-lo) */
  --nut-hi: #4a505b;              /* a jack nut and a screw head: lit side; a step bar under the pointer; a tray item's edge */
  --nut-lo: #1a1d22;              /*   …and shadow side */
  --chip: #14171c;                /* a style chip; a plug's boot */
  --play-duel-glass: #14171cd9;   /* the duel strip under PATCH */
  --play-duel-edge: #262b33;      /*   …and its top edge */
  --pad-off-lo: #15171c;          /* a disabled pad's foot */
  --card-hi: #1f242b;             /* a card: node-bank item, warm-start item, duel cell */
  --card-lo: #15181d;             /*   …and its foot */
  --pd-side: #191d24;             /* a side of PATCH's duel */
  --menubar-hi: #1c2026;          /* the menu bar's top */
  --pop-hi: #1e232a;              /* a floating surface's top */
  --toast-hi: #21262e;            /* a toast's top */
  --tray-empty-edge: #1c2027;     /* an empty tray's edge */
  --tray-edge: #3a4048;           /* the tray's dashed edge */
  --kbd-edge: #333a45;            /* a printed key's edge: Z and X on the keys bar */
  --plate-edge: #2c313a;          /* the edge of a plate or well; the scrollbar's thumb */
  --screw-rim: #30343c;           /* a screw's rim on the menu bar and the dock */
  --knob-ring: #343a44;           /* a rack knob's ring */
  --knob-tick: #3a4049;           /* a knob's scale ticks */
  --port-rim: #3a3f48;            /* a module port's rim */
  --modside-edge: #57431f;        /* a modulation module's plate edge */
  --lock-dot-rim: #3a2f1d;        /* an unlit lock dot's rim */
  --led-off: #2a1512;             /* an unlit red LED */
  --nofit-a: #241b12;             /* a bank pip the model cannot score: stripe */
  --nofit-b: #3a2c1a;             /*   …and the other stripe */
  --evolve-hi: #ffc978;           /* EVOLVE FROM THIS: top of the amber face */
  --evolve-lo: #d18f2c;           /*   …bottom */
  --evolve-edge: #6b4a12;         /*   …edge */
  --evolve-cast: #3a2708;         /*   …the hard shadow under it */
  --evolve-ink: #1a1204;          /*   …its label */
  --alarm-hi: #ffc774;            /* an alarm toast's amber face: top */
  --alarm-ink: #21160a;           /*   …its words */
  --ix-go-hi: #3a2a12;            /* an amber go button: top */
  --ix-go-lo: #241a0c;            /*   …and foot */
  --ix-go-hover-hi: #4a3517;      /*   …under a pointer: top */
  --ix-go-hover-lo: #2c200e;      /*   …and foot */
  --bkey-hi: #23262c;             /* a black key: top (the foot is --rack) */
  --bkey-down-hi: #2f7a4d;        /* a black key held: top */
  --bkey-down-lo: #174a2c;        /*   …and foot */
  --pkey-hi: #e8e4da;             /* a white key: top */
  --pkey-mid: #cfcabd;            /*   …lip */
  --pkey-lo: #b7b2a5;             /*   …foot */
  --pkey-mapped-hi: #f6efdc;      /* a white key with a computer key on it: top */
  --pkey-mapped-mid: #e2d9c2;     /*   …lip */
  --pkey-mapped-lo: #c9c0a9;      /*   …foot */
  --pkey-unmapped-hi: #d8d4ca;    /* a white key without one: top */
  --pkey-unmapped-mid: #bfbaae;   /*   …lip */
  --pkey-unmapped-lo: #a7a296;    /*   …foot */
  --pkey-down-hi: #9be8b6;        /* a white key held: top */
  --pkey-down-lo: #6fcf95;        /*   …foot */
  --pkey-edge: #55524a;           /* the line between white keys */
  --pkey-hint: #4a473f;           /* a white key's printed computer key */
  --pkey-hint-key: #3a372f;       /*   …its letter */
  --pkey-down-hint: #14532b;      /*   …while the key is held */

  /* opacities: `--phos-b-30` is --phos-b at 30%; a third digit is a tenth
     (`--phos-a-045` is 4.5%) */
  --white-012: rgba(255, 255, 255, 0.012);
  --white-02: rgba(255, 255, 255, 0.02);
  --white-04: rgba(255, 255, 255, 0.04);
  --white-05: rgba(255, 255, 255, 0.05);
  --white-06: rgba(255, 255, 255, 0.06);
  --white-08: rgba(255, 255, 255, 0.08);
  --white-09: rgba(255, 255, 255, 0.09);
  --white-13: rgba(255, 255, 255, 0.13);
  --white-18: rgba(255, 255, 255, 0.18);
  --white-35: rgba(255, 255, 255, 0.35);
  --black-25: rgba(0, 0, 0, 0.25);
  --black-30: rgba(0, 0, 0, 0.3);
  --black-35: rgba(0, 0, 0, 0.35);
  --black-45: rgba(0, 0, 0, 0.45);
  --black-50: rgba(0, 0, 0, 0.5);
  --black-55: rgba(0, 0, 0, 0.55);
  --black-60: rgba(0, 0, 0, 0.6);
  --black-62: rgba(0, 0, 0, 0.62);
  --black-70: rgba(0, 0, 0, 0.7);
  --black-75: rgba(0, 0, 0, 0.75);
  --black-85: rgba(0, 0, 0, 0.85);
  --black-90: rgba(0, 0, 0, 0.9);
  --phos-b-06: rgba(255, 180, 84, 0.06);
  --phos-b-07: rgba(255, 180, 84, 0.07);
  --phos-b-08: rgba(255, 180, 84, 0.08);
  --phos-b-10: rgba(255, 180, 84, 0.1);
  --phos-b-12: rgba(255, 180, 84, 0.12);
  --phos-b-14: rgba(255, 180, 84, 0.14);
  --phos-b-16: rgba(255, 180, 84, 0.16);
  --phos-b-18: rgba(255, 180, 84, 0.18);
  --phos-b-20: rgba(255, 180, 84, 0.2);
  --phos-b-22: rgba(255, 180, 84, 0.22);
  --phos-b-25: rgba(255, 180, 84, 0.25);
  --phos-b-30: rgba(255, 180, 84, 0.3);
  --phos-b-34: rgba(255, 180, 84, 0.34);
  --phos-b-35: rgba(255, 180, 84, 0.35);
  --phos-b-40: rgba(255, 180, 84, 0.4);
  --phos-b-45: rgba(255, 180, 84, 0.45);
  --phos-b-55: rgba(255, 180, 84, 0.55);
  --phos-b-60: rgba(255, 180, 84, 0.6);
  --phos-b-62: rgba(255, 180, 84, 0.62);
  --phos-b-70: rgba(255, 180, 84, 0.7);
  --phos-b-75: rgba(255, 180, 84, 0.75);
  --phos-b-80: rgba(255, 180, 84, 0.8);
  --phos-b-85: rgba(255, 180, 84, 0.85);
  --phos-b-90: rgba(255, 180, 84, 0.9);
  --phos-a-06: rgba(142, 240, 177, 0.06);
  --phos-a-07: rgba(142, 240, 177, 0.07);
  --phos-a-09: rgba(142, 240, 177, 0.09);
  --phos-a-10: rgba(142, 240, 177, 0.1);
  --phos-a-12: rgba(142, 240, 177, 0.12);
  --phos-a-15: rgba(142, 240, 177, 0.15);
  --phos-a-16: rgba(142, 240, 177, 0.16);
  --phos-a-18: rgba(142, 240, 177, 0.18);
  --phos-a-24: rgba(142, 240, 177, 0.24);
  --phos-a-25: rgba(142, 240, 177, 0.25);
  --phos-a-30: rgba(142, 240, 177, 0.3);
  --phos-a-34: rgba(142, 240, 177, 0.34);
  --phos-a-35: rgba(142, 240, 177, 0.35);
  --phos-a-45: rgba(142, 240, 177, 0.45);
  --phos-a-50: rgba(142, 240, 177, 0.5);
  --phos-a-55: rgba(142, 240, 177, 0.55);
  --phos-a-60: rgba(142, 240, 177, 0.6);
  --phos-a-70: rgba(142, 240, 177, 0.7);
  --phos-a-72: rgba(142, 240, 177, 0.72);
  --phos-a-80: rgba(142, 240, 177, 0.8);
  --phos-a-85: rgba(142, 240, 177, 0.85);
  --phos-a-90: rgba(142, 240, 177, 0.9);
  --rack-55: rgba(12, 13, 16, 0.55);
  --rack-96: rgba(12, 13, 16, 0.96);
  --led-red-12: rgba(255, 87, 71, 0.12);
  --led-red-50: rgba(255, 87, 71, 0.5);
  --led-red-80: rgba(255, 87, 71, 0.8);
  --recess-hi-00: rgba(20, 23, 27, 0);
  --shadow-ink-60: rgba(10, 11, 13, 0.6);
  --scrim-82: rgba(10, 12, 15, 0.82);
  --scrim-90: rgba(10, 12, 15, 0.9);
  --scrim-94: rgba(10, 12, 15, 0.94);
  --bezel-72: rgba(7, 8, 10, 0.72);
  --panel-94: rgba(23, 26, 31, 0.94);

  /* families */
  --font-silk: "Jost", "Futura", "Avenir Next", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --font-voice: "Newsreader", Georgia, "Times New Roman", serif;

  /* type: one ratio (1.2) and six roles. The steps run up from the value size,
     12 × 1.2ⁿ rounded: 12, 14, 17, 21, and 52 at n = 8. Labels sit under the
     scale at 11 px, the floor for text in a page; the rack, drawn to its zoom,
     has sizes of its own, and a lone glyph is sized to its button */
  --t-label: 11px;   /* silk caps: panel labels, tabs, buttons; the floor for text */
  --t-value: 12px;   /* mono values, readouts, small print */
  --t-body: 14px;    /* prose, names, hints */
  --t-voice: 17px;   /* the model speaking (Newsreader italic), and nothing else */
  --t-title: 21px;   /* a title: a sound's name, a card's heading, the wordmark */
  --t-display: 52px; /* a level's one headline */
  --t-canvas: 12px;  /* the smallest text drawn on a canvas (TA20 found 10 px too small) */

  /* space: a 4 px grid to 16, then wider. Under the first step (1 to 3 px) is
     an optical nudge, not a space, and is written as it is */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;

  /* radius: three. A circle is 50% and a pill 999px, shapes rather than steps;
     1 to 3 px is a hairline's rounding */
  --r1: 4px;  /* a row, a chip, a small control */
  --r2: 8px;  /* a button, a pad, a card */
  --r3: 14px; /* the well, a sheet */

  /* motion: four durations and two easings. Under prefers-reduced-motion every
     duration is 0 ms. A loop's period is not one of these */
  --d-press: 90ms;                /* a press: a control going down, a row lighting */
  --d-state: 180ms;               /* a state changing: a color, a hover, a reveal */
  --d-move: 320ms;                /* something moving: a view, a panel, a card arriving */
  --d-zoom: 620ms;                /* a move between the levels, the sound's face carried across (the specimen's morph) */
  --e-settle: cubic-bezier(0.2, 0.8, 0.2, 1); /* arriving and coming to rest */
  --e-swap: cubic-bezier(0.6, 0, 0.2, 1); /* one thing giving way to another */

  /* app: sizes only this surface uses */
  --t-rack-micro: 9px;  /* the rack at zoom 1 (its camera scales all four; main.js's module geometry is sized around them): mono numerals, in/out */
  --t-rack-label: 10px; /*   …knob labels, step numbers, mod tabs */
  --t-rack-value: 11px; /*   …readouts, enum chips, a narrow plate's title */
  --t-rack-title: 13px; /*   …plate titles */
  --t-wordmark: 17px;   /* the header's wordmark, spaced wide beside its lamp (Plan-008, the prototype's) */
}
@media (prefers-reduced-motion: reduce) {
  :root { --d-press: 0ms; --d-state: 0ms; --d-move: 0ms; --d-zoom: 0ms; }
}
/* tokens:end */

:root {
  /* The type scale, spacing, radii and motion are in the block above, from
     www/brand/tokens.json. The rack's own type tier (--t-rack-*) is there
     too. The rack draws through a camera the player aims (0.3×–2.5×, fitted
     to the frame on load and capped at 2.2× there), so its sizes are the
     rendered sizes at the *worst legible* case, zoom 1, and everything above
     1× is a bonus. They are separate tokens rather than the page's because
     the module geometry (MOD_W / COL_W / KNOB_ROW in main.js) is sized
     around them. Its 9px tier is deliberately under the page's floor, and
     the exception is argued rather than hidden: it carries mono *numerals*
     (`1.3 ms`, `−22.7 dB`) with uniform advance widths, read as values
     rather than prose, and `.jack text` shares it for `in`/`out`, port
     symbols from a closed set of two rather than words. The rack is NOT
     always magnified (a patch of any size fits at 1× or below, and the
     player can zoom further out than that), so treat 9px as the real
     rendered size and not a best case. */

  /* light comes from 315° everywhere */
  --lift: inset 1px 1px 0 var(--white-08);
  --cast: 0 3px 8px var(--black-50);

  /* one surface + one shadow for every floating panel — three popovers had
     three backgrounds, two border colours and three shadows */
  --surface-pop: linear-gradient(180deg, var(--pop-hi), var(--pop-lo));
  --shadow-pop: var(--lift), 0 14px 34px var(--black-62);

  /* fixed chrome heights, so nothing hand-tunes an offset to them. The menu
     bar takes its height from this (`min-height`), and type_scale.spec.js
     measures that it does: at 44px it was 49px tall, and the alarm placed
     under it overlapped it. */
  --menubar-h: 56px;
  --keybar-h: 78px;

  /* Never let the UA leak someone else's design system: dark form controls,
     amber selection instead of system blue. */
  color-scheme: dark;
}

::selection { background: var(--phos-b-30); color: var(--silk); }

.bank-list, .nb-body, .ctx-menu, .tray-items {
  scrollbar-width: thin;
  scrollbar-color: var(--hairline) transparent;
}
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-thumb {
  background: var(--plate-edge);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-corner, ::-webkit-scrollbar-track-piece { background: transparent; }

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body { height: 100%; overflow: hidden; }

body {
  background: var(--rack);
  color: var(--silk);
  font-family: var(--font-silk);
  font-size: var(--t-body);
  -webkit-font-smoothing: antialiased;
}

/* Form controls do not inherit type. Without this, every button, input and
   select in the app renders in the UA default — which was 284 elements in
   Arial, including all 200 bank stars. */
button, input, select, textarea { font: inherit; color: inherit; }

.mono { font-family: var(--font-mono); letter-spacing: 0; }
.hidden { display: none !important; }
/* Sentence-case text takes no tracking (law 2). */
.silk-dim { color: var(--silk-dim); font-weight: 400; text-transform: none; }

.panel-label {
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.panel-label.amber { color: var(--phos-b); }
/* Tracked caps at 1× on Linux: hinting rounds a Jost "T" up to a whole
   pixel, and with the tracking these carry that opens a gap after it — "ST
   YLES", "T RUST", "PATC H". Fractional (unhinted) advances close it, and
   run a few pixels narrower. Only the tracked caps: on running text the same
   switch re-wraps paragraphs, and there is no gap there to close. */
.panel-label, .tab, .counter-label, .util-btn, .hw-btn, .toggle,
.pc-cap, .pf-k-name, .pf-pad, .pf-xy-cap, .pf-hood-h,
.sp-title, .midi-h, .toast-undo, .tray-label, .ce-title {
  text-rendering: geometricPrecision;
}

/* ===================== app frame ===================== */
/* Plan-008: a quiet graphite frame, the bar, the bank, the stage and the
   keys, around the stage, where the levels are. The deck wrapper takes no box
   of its own (`display: contents`), so the bank and the stage are the grid's
   cells. The bank sets its own width (280 px, 228 at 1080 and under). */
.app {
  height: 100vh;
  display: grid;
  grid-template-areas: "bar bar" "bank stage" "keys keys";
  grid-template-rows: var(--menubar-h) minmax(0, 1fr) var(--keybar-h);
  grid-template-columns: auto minmax(0, 1fr);
  overflow: hidden;
}
.deck { display: contents; }
.menubar { grid-area: bar; }
.bank { grid-area: bank; }
.stage { grid-area: stage; }
.keybar { grid-area: keys; }

/* ---- the bar ----
   One row at every width: the wordmark and its lamp, where you are, the film
   of this level, long work while it runs, the sound in hand, TAUGHT, and
   ? and ⋯ (until the ⌘K list takes them in, Plan-008 PR D). What gives way
   on a narrower window: the film's greeting at 1320, TAUGHT's word at 1240,
   the level's line and the sound's name at 980, the level at 560. */
.menubar {
  position: relative;
  z-index: 5;
  height: var(--menubar-h);
  display: flex;
  align-items: center;
  gap: var(--s5);
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  padding: 0 var(--s4) 0 var(--s5);
  border-bottom: 1px solid var(--hairline);
  background: var(--panel);
}
.brand {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--s2);
  color: var(--silk);
  text-decoration: none;
}
.brand .word {
  font-size: var(--t-wordmark);
  font-weight: 500;
  line-height: 1;
  /* The prototype's spacing: wide, so the word reads as a mark beside its
     lamp; the last letter's tracking is taken back so the lamp sits at the
     same gap as the letters. */
  letter-spacing: 0.34em;
  margin-right: -0.34em;
}
/* The model's lamp: dark amber at rest, lit while it works (main.js,
   `renderJobSlot`: a generation, ⚡, a refit), breathing while lit. */
.lamp {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--phos-b-deep);
  box-shadow: inset 0 0 0 1px var(--black-50);
  transition: background var(--d-state), box-shadow var(--d-state);
}
.lamp.thinking {
  background: var(--phos-b);
  box-shadow: 0 0 10px var(--phos-b-35), 0 0 2px var(--phos-b);
  animation: listen 1.1s ease-in-out infinite; /* token-exempt: a loop's period, not a transition */
}
@keyframes listen { 50% { opacity: 0.55; } }

/* Where you are (shell.js): the level's name in silk caps, and what it is. */
.where {
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  height: 22px;
  margin-left: var(--s2);
  padding-left: var(--s5);
  border-left: 1px solid var(--hairline);
}
.where-n {
  flex: none;
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk);
  text-rendering: geometricPrecision;
}
.where-d {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: var(--t-value);
  color: var(--silk-dim);
}

/* The sound in hand: its face, its name, and ▶ (Space). */
.inhand {
  flex: 0 1 auto;
  min-width: 0;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s2);
  height: 38px;
  padding: 3px var(--s3) 3px var(--s1);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: var(--rack);
}
.inhand-face { flex: none; display: grid; place-items: center; width: 30px; height: 30px; }
.inhand .live-slot { min-width: 0; }
.inhand .live-label {
  max-width: 150px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--silk);
}
/* Space waiting for an edit outside PATCH (main.js `paintLiveLabel`): said in
   the name's place, in the amber of ▶'s waiting ring, whole: the chip grows
   to the left for it, and nothing right of it moves. */
.inhand .live-slot.waiting .live-label { display: none; }
.live-wait { font-size: var(--t-value); color: var(--phos-b); white-space: nowrap; }
.live-wait:empty { display: none; }
.inhand-play {
  flex: none;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border: 1px solid transparent;
  border-radius: 50%;
  background: none;
  color: var(--phos-a);
  cursor: pointer;
}
.inhand-play svg { width: 24px; height: 24px; fill: currentColor; }
.inhand-play .i-stop { display: none; }
.inhand-play.playing .i-play { display: none; }
.inhand-play.playing .i-stop { display: block; }
.inhand-play:hover:not(:disabled) { background: var(--plate-hi); }
.inhand-play:disabled { color: var(--silk-mute); cursor: default; }
/* Waiting for an edit to land, as the rack's ▶ does (`.xport.pending`). */
.inhand-play.pending { border: 1px dotted var(--phos-b); color: var(--phos-b); }

/* TAUGHT: the word in silk caps, the count in the model's amber. */
.counter { font-family: var(--font-mono); font-size: var(--t-body); font-weight: 500; color: var(--phos-b); white-space: nowrap; font-variant-numeric: tabular-nums; }
.counter-label { color: var(--silk-dim); font-family: var(--font-silk); font-size: var(--t-label); font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase; margin-right: var(--s1); }
.taught { flex: none; }

/* No keys under a finger: MODEL's ⌥ hint goes, as the other key hints do. */
@media (pointer: coarse) { .model-btn kbd { display: none; } }
/* MODEL: the model view's pill (shell.js). Its LED is the model's amber,
   dark at rest and lit while the view is up, however it came up (held or
   tapped); tapped on, the pill is pressed too. */
.model-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  height: 30px;
  padding: 0 var(--s2) 0 var(--s3);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: none;
  cursor: pointer;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk-dim);
  text-rendering: geometricPrecision;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  transition: color var(--d-state), border-color var(--d-state);
}
.model-btn:hover { color: var(--silk); border-color: var(--plate-edge); }
.model-btn .led {
  width: 7px;
  height: 7px;
  background: var(--phos-b-deep);
  box-shadow: none;
  transition: background var(--d-state), box-shadow var(--d-state);
}
.model-btn kbd {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: 0;
  color: var(--silk-dim);
  padding: 0 var(--s1);
  border: 1px solid var(--hairline);
  border-radius: 3px;
  line-height: 1.3;
}
.model-btn[aria-pressed="true"], body.model-view .model-btn { color: var(--phos-b); border-color: var(--phos-b-deep); }
.model-btn[aria-pressed="true"] .led, body.model-view .model-btn .led { background: var(--phos-b); box-shadow: 0 0 10px var(--phos-b-60); }
/* ⌥ (or MODEL) held, not yet for long enough to raise the view: the LED
   half-lit, from the keydown (shell.js `arming`). */
.model-btn.arming .led { background: var(--phos-b-60); }
/* The model view's tag: what it believes, in the model's voice, under the
   bar while the view is up. A tapped view says it once and the tag settles
   away (`model-quiet`); the amber stays. */
.model-tag {
  position: fixed;
  top: calc(var(--menubar-h) + var(--s2));
  left: 50%;
  z-index: 31;
  display: flex;
  align-items: center;
  gap: var(--s2);
  max-width: calc(100vw - var(--s6));
  padding: var(--s1) var(--s3);
  border: 1px solid var(--phos-b-deep);
  border-radius: 999px;
  background: var(--crt-amber);
  color: var(--phos-b);
  opacity: 0;
  transform: translate(-50%, -6px);
  pointer-events: none;
  transition: opacity var(--d-state), transform var(--d-state) var(--e-settle);
}
.model-tag .led { width: 7px; height: 7px; background: var(--phos-b); box-shadow: 0 0 8px var(--phos-b-60); }
.model-tag .mt-voice { font-family: var(--font-voice); font-style: italic; font-size: var(--t-voice); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
body.model-view .model-tag { opacity: 1; transform: translate(-50%, 0); }
body.model-view.model-quiet .model-tag { opacity: 0; transform: translate(-50%, -4px); }


/* ⌘K in the bar (the mock's): a glass, the words, its key. The words go
   under 1240 px, as the mock's do, and the key on a touch screen. */
.cmdk-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  height: 34px;
  padding: 0 var(--s2) 0 var(--s3);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: var(--rack);
  color: var(--silk-dim);
  font-size: var(--t-value);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--d-state), border-color var(--d-state);
}
.cmdk-btn:hover, .cmdk-btn[aria-expanded="true"] { color: var(--silk); border-color: var(--plate-edge); }
.cmdk-glass { width: 15px; height: 15px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
.cmdk-btn kbd, .cmdk-hint kbd, .cmdk-foot kbd {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--silk-dim);
  padding: 0 var(--s1);
  border: 1px solid var(--hairline);
  border-radius: 3px;
  line-height: 1.4;
}
@media (max-width: 1240px) { .cmdk-btn .cmdk-word { display: none; } }
@media (pointer: coarse) { .cmdk-btn kbd, .cmdk-hint kbd, .cmdk-foot > span:not(.cmdk-fine) { display: none; } }

/* The job slot: long work, only while it runs. Amber, because it is the
   model's work; the fill is the share done; stop is a small raised button.
   It stands just left of the sound in hand. */
.job-slot {
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  flex: 0 1 auto;
  min-width: 0;
  max-width: 340px;
  margin-left: auto;
  padding: 3px var(--s1) 3px var(--s2);
  border: 1px solid var(--phos-b-deep);
  border-radius: 999px;
  color: var(--phos-b);
  font-size: var(--t-label);
  white-space: nowrap;
}
.job-slot:not(.hidden) + .inhand { margin-left: 0; }
.job-slot.hidden { display: none; }
.job-slot .job-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: var(--phos-b-12);
  transition: width var(--d-move) ease;
  pointer-events: none;
}
.job-slot > .job-text, .job-slot > .job-stop { position: relative; }
.job-slot .job-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.job-slot .job-stop {
  flex: none;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--silk);
  background: linear-gradient(180deg, var(--btn-hi), var(--btn-lo));
  border: 1px solid var(--shadow-ink);
  border-radius: 999px;
  padding: 3px var(--s2);
  cursor: pointer;
}
.job-slot .job-stop.hidden { display: none; }
.job-slot .job-stop:hover { color: var(--phos-b); }
.job-slot .job-stop:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .job-slot .job-fill { transition: none; } }
@media (max-width: 1240px) { .job-slot { max-width: 270px; } }

/* The film of this level (main.js, pointFilmChip), folded after the level's
   name. Folded, a quiet "▶ film"; open, on a level's first visit, the one
   line a newcomer needs, in the phosphor of sound, with a way to fold it. */
.film-chip { display: flex; align-items: center; gap: 2px; flex: 0 1 auto; min-width: 0; margin-left: calc(-1 * var(--s3)); }
.fc-link {
  display: inline-flex;
  align-items: baseline;
  gap: var(--s1);
  min-width: 0;
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--silk-dim);
  text-decoration: none;
  white-space: nowrap;
  padding: 3px var(--s2);
  border: 1px solid transparent;
  border-radius: 3px;
}
.fc-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.fc-play { color: var(--phos-a-dim); }
.fc-link:hover, .fc-link:focus-visible { color: var(--phos-a); border-color: var(--phos-a-dim); }
.fc-link:hover .fc-play, .fc-link:focus-visible .fc-play { color: var(--phos-a); }
.film-chip.open .fc-link {
  color: var(--phos-a);
  border-color: var(--phos-a-dim);
  background: var(--phos-a-06);
  box-shadow: 0 0 10px var(--phos-a-12);
  animation: fc-arrive var(--d-move) ease-out;
}
.film-chip.open .fc-play { color: var(--phos-a); }
.fc-lead { color: var(--silk); }
.fc-close {
  display: none;
  background: none;
  border: none;
  border-radius: 3px;
  color: var(--silk-dim);
  font-size: var(--t-body);
  line-height: 1;
  padding: 2px var(--s1);
  cursor: pointer;
}
.film-chip.open .fc-close { display: inline-block; }
.fc-close:hover, .fc-close:focus-visible { color: var(--silk); }
@keyframes fc-arrive { from { opacity: 0; transform: translateY(-3px); } }
@media (prefers-reduced-motion: reduce) { .film-chip.open .fc-link { animation: none; } }
/* A narrower bar keeps the film's name and length and drops the greeting. */
@media (max-width: 1320px) { .fc-lead { display: none; } }

/* The prototype's own narrow rules, on the bar's one row. */
@media (max-width: 1240px) {
  .menubar { gap: var(--s4); }
  .taught .counter-label { display: none; }
  /* The mock's narrow MODEL: the LED and ⌥. */
  .model-btn .mb-word { display: none; }
}
@media (max-width: 980px) {
  .menubar { gap: var(--s3); padding: 0 var(--s3) 0 var(--s4); }
  .where-d, .inhand .live-label { display: none; }
}
@media (max-width: 560px) {
  /* A phone: the bar keeps the wordmark, the sound in hand, its count and
     ⌘K (its glass alone). */
  .where { display: none; }
  .menubar { gap: var(--s2); }
}

/* Same physics as .hw-btn at reduced amplitude — a tool, not a web chip.
   Without the cast and the depress, PLAY read as a web app with a synth
   pasted in the middle. */
.util-btn {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--silk);
  background: linear-gradient(180deg, var(--plate-hi), var(--plate-lo));
  border: 1px solid var(--black);
  border-radius: 3px;
  padding: var(--s1) var(--s2);
  cursor: pointer;
  box-shadow: var(--lift), 0 2px 0 var(--shadow-ink);
  transition: transform var(--d-press) ease, box-shadow var(--d-press) ease;
}
.util-btn:hover { border-color: var(--silk-dim); }
.util-btn:active {
  transform: translateY(1px);
  box-shadow: inset 0 1px 0 var(--white-05);
}
.util-btn.amber { color: var(--phos-b); border-color: var(--phos-b-dim); }
.util-btn.amber:hover { box-shadow: 0 0 10px var(--phos-b-25); }
.util-btn:disabled { opacity: 0.4; cursor: default; }
.util-btn.lit {
  color: var(--phos-b);
  border-color: var(--phos-b);
  box-shadow: 0 0 10px var(--phos-b-30), inset 0 0 6px var(--phos-b-12);
}
.util-btn.sq { width: 30px; padding: var(--s1) 0; text-align: center; position: relative; }

.led-block { display: flex; align-items: center; gap: var(--s1); font-size: var(--t-label); letter-spacing: 0.18em; text-transform: uppercase; color: var(--silk-dim); }
.led {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--led-off);
  box-shadow: inset 0 0 2px var(--black);
}
.led.on {
  background: var(--led-red);
  box-shadow: 0 0 8px var(--led-red), 0 0 2px var(--led-red);
  animation: led-pulse 900ms ease-in-out infinite; /* token-exempt: a loop's period, not a transition */
}
@keyframes led-pulse { 50% { opacity: 0.55; } }

/* ---- the bank ---- */
/* To the mock (Plan-008 PR B, prototype `style.css` 136–178): the head holds
   three tabs and Find a sound; a row is the face, the name and, on approach,
   its actions. Every rule for the bank lives in this block. */

.bank {
  position: relative; /* containing block for .bank-tour, which anchors to
                         the rail it describes rather than to the page */
  /* 252px of rows, and the faces' column: the column is added, not taken
     from the names (Plan-005, a mark never narrows a name). */
  width: 280px;
  flex: 0 0 280px;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--hairline);
  background: var(--panel);
  min-height: 0;
}
.bank-head {
  display: grid;
  /* The bank's width, whatever a field's own width would ask (a finger's
     16 px find field asked 20 characters of it, and pushed the tabs past
     the bank's edge). */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s3);
  padding: var(--s4) var(--s4) var(--s3);
}
/* The three banks: one tab stop, the arrows between them. */
.bank-tabs { display: flex; gap: 2px; }
.btab {
  flex: 1;
  min-width: 0;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s1);
  padding: 0 var(--s1);
  border: 1px solid transparent;
  border-radius: var(--r1);
  background: none;
  cursor: pointer;
  white-space: nowrap;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--silk-dim);
  text-rendering: geometricPrecision;
  transition: background var(--d-press), color var(--d-press);
}
.btab:hover { color: var(--silk); }
.btab[aria-selected="true"] { background: var(--plate-hi); color: var(--silk); }
/* How many are in a bank you are not looking at. */
.btab .bt-n {
  font-family: var(--font-mono);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--silk-dim);
}
/* SAVED at the save cap: the one state of the budget worth seeing without
   hovering (its title says the numbers). */
.btab .bt-n.cap { color: var(--phos-b); }
/* POOL's "+N": sounds still on their way in (`renderFillHint`). */
.btab .bt-fresh { font-family: var(--font-mono); font-weight: 400; letter-spacing: 0; color: var(--phos-a-dim); }
.btab .bt-fresh:empty { display: none; }
/* The tab the walkthrough's step is about. */
.btab.tour-lit { box-shadow: 0 0 0 1px var(--phos-b), 0 0 0 3px var(--phos-b-25); }

/* Find a sound, and the walkthrough's quiet ? at the end of its row. */
.bank-find-row { display: flex; align-items: center; gap: var(--s2); }
.bank-find {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--s2);
  height: 32px;
  padding: 0 var(--s2);
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  background: var(--rack);
  color: var(--silk-dim);
  cursor: text;
}
.bank-find:focus-within { border-color: var(--silk-dim); }
.bank-find svg { flex: none; width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
.bank-find input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: none;
  padding: 0;
  font-family: var(--font-silk);
  font-size: var(--t-body);
  color: var(--silk);
}
.bank-find input::placeholder { color: var(--silk-dim); }
.bank-find input::-webkit-search-cancel-button { filter: grayscale(1); opacity: 0.6; }

.bank-list {
  flex: 1;
  overflow-y: auto;
  /* main.js keeps the row under the pointer in place across a redraw
     (`bankKeep`); the browser's own anchoring would move it twice. */
  overflow-anchor: none;
  padding: 0 var(--s2) var(--s4);
  display: flex;
  flex-direction: column;
  gap: 1px;
  scrollbar-width: thin;
  scrollbar-color: var(--plate-edge) transparent;
}

/* A row: the face's column, then two lines, always two. The first is the
   name, which has the row's whole width, and no action or star covers it.
   The marks, which are said about the row and are not pressed, stand over
   its end instead, on this line: the seed and may-be-replaced words, the
   model view's percentage and (under a coarse pointer) IN POOL; the mark
   column keeps the name at one x. The second line is the actions' (a bred
   sound's lineage shares it): their own strip, kept whether they show or
   not, so a row is the height it is at rest, under the pointer and with the
   stars out, and nothing under the hand moves (#312, #534). The name that
   does not fit ends in an ellipsis, its full text in its tooltip. */
.bank-item {
  position: relative;
  flex: none;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: 0;
  min-height: 48px; /* the name's line, then the actions' 22 px strip and its 5 px foot */
  padding: var(--s1) var(--s2) var(--s1) calc(var(--s2) + 26px + var(--s3)); /* token-exempt: the width kept for the face (26 px, the mock's), not spacing */
  border-radius: var(--r1);
  /* A 2px rail on the left carries row state: green for the sound you're
     playing, silk for saved (your own organisation, neither phosphor),
     amber solid and dashed for what EVOLVE POOL would breed from and may
     replace. Selection never depends on a 1px change on a dark ground. */
  border-left: 2px solid transparent;
  cursor: pointer;
  transition: background var(--d-press);
}
.bank-item:hover, .bank-item.kbd, .bank-item.rating { background: var(--panel-hi); }
.bank-item.kbd { box-shadow: inset 0 0 0 1px var(--phos-a-deep); }
.bank-item.saved { border-left-color: var(--silk-dim); }
.bank-item.live { border-left-color: var(--phos-a); }
.bank-item.live .bi-name { color: var(--phos-a); }
.bank-item > .face-slot {
  position: absolute;
  left: var(--s2);
  top: 50%;
  transform: translateY(-50%);
}
.bank-item .bi-top {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
}
/* The mark column, left of the name on every row: the origin glyph, or NEW in
   its place on the latest generation's children, and the unheard dot in the
   gap after it. It has NEW's width whether a mark is there or not, so a name
   starts at the same x, at the same width, with or without one: a mark never
   moves or cuts a name (Plan-005, the rule for a mark on a row). */
.bank-item .bi-mark {
  position: relative;
  flex: none;
  width: 30px;
  margin-right: var(--s1); /* with the row's gap, room for the dot to breathe */
  display: inline-flex;
  align-items: center;
}
.bank-item .bi-origin { font-size: var(--t-label); }
.bank-item .bi-origin.prior { color: var(--silk-dim); }
.bank-item .bi-origin.refined { color: var(--phos-b); }
.bank-item .bi-origin.edited { color: var(--phos-a); }
.bank-item .bi-origin.preset { color: var(--phos-a-dim); }
/* The latest generation's children, said in a word, in the glyph's place:
   every one of them is bred (⚡). */
.bank-item .bi-new {
  flex: none;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--phos-b);
}
/* Not heard yet: a green dot (the sound's color) centred in the gap between
   the mark column and the name (s1 + s2: 12 px). */
.bank-item .bi-dot {
  position: absolute;
  right: -8.5px;
  top: 50%;
  width: 5px;
  height: 5px;
  margin-top: -2.5px;
  border-radius: 50%;
  background: var(--phos-a);
  box-shadow: 0 0 6px var(--phos-a-60);
  pointer-events: none;
}
/* The name is the row's headline: a musician scans names, not signatures. */
.bank-item .bi-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-silk);
  font-size: var(--t-body);
  font-weight: 500;
  line-height: 1.2;
  color: var(--silk);
  cursor: text;
}
.bank-item .bi-name.custom { color: var(--phos-b); }
.bank-item.live .bi-name.custom { color: var(--phos-a); }
/* A patch id ("#43") and a topology signature ("saw-ladr") are the engine's
   bookkeeping, not anything a player chose or can hear: they wait behind
   ⌘K's Show measurements (`html.engineer`, set in main.js). */
.bank-item .bi-id, .preset-item .pb-sig { flex: none; font-family: var(--font-mono); font-size: var(--t-label); color: var(--silk-dim); }
html:not(.engineer) .bank-item .bi-id,
html:not(.engineer) .preset-item .pb-sig { display: none; }
.bank-item input.bi-rename {
  font-family: var(--font-mono);
  font-size: var(--t-value);
  flex: 1;
  min-width: 0;
  color: var(--phos-b);
  background: var(--bezel);
  border: 1px solid var(--phos-b-dim);
  border-radius: 3px;
  padding: 1px var(--s1);
  outline: none;
}
/* A bred sound's lineage, under its name and aligned with it: the seed it
   grew from and what changed. Pressed, it opens Compare. */
.bank-item .bi-sub { display: flex; align-items: center; gap: var(--s2); min-width: 0; min-height: 22px; }
.bank-item .bi-from {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--t-value);
  line-height: 1.3;
  color: var(--silk-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bank-item .bi-from:hover { color: var(--silk); }

/* What sits over the row's end: placed the same way, each on a ground that
   fades in from the row, so what it covers reads as passing under it. The
   words are on the name's line, level with it; the actions are on the line
   under it, where no name is. */
.bank-item .bi-flag,
.bank-item .bi-acts,
.preset-item .pb-in {
  position: absolute;
  right: var(--s1);
  display: flex;
  align-items: center;
  padding-left: var(--s4);
}
.bank-item .bi-flag { top: var(--s1); height: 17px; } /* token-exempt: the name's line: --t-body at 1.2 */
.bank-item .bi-acts, .preset-item .pb-in { bottom: 5px; } /* token-exempt: the strip's foot: the row's 48 less the name's line, the strip and the model view's bar (3 px, 2 px tall) */
.preset-item .pb-in { height: 22px; } /* token-exempt: the strip's, which it shares */
/* Pointing at EVOLVE POOL marks what a generation does, as the model chose it
   (amber): its seeds, and what it may replace, each in a word at the row's
   end and on the rail. A seed whose bud is out (`.seeding`) is lit the same. */
.bank-item .bi-flag {
  display: none;
  background: linear-gradient(to right, transparent, var(--panel) var(--s4));
  font-family: var(--font-silk);
  font-size: var(--t-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--phos-b);
  pointer-events: none;
}
.bank-item:hover .bi-flag, .bank-item.kbd .bi-flag { background: linear-gradient(to right, transparent, var(--panel-hi) var(--s4)); }
.bank-item.seed .bi-flag,
.bank-item.may-go .bi-flag { display: flex; }
.bank-item.may-go .bi-flag { color: var(--phos-b-dim); }
.bank-item.seed,
.bank-item.seeding { border-left-color: var(--phos-b); }
.bank-item.may-go { border-left-style: dashed; border-left-color: var(--phos-b-dim); }
/* The row's actions, on approach: the pointer, the keyboard cursor, focus,
   on the line under the name. Hidden (`visibility`), they cannot be pressed
   either, so nothing invisible is live; and they become pressable a press's
   length after the pointer arrives (`--d-press`, step-end), so a quick click
   on a row opens it rather than landing on an action that appeared under it.
   The strip itself never takes the pointer, only its buttons, so a bred
   sound's lineage line, which it passes over, can still be pressed where no
   button sits. */
.bank-item .bi-acts {
  gap: 2px;
  background: linear-gradient(to right, transparent, var(--panel-hi) var(--s4));
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--d-state), visibility var(--d-press) step-end;
}
.bank-item .bi-acts > * { pointer-events: auto; }
.bank-item:hover .bi-acts,
.bank-item:focus-within .bi-acts,
.bank-item.kbd .bi-acts,
.bank-item.rating .bi-acts { visibility: visible; opacity: 1; }
/* The word at the row's end gives way while the actions show: one thing at a
   time says what the row is for. */
.bank-item:hover .bi-flag, .bank-item.under .bi-flag, .bank-item.kbd .bi-flag, .bank-item.rating .bi-flag { visibility: hidden; }
.bank-item .bi-acts button {
  flex: none;
  width: 24px;
  height: 22px;
  display: grid;
  place-items: center;
  padding: 0;
  border: none;
  border-radius: var(--r1);
  background: none;
  color: var(--silk-dim);
  cursor: pointer;
}
.bank-item .bi-acts button:hover { background: var(--plate-hi); color: var(--silk); }
.bank-item .bi-acts svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.bank-item .bi-acts .bi-hear svg { fill: currentColor; stroke: none; }
.bank-item .bi-acts .bi-hear .i-stop { display: none; }
.bank-item .bi-acts .bi-hear.playing .i-play { display: none; }
.bank-item .bi-acts .bi-hear.playing .i-stop { display: block; }
/* Sounding: lit, as ▶ SAMPLE and the warm start's ▶ are, and its glyph says
   what pressing it again will do. */
.bank-item .bi-acts .bi-hear.playing { color: var(--phos-a); box-shadow: inset 0 0 8px var(--phos-a-25); }
/* Pressed, and its buffer is on its way: a dotted amber ring at once, so a
   ▶ that has to wait for a render says it heard you. */
.bank-item .bi-acts .bi-hear.pending { outline: 1px dotted var(--phos-b); outline-offset: -2px; color: var(--phos-b); }
/* A row that plays, or is on its way to, keeps its ▶ in sight. */
.bank-item:has(.bi-hear.playing) .bi-acts,
.bank-item:has(.bi-hear.pending) .bi-acts,
.bank-item:has(.bi-hear.loading) .bi-acts,
.bank-item.refocus .bi-acts { visibility: visible; opacity: 1; }
/* A row drawn again under a resting pointer whose actions were showing
   (`.under`, `bankKeep`) shows them at once: the approach's delay is for a
   pointer arriving, and a redraw is no arrival (ADR-025, #458). Until the
   pointer leaves the row; then they hide as they do on leaving. */
.bank-item.under .bi-acts { visibility: visible; opacity: 1; transition: none; }
.bank-item .bi-acts .bi-star.on { color: var(--phos-b); }
.bank-item .bi-acts .bi-star.on svg { fill: currentColor; }
/* Save is the user's own channel: silk, not a phosphor. */
.bank-item .bi-acts .bi-save.on { color: var(--silk); }
.bank-item .bi-acts .bi-kill:hover { color: var(--led-red); }
/* ★ open: the five stars in the actions' place, lit to the rating. */
.bank-item .bi-acts .stars { display: none; gap: 0; }
.bank-item.rating .bi-acts > button { display: none; }
.bank-item.rating .bi-acts .stars { display: flex; }
.bank-item .bi-acts .star.lit { color: var(--phos-b); }
.bank-item .bi-acts .star.lit svg { fill: currentColor; }
/* Under the pointer, the stars up to it light: the rating a press gives. */
.bank-item .bi-acts .stars:hover .star { color: var(--phos-b); }
.bank-item .bi-acts .stars .star:hover ~ .star { color: var(--silk-dim); }

/* The model's guess, the model view's (⌥, `body.model-view`): its percentage
   over the row's end, and its bar along the row's foot, the band how sure it
   is and the tick its guess. Before a fit there is no guess: "·", and the bar
   hatched. Over the row, not beside the name, so no name moves or narrows
   when it shows; on the name's line, so it is never over the actions. */
.bank-item .bi-pct {
  position: absolute;
  right: var(--s2);
  top: var(--s1);
  height: 17px; /* token-exempt: the name's line: --t-body at 1.2 */
  line-height: 17px; /* token-exempt: as the height */
  min-width: 30px;
  padding-left: var(--s4);
  background: linear-gradient(to right, transparent, var(--panel) var(--s4));
  text-align: right;
  font-size: var(--t-value);
  font-variant-numeric: tabular-nums;
  color: var(--phos-b);
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--d-state), visibility var(--d-state);
}
.bank-item .bi-u {
  position: absolute;
  left: calc(var(--s2) + 26px + var(--s3));
  right: var(--s2);
  bottom: 3px;
  height: 2px;
  border-radius: 2px;
  background: var(--amber-off);
  overflow: hidden;
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--d-state), visibility var(--d-state);
}
.bank-item .bi-u b {
  position: absolute;
  top: 0;
  height: 100%;
  background: var(--phos-b-deep);
  border-radius: 2px;
  pointer-events: none;
}
.bank-item .bi-u i {
  position: absolute;
  top: 0;
  width: 2px;
  height: 100%;
  margin-left: -1px;
  background: var(--phos-b);
  box-shadow: 0 0 6px var(--phos-b-60);
  pointer-events: none;
}
/* No fit yet: an explicitly hatched empty track, not a zero. */
.bank-item .bi-u.nofit {
  background: repeating-linear-gradient(45deg, var(--nofit-a) 0 3px, var(--nofit-b) 3px 6px);
}
body.model-view .bank-item .bi-pct,
body.model-view .bank-item .bi-u { visibility: visible; opacity: 1; }
.bank-item:hover .bi-pct, .bank-item.kbd .bi-pct { background: linear-gradient(to right, transparent, var(--panel-hi) var(--s4)); }
/* A row EVOLVE POOL marks keeps its word (SEED, MAY BE REPLACED) at its end;
   its guess gives way while the mark shows, so the two never overlap. */
body.model-view .bank-item.seed .bi-pct,
body.model-view .bank-item.may-go .bi-pct { visibility: hidden; opacity: 0; }
/* Under the model view the end of a row is the guess's; its actions come
   back under the pointer. */
body.model-view .bank-item:not(:hover, .under):not(.rating) .bi-acts { visibility: hidden; opacity: 0; }

/* A child that has just landed glows once. */
.bank-item.landed { animation: bank-landed 900ms ease-out; } /* token-exempt: a one-shot glow, held long enough to be seen; a move is 320 ms */
@keyframes bank-landed {
  from { box-shadow: 0 0 14px var(--phos-b-45), inset 0 0 0 1px var(--phos-b); }
}
@media (prefers-reduced-motion: reduce) { .bank-item.landed { animation: none; } }
/* Its bud is on its way: the name arrives with it. */
.bank-item.budding .bi-name { visibility: hidden; }

/* A preset: the face and the name, IN POOL at the end once it is in the pool,
   and ▶ on approach. IN POOL is on the line under the name, where the ▶ is, so
   the name has the row's whole width: neither takes any of it. */
.preset-item {
  --row-ground: var(--panel); /* the row's own ground, for what fades in over its end */
}
.preset-item:hover, .preset-item.kbd { --row-ground: var(--panel-hi); }
.preset-item .pb-in {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--phos-a-dim);
  pointer-events: none;
}
/* While ▶ is in sight (on approach, with the focus in the row, and while it
   plays) IN POOL stands clear of it, left of the ▶'s strip, on a ground that
   fades in from the row, so both read whole: hearing a preset is what put it
   in the pool, and its ▶ (■ while it sounds) stays up while it plays. The
   name keeps its x; its end passes under the word, as it passes under a pool
   row's actions. The selectors are the ones that show the actions
   (`.bank-item .bi-acts`, above), less `.pending`, which a preset's ▶ never
   carries (a preset row waits as `.loading`). */
.preset-item:is(:hover, .under, :focus-within, .kbd, :has(.bi-hear.playing)) .pb-in {
  right: calc(var(--s1) + 24px + var(--s4)); /* the ▶ (24 px) and its strip's fade */
  background: linear-gradient(to right, transparent, var(--row-ground) var(--s4));
}
/* Under the model view the actions show only under the pointer, so IN POOL
   keeps the row's end until then. */
body.model-view .preset-item:not(:hover, .under) .pb-in { right: var(--s1); background: none; }
.preset-item.in-bank { border-left-color: var(--phos-a-dim); }
.preset-item.kbd { box-shadow: inset 0 0 0 1px var(--phos-a); }

/* The bank's groups: the latest generation's children lead the pool under
   their own heading, the presets under their families, each with its count. */
.bank-group {
  flex: none;
  display: flex;
  justify-content: space-between;
  gap: var(--s2);
  padding: var(--s4) var(--s2) var(--s2);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--silk-dim);
  text-rendering: geometricPrecision;
}
/* New is the sounds just bred: the sound's green, as the specimen sets it. */
.bank-group.new { color: var(--phos-a-dim); }
.bank-group .bg-n {
  font-family: var(--font-mono);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--silk-dim);
}
/* Under New: what its generation bred that is not in it. */
.bank-below {
  padding: 0 var(--s2) var(--s1);
  font-size: var(--t-value);
  line-height: 1.4;
  color: var(--silk-dim);
}
/* Replaced, at the foot of the pool: a heading that opens on names only. */
.bank-group.replaced { padding: 0; }
.bank-group .bg-fold {
  display: flex;
  justify-content: space-between;
  gap: var(--s2);
  width: 100%;
  padding: var(--s4) var(--s2) var(--s2);
  border: none;
  background: none;
  cursor: pointer;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
}
.bank-group .bg-fold:hover { color: var(--silk); }
.replaced-names {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s3);
  padding: 0 var(--s2) var(--s3);
  font-family: var(--font-silk);
  font-size: var(--t-body);
  line-height: 1.4;
  color: var(--silk-dim);
}
/* The pool's sounds kept safe (takes.js): each name, the engine's sentence
   for it, and RECORD AGAIN, lit while it records. */
.kept-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--s1) var(--s2);
  padding: var(--s2) var(--s2);
  border-radius: var(--r1);
}
.kept-row.kbd { box-shadow: inset 0 0 0 1px var(--phos-a-deep); background: var(--panel-hi); }
.kept-name { font-size: var(--t-body); color: var(--silk); }
.kept-note { grid-column: 1 / -1; font-size: var(--t-label); color: var(--silk-dim); }
.kept-rec { grid-row: 1; grid-column: 2; }
.kept-rec.on { color: var(--phos-a); border-color: var(--phos-a); }

.bench-empty { color: var(--silk-dim); font-size: var(--t-body); font-family: var(--font-silk); padding: var(--s3) var(--s2); }

/* ---- stage + views ---- */
.stage { --rail-w: 96px; min-width: 0; position: relative; }
/* Every level leaves the stage's right edge to the rail: wide enough that
   EVOLVE's stop, which branches left of PERFORM's, never covers a control
   (the prototype's 76 px let it sit on PERFORM's pads and PATCH's module
   rail). */
.view {
  position: absolute;
  inset: 0 var(--rail-w) 0 0;
  display: flex;
  flex-direction: column;
  padding: var(--s3);
  gap: var(--s2);
  min-height: 0;
  overflow: hidden;
}
/* One level is `.on` (shell.js). During a move the one you leave stays on
   until it has faded (`.leaving`, inert), and the two overlap, each scaled
   or slid by the move (levels.js `MORPH`). */
.view:not(.on) { display: none !important; }
.view.leaving { pointer-events: none; }
/* A pinch on a touch screen is a move between the levels (shell.js), not
   the page's zoom; a finger still scrolls what scrolls. */
.stage { touch-action: pan-x pan-y; }
/* The face carried between the levels, on one canvas over the page, drawn
   only while it flies (shell.js `overlay`): over the bank and the levels,
   under the toasts (30), the menus and every dialog. */
.zoom-face { position: fixed; inset: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 29; display: none; }
.zoom-face.on { display: block; }
/* ---- the levels: where you are (shell.js) ----
   A cross at the stage's right edge, to the prototype: out to in, LEARNING,
   TASTE, PERFORM and PATCH, with EVOLVE branching left of PERFORM. The
   current stop is lit green. A stop's name and key are its tooltip (its
   `title`): nothing is drawn beside the cross, over the level. */
.rail {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 7;
  display: grid;
  justify-items: end;
}
.rail-stop {
  position: relative;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 1px solid var(--hairline);
  border-radius: 50%;
  background: var(--panel);
  color: var(--silk-mute);
  cursor: pointer;
  transition: color var(--d-state), border-color var(--d-state), box-shadow var(--d-state), background var(--d-state);
}
.rail-stop svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.rail-stop:hover { color: var(--silk); border-color: var(--plate-edge); }
.rail-stop[aria-current="location"] {
  color: var(--phos-a);
  border-color: var(--phos-a-deep);
  background: var(--panel-hi);
  box-shadow: 0 0 0 4px var(--phos-a-06), 0 0 18px -2px var(--phos-a-35);
}
.rail-mid { display: flex; align-items: center; }
.rail-stop.aside { width: 34px; height: 34px; }
.rail-stop.aside svg { width: 15px; height: 15px; }
/* The puck: a light that travels from the stop you left to the one you
   reach (shell.js); while it does, the stop you reach waits unlit. */
.rail-puck {
  position: absolute;
  left: -3.5px;
  top: -3.5px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--phos-a);
  box-shadow: 0 0 10px var(--phos-a), 0 0 2px var(--phos-a-35);
  opacity: 0;
  pointer-events: none;
  z-index: 2;
}
.rail.traveling .rail-stop[aria-current="location"] {
  color: var(--silk-mute);
  border-color: var(--plate-edge);
  background: var(--panel);
  box-shadow: none;
}
.rail-line { width: 1px; height: 26px; margin-right: calc(var(--s4) + 3.5px); background: var(--plate-edge); }
.rail-branch { width: 12px; height: 1px; background: var(--plate-edge); }
.rail-hint { font-family: var(--font-mono); font-size: var(--t-label); color: var(--silk-dim); margin: var(--s1) var(--s3) var(--s1) 0; }
/* At 1080 px and under every pixel of the stage is wanted: the cross draws
   smaller and closer to the edge, and the levels leave it the prototype's
   76 px, still clear of EVOLVE's stop. */
@media (max-width: 1080px) {
  .stage { --rail-w: 76px; }
  .rail { right: 6px; }
  .rail-stop { width: 32px; height: 32px; }
  .rail-stop svg { width: 16px; height: 16px; }
  .rail-stop.aside { width: 28px; height: 28px; }
  .rail-stop.aside svg { width: 13px; height: 13px; }
  .rail-branch { width: 6px; }
  .rail-line { height: 20px; margin-right: calc(var(--s3) + 3.5px); }
  .rail-hint { margin-right: var(--s2); }
}

.stage-toolbar {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}
.seg { display: flex; align-items: center; }
.seg > * { display: flex; }
/* A segmented control: shared border, square inner corners, no gaps. */
.seg .seg-item {
  border-radius: 0;
  margin-left: -1px;
  white-space: nowrap;
}
.seg > :first-child .seg-item, .seg > .seg-item:first-child {
  border-top-left-radius: 3px; border-bottom-left-radius: 3px; margin-left: 0;
}
.seg > :last-child .seg-item, .seg > .seg-item:last-child {
  border-top-right-radius: 3px; border-bottom-right-radius: 3px;
}
.seg .seg-item:hover:not(:disabled) { position: relative; z-index: 1; }
/* The only filled amber button in the view. Everything else in the head is
   quiet; this is the act the whole app exists to perform. */
#rack-evolve:not(:disabled) {
  color: var(--evolve-ink);
  background: linear-gradient(180deg, var(--evolve-hi), var(--phos-b) 62%, var(--evolve-lo));
  border-color: var(--evolve-edge);
  box-shadow: inset 0 1px 0 var(--white-35), 0 3px 0 var(--evolve-cast), 0 6px 14px var(--phos-b-18);
}
#rack-evolve:not(:disabled):hover { filter: brightness(1.06); }
.tool-sep { width: 1px; height: 18px; background: var(--hairline); margin: 0 2px; }
.toggle {
  display: flex; align-items: center; gap: var(--s1);
  font-size: var(--t-label); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--silk-dim); cursor: pointer;
}
/* The checkbox is drawn, not borrowed: a bezel square that lights amber —
   this is the model's own channel, after all. */
.toggle input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  margin: 0;
  border: 1px solid var(--hairline);
  border-radius: 3px;
  background: var(--bezel);
  box-shadow: inset 0 1px 2px var(--black);
  cursor: pointer;
  position: relative;
}
.toggle input[type="checkbox"]:hover { border-color: var(--silk-dim); }
.toggle input[type="checkbox"]:checked {
  border-color: var(--phos-b);
  background: var(--phos-b-10);
  box-shadow: inset 0 0 6px var(--phos-b-20);
}
.toggle input[type="checkbox"]:checked::after {
  content: "✓";
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  font-size: 10px; /* token-exempt: a glyph sized to its 14px box */
  color: var(--phos-b);
  text-shadow: 0 0 6px var(--phos-b-60);
}

/* ---- workbench rack ---- */
/* The frame is the fixed object; the viewport inside it is what the camera
   looks through. Anything parented to the frame (scope, minimap, empty
   state) is guaranteed not to move when the patch pans. */
.rack-frame {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
}
.rack-scroll {
  position: relative;
  touch-action: none;
  flex: 1;
  min-height: 0;
  min-width: 0;
  /* Not `auto`. Scrollbars were the only way out of a patch that had grown
     past the frame, and they could only ever reach content the baked-in
     magnification had already decided to draw. The viewBox is the scroller
     now, and it goes both ways. */
  overflow: hidden;
  border-radius: var(--r1);
  border: 1px solid var(--hairline);
  /* The dot grid moved into the SVG so it parallaxes with the patch instead
     of staying nailed to the frame — a ground plane you can't move over is
     not a ground plane, it is wallpaper. */
  background: linear-gradient(180deg, var(--rack-bed-hi), var(--rack-bed-lo));
  box-shadow: inset 0 0 22px var(--black-85), inset 0 0 2px 1px var(--black);
}
/* A hard cut mid-plate says "the window ends here"; a fade says "there is
   more this way". Applied only on the side that actually has content past
   it, so a fitted patch keeps its full contrast right to the hairline. */
.rack-scroll.faded {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, var(--black) var(--fade-l, 0px),
    var(--black) calc(100% - var(--fade-r, 0px)), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, var(--black) var(--fade-l, 0px),
    var(--black) calc(100% - var(--fade-r, 0px)), transparent 100%);
}
.rack-scroll.grabbing { cursor: grabbing; }
.rack-scroll.grabbing * { cursor: grabbing !important; }
/* Freeform: the faceplate says it can be picked up, and the controls on it
   keep saying they are controls. The plate layer is the only thing under the
   pointer on bare panel — knobs, jacks and the ⋯/▢ well all sit in the
   control layer above it and set their own cursors — so this one selector is
   the whole affordance. */
.rack-scroll.freeform:not(.placing) .rack-plates g[data-mid] { cursor: grab; }
.rack-scroll.moving-plate, .rack-scroll.moving-plate * { cursor: grabbing !important; }
#rack-svg { display: block; width: 100%; height: 100%; }
/* The ground plane and the pool of light under the patch. Both are drawn in
   rack coordinates, which is the whole point: they are the parallax
   reference that tells you the canvas moved. */
.rack-dots { fill: url(#dotGrid); pointer-events: none; }
.rack-pool { pointer-events: none; }

/* ---- minimap ---- */
.rack-map {
  position: absolute;
  left: 10px; bottom: 10px;
  width: 172px; height: 116px;
  z-index: 6;
  border-radius: var(--r1);
  border: 1px solid var(--hairline);
  background: var(--rack-map-glass);
  box-shadow: 0 6px 18px var(--black-50), inset 0 1px 0 var(--white-04);
  cursor: crosshair;
  touch-action: none;
}
.rack-map.hidden { display: none; }
.mm-node { fill: var(--phos-a-34); }
.mm-node.mod { fill: var(--phos-b-34); }
.mm-node.amp { fill: var(--phos-a-70); }
.mm-view { fill: var(--white-05); stroke: var(--phos-b); stroke-width: 1.2; }
/* Bookmarks. Amber, because a bookmark is a decision about where to look and
   the amber channel is where this instrument keeps decisions — and filled
   rather than outlined so a pip reads as a marker on the map rather than as a
   second, smaller viewport rect. Drawn under `.mm-view`, which is the one
   thing on the map that must never be obscured. */
.mm-pip {
  fill: var(--phos-b-90);
  stroke: var(--mm-pip-rim);
  stroke-width: 1;
}
.mm-pip-n {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: 600;
  fill: var(--mm-pip-ink);
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
}
/* A control that does not apply right now, holding its slot.
   `display: none` on a button inside a horizontal group moves every button to
   its right — and the layout-mode toggle was to its right, so entering
   freeform slid the toggle ~100 px under the pointer that had just pressed it
   and a second press fired *apply grid*, which rewrites every position (m6).
   The fixed-slot rule is already how the evolve callout keeps its row; this is
   the same rule for the same reason. */
.tt.held { visibility: hidden; }

/* The frame's own recovery hint (M3(e)). Centred rather than cornered: it is
   about the whole view, not about anything under it, and at the zoom that
   summons it there is nothing under it anyway. */
.fit-hint {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 7;
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s2) var(--s2) var(--s3);
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  background: var(--surface-pop);
  box-shadow: 0 6px 20px var(--black-60);
  font-family: var(--font-silk);
  font-size: var(--t-body);
  color: var(--silk-dim);
  pointer-events: auto;
}
.fit-hint.hidden { display: none; }

.rack-empty {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--silk-dim);
  font-family: var(--font-silk); font-size: var(--t-body);
  padding: 0 var(--s7); text-align: center;
}

/* svg module styling */
.mod-plate { fill: url(#plateGrad); stroke: var(--plate-edge); stroke-width: 1; }
.mod-plate.modside { stroke: var(--modside-edge); }
.mod-plate.locked { stroke: var(--phos-b); stroke-dasharray: 4 3; }
/* A gesture waiting its turn in the bench lane — a bypass pressed while a
   knob turn was still landing — is aimed at this plate. It will happen, in
   order; the rack's caption counts what is waiting (main.js `lanePaint`). */
.mod-group.queued .mod-plate { stroke: var(--phos-a-dim); stroke-dasharray: 2 3; }
/* The opt-in belief tint. A stroke rather than a fill, and inset rather than
   on the plate's own edge, so it can never be confused with the dashed amber
   `locked` outline — one says "the model leans this way about this family",
   the other says "you pinned this", and they are routinely on at once.
   `stroke-opacity` is written per plate: it carries the model's certainty. */
.belief-edge { fill: none; stroke-width: 2; pointer-events: none; }
.belief-edge.pos { stroke: var(--phos-b); }
.belief-edge.neg { stroke: var(--led-red); }

/* ---- the per-port probe (WS-9) ---- */
/* A CRT window in the same idiom as the big scope's bezel, clipped to a
   module's out ○ by a short lead. Nothing here is interactive except the
   tooltip, so the whole group is pointer-transparent apart from the bezel
   itself, which has to be hoverable for the title to be readable. */
.port-trace { pointer-events: none; }
.pt-lead { fill: none; stroke: var(--phos-a-deep); stroke-width: 1.4; }
.pt-bezel {
  fill: var(--bezel);
  stroke: var(--hairline);
  stroke-width: 1;
  pointer-events: auto;
}
.pt-zero { stroke: var(--phos-a-16); stroke-width: 0.8; }
.pt-wave { fill: var(--phos-a); fill-opacity: 0.82; stroke: none; }
.pt-msg {
  font-family: var(--font-mono);
  font-size: var(--t-rack-micro);
  fill: var(--silk-dim);
  pointer-events: none;
}
/* Stale: the patch has moved and the render on screen is of the one before
   it. Dimmed rather than emptied — a picture of a moment ago is worth more
   than a blank box, and the tooltip says which it is. */
.port-trace.stale { opacity: 0.45; }
/* Hierarchy by ratio, not by a pixel. The three pieces of text on a plate had
   one size between them and the brightest of them was the one that never
   changed — so the plate read as a caption block rather than as a control
   surface. Now: the name is large and quiet (silkscreen is *printed*, and
   printed things are the calmest thing on any panel), the knob label is small
   and quieter still, and the readout — the only text on the plate that ever
   moves — owns the contrast. */
.mod-title {
  font-family: var(--font-silk);
  font-size: var(--t-rack-title);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  fill: var(--silk-dim);
}
/* A one-knob plate is 96 units wide and gives up ~44 of them to the control
   well. 13px there means condensing the glyphs, and a squeezed name reads as
   a rendering fault; one step down reads as a smaller panel, which it is.
   One step down on the rack's own scale — the readout size — so the
   silkscreen floor below treats it as the size it is. */
.mod-title.narrow { font-size: var(--t-rack-value); letter-spacing: 0.12em; }
.mod-title.modside { fill: var(--phos-b); }
/* The masthead rule. One hairline is the difference between a name floating
   on a plate and a name that the knob grid hangs from. */
/* Not `--hairline`: that token is tuned against the app's near-black panels
   and disappears entirely on the plate gradient, which is two stops lighter.
   A rule you cannot see is a rule that is not there. */
.plate-rule { fill: var(--white-09); pointer-events: none; }
.knob-label {
  font-family: var(--font-silk);
  font-size: var(--t-rack-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  fill: var(--silk-dim);
  text-anchor: middle;
}
/* Silkscreen is printed on the panel; it is not a control and must never take
   a press. It never did anything with one either — the label and the readout
   sit in the knob's group but outside its hit circle, so a press on "71%" was
   simply swallowed — and now that the faceplate under them is draggable in
   freeform, swallowed means the plate does not move. */
.mod-title, .knob-label, .knob-value, .plate-hint { pointer-events: none; }
/* Green is reserved for things that carry signal — cables, jacks, arcs, the
   scope trace. A readout carries a *report*, so it is neutral at rest and
   borrows the phosphor for the ~400ms after it changes, which is the only
   moment it is about the sound. `--silk-dim` is brighter than the
   `--silk-mute` above it, which is what makes it the brightest text here. */
.knob-value {
  font-family: var(--font-mono);
  font-size: var(--t-rack-value);
  fill: var(--silk-dim);
  text-anchor: middle;
  transition: fill var(--d-move) ease-out;
}
.knob-value.flashing { fill: var(--phos-a); transition: none; }
/* The silkscreen floor (`syncSilkFloor` in main.js): the camera draws the
   rack at 0.3–2.5×, and a tier of type that would print smaller than the
   rack's own smallest type at 1× (`--t-rack-micro`) steps aside rather than
   being drawn illegible — 5–7px labels were the rule at 1280×800, not the
   exception. The tier names are set from the size tokens, so a token change
   moves its threshold with it. Hidden, not removed: `fitSilk` still measures
   them, and they come back the moment the camera does. Under "detail full"
   nothing is hidden. */
#rack-svg.silk-floor[data-illegible~="micro"] .jack text,
#rack-svg.silk-floor[data-illegible~="micro"] .plate-hint,
#rack-svg.silk-floor[data-illegible~="micro"] .pt-msg,
#rack-svg.silk-floor[data-illegible~="label"] .knob-label,
#rack-svg.silk-floor[data-illegible~="label"] .out-name,
#rack-svg.silk-floor[data-illegible~="label"] .step-num,
#rack-svg.silk-floor[data-illegible~="label"] .mod-tab-name,
#rack-svg.silk-floor[data-illegible~="value"] .knob-value,
#rack-svg.silk-floor[data-illegible~="value"] .enum-text,
#rack-svg.silk-floor[data-illegible~="value"] .mod-title.narrow,
#rack-svg.silk-floor[data-illegible~="title"] .mod-title { visibility: hidden; }
.knob-body { fill: url(#knobGrad); stroke: var(--black); stroke-width: 1; cursor: ns-resize; }
/* A knob's face is bigger than its body: the ticks, the travel track and the
   value arc all ride outside r, and all of them read as part of the control.
   None of them may intercept the press — an unfilled ring still hit-tests on
   its stroke, and a press that lands on decoration is a knob that doesn't
   turn. `.knob-hit` is the real target and covers the whole face. */
.knob-hit { fill: transparent; cursor: ns-resize; }
.knob-ticks, .knob-track, .knob-arc { pointer-events: none; }
.knob-ring { fill: none; stroke: var(--knob-ring); stroke-width: 1; }
.knob-ind { stroke: var(--phos-a); stroke-width: 2; stroke-linecap: round; pointer-events: none; }
.knob-ind.modside { stroke: var(--phos-b); }
.knob-locked-halo { fill: none; stroke: var(--phos-b); stroke-width: 1.2; stroke-dasharray: 2 2.5; pointer-events: none; }
.enum-body { fill: var(--well); stroke: var(--plate-edge); stroke-width: 1; cursor: pointer; }
/* The step lane (a `steps` module's eight values). The bar is bipolar: the
   fill grows from the 0 V line, up for a positive step and down for a
   negative one. Steps past `length` stay drawn — they are still in the
   genome and still editable — but recede, because they are not playing. */
.step-track { fill: var(--well); stroke: var(--plate-edge); stroke-width: 1; pointer-events: none; }
.step-fill { fill: var(--phos-a); opacity: 0.85; pointer-events: none; }
.step-fill.modside { fill: var(--phos-b); }
.step-zero { stroke: var(--hairline); stroke-width: 1; pointer-events: none; }
.step-num {
  font-family: var(--font-mono); font-size: var(--t-rack-label); fill: var(--silk-dim);
  text-anchor: middle; pointer-events: none;
}
.step-hit { fill: transparent; cursor: ns-resize; }
.step-bar.latent .step-fill { opacity: 0.25; }
.step-bar.latent .step-num { opacity: 0.45; }
.step-bar.dragging .step-track, .step-bar:hover .step-track { stroke: var(--nut-hi); }
[data-addr].step-bar:focus-visible .step-track { stroke: var(--phos-a); stroke-width: 2; }
/* AUDIO IN's lane (audio-in.js `drawLane`): its input as a chip that opens
   the input menu, the face slot (the level meter until faces land), MONITOR
   and NEW CLIP, and the headphones note. MONITOR lit is green: it is the
   input carrying signal to the speakers. */
.ain-dev-body, .ain-btn-body { fill: var(--well); stroke: var(--plate-edge); stroke-width: 1; cursor: pointer; }
.ain-dev:hover .ain-dev-body, .ain-btn:hover .ain-btn-body { stroke: var(--nut-hi); }
.ain-dev:focus-visible .ain-dev-body, .ain-btn:focus-visible .ain-btn-body { stroke: var(--phos-a); stroke-width: 2; }
.ain-dev-text {
  font-family: var(--font-mono); font-size: var(--t-rack-label); fill: var(--phos-a);
  pointer-events: none;
}
.ain-lane:not([data-state="live"]):not([data-state="meter"]) .ain-dev-text { fill: var(--silk-dim); }
.ain-btn-text {
  font-family: var(--font-silk); font-size: var(--t-rack-label); letter-spacing: 0.1em;
  text-transform: uppercase; fill: var(--silk-dim); text-anchor: middle; pointer-events: none;
}
.ain-btn.on .ain-btn-body { fill: var(--phos-a-16); stroke: var(--phos-a); }
.ain-btn.on .ain-btn-text { fill: var(--phos-a); }
.ain-btn.off { opacity: 0.45; }
.ain-face-body { fill: var(--well); stroke: var(--plate-edge); stroke-width: 1; pointer-events: none; }
.ain-meter-track { fill: var(--white-09); pointer-events: none; }
.ain-meter-fill { fill: var(--phos-a); opacity: 0.85; pointer-events: none; }
/* The live face's canvas fills its square (its pixels are FACE_PX per unit). */
.ain-face-live { pointer-events: none; }
.ain-face-live canvas { display: block; width: 100%; height: 100%; }
.ain-note {
  font-family: var(--font-silk); font-size: var(--t-rack-label); letter-spacing: 0.1em;
  text-transform: uppercase; fill: var(--silk-dim); pointer-events: none;
}
#rack-svg.silk-floor[data-illegible~="label"] .ain-note,
#rack-svg.silk-floor[data-illegible~="label"] .ain-btn-text,
#rack-svg.silk-floor[data-illegible~="label"] .ain-dev-text { visibility: hidden; }
/* CAPTURE's lane (takes.js): RECORD, lit while it records, and the length of
   the recording the capture holds. */
.take-line { font-family: var(--font-mono); font-size: var(--t-rack-label); fill: var(--silk-dim); pointer-events: none; }
#rack-svg.silk-floor[data-illegible~="label"] .take-line { visibility: hidden; }
/* An enum chip's text is its readout — the option it is set to, and the part
   of the chip that changes — so it is set at the readout size and leaves with
   the readouts under the silkscreen floor, rather than a step below them,
   where the floor emptied the chip while the knobs beside it still read. */
.enum-text {
  font-family: var(--font-mono); font-size: var(--t-rack-value); fill: var(--phos-a);
  text-anchor: middle; pointer-events: none;
}
.lock-dot { cursor: pointer; }
.lock-dot circle { fill: var(--amber-off); stroke: var(--lock-dot-rim); stroke-width: 1; }
.lock-dot.on circle { fill: var(--phos-b); stroke: var(--phos-b); filter: drop-shadow(0 0 3px var(--phos-b-80)); }
.mod-lock { font-family: var(--font-mono); font-size: var(--t-rack-label); fill: var(--silk-dim); cursor: pointer; }
.mod-lock.on { fill: var(--phos-b); }
/* Cables run *over* the faceplates now (see .rack-wires in main.js): under
   them, a run that crossed a module simply vanished and reappeared — the graph
   asserted a connection and then hid the evidence. Front-panel cables are
   physically true for this instrument, and they only read that way with two
   things: a casing, and a shadow.

   The casing is the cable's jacket — background-coloured and wider than the
   ink, so a crossing cuts a channel through the plate instead of tinting it.
   The shadow is on the whole layer at the same 315° offset as .mod-plate's, so
   a cable casts onto the panel it lies on and the two agree about the lamp. */
.wire { fill: none; stroke-linecap: round; pointer-events: none; }
/* The stroke is set per cable in JS (the five-stop green ramp, and the level
   at that point in the chain); this is the fallback for anything drawn
   outside `buildRack` — the drag band, the duel minis' cutouts. */
.wire.audio { stroke: var(--phos-a-72); stroke-width: 2.2; }
/* The casing was an opaque near-black, which worked over a black void and
   became a visible dark stripe the moment the ground plane arrived under it.
   Translucent black sits on whatever it is over — panel, dots, pool — and
   still separates two crossing cables, which is the whole job. */
.wire.audio-case { stroke: var(--black-50); stroke-width: 5.5; }
.wire.mod {
  stroke: var(--phos-b-62); stroke-width: 1.8; stroke-dasharray: 6 4;
  filter: drop-shadow(0 0 3px var(--phos-b-35));
}
.wire.mod-case { stroke: var(--black-50); stroke-width: 4.5; }
.rack-wires { filter: drop-shadow(1px 3px 3px var(--black-55)); }
.port { fill: var(--shadow-ink); stroke: var(--port-rim); stroke-width: 1; }

/* ---- cable terminations ----
   The plug in the socket. A barrel, a knurled collar, and the strain-relief
   boot the lead emerges from — which is the point: a cable that starts at a
   coordinate is a graph edge, and a cable that starts in a boot is a patch
   lead. Everything here is inert; the jack's own hit circle is above it. */
.plug, .plug * { pointer-events: none; }
.plug-barrel { fill: url(#jackNut); stroke: var(--black-70); stroke-width: 0.8; }
.plug-collar { fill: var(--black-35); }
.plug-boot { fill: var(--chip); stroke: var(--black-60); stroke-width: 0.6; }
/* A bare plug: a link arriving mid-CV-chain, where there is no patch point to
   draw a socket for. */
.jack.bare { cursor: default; }

/* ---- the mod-slot housing ----
   Same silhouette empty or filled, always: occupancy is carried by the
   stroke, never by whether the element is there. That is what makes "this
   module can be modulated, here" a fact you can read at rest on every plate
   instead of one you learn by accident after cabling one. */
.mod-tab {
  fill: var(--mod-tab-fill);
  stroke: var(--phos-b-deep);
  stroke-width: 1;
  stroke-dasharray: 3 2.5;
  pointer-events: none;
}
.mod-tab.filled { fill: var(--mod-tab-filled); stroke-dasharray: none; }
.mod-tab-name {
  font-family: var(--font-silk);
  font-size: var(--t-rack-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  fill: var(--phos-b);
  /* 0.45 was 3.0:1 at rack sizes — present but unreadable. */
  opacity: 0.7;
  pointer-events: none;
}
/* Occupied, not shouting. The destination name is silkscreen either way —
   what changed is the stroke around it and the cable in it — and at full
   amber it out-ranked the knob readouts, which are the one thing on the plate
   that is allowed to be the brightest. */
.mod-tab-name.filled { opacity: 0.8; }

/* ---- the control well ----
   A recessed pocket, so the two structure verbs read as controls that live
   somewhere rather than as glyphs that happen to be printed there. It lifts
   as the hand approaches the plate — `.plate-hot` is set on both the plate
   group and the control group in JS, because they are in different layers and
   no CSS selector spans them. */
.ctrl-well {
  fill: var(--black-25);
  stroke: var(--hairline);
  stroke-width: 1;
  pointer-events: none;
  transition: fill var(--d-state) ease, stroke var(--d-state) ease;
}
.plate-hot .ctrl-well { fill: var(--white-04); stroke: var(--silk-mute); }
/* "What have I pinned?" is asked before every ⚡, so a locked plate answers it
   without being hovered, poked or focused. */
.ctrl-well.locked { fill: var(--phos-b-16); stroke: var(--phos-b); }
.plate-hot .mod-menu-btn, .plate-hot .mod-lock { fill: var(--silk); }

/* Plates and cables on their way out (see the motion system in main.js). They
   are clones of what was on screen a frame ago, stripped of every attribute
   anything queries by, so they are pictures rather than modules — and the
   `*` here is belt to that braces: a ghost must never be able to take a click,
   a cable drop or a hover away from the plate that replaced it, and a class
   selector beats the `pointer-events` presentation attribute a child might
   otherwise win with. */
.rack-exit, .rack-exit * { pointer-events: none; }

/* ---- EVOLVE view ---- */
/* To the specimen (Plan-008 C1, `z-desk-evolve.png`): the head (cap, the
   question, the pips and the teaching line) with a small TASTE map at its
   right; two cards, each its sound's face in a well, then its name, family
   and blurb and PLAY and PICK; the foot (ANOTHER PAIR and the deal rule,
   the forecast, EVOLVE POOL and what each generation did). */
#view-evolve { padding: var(--s5) var(--s5) var(--s4); gap: var(--s4); }
.ev-head { flex: none; display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s5); }
.ev-head-words { min-width: 0; display: grid; gap: var(--s2); }
#view-evolve .ev-title { margin: 0; font-size: clamp(var(--t-title), 3.4vw, var(--t-display)); } /* (1,1,0): over .lv-title's display size, set further down */
.duel-foot .hw-btn { white-space: nowrap; }
@media (max-width: 1180px) {
  #view-evolve { padding: var(--s4) var(--s3) var(--s3); gap: var(--s3); }
  #view-evolve .ev-title { font-size: var(--t-title); }
  .ev-map { width: 200px; height: 110px; }
  /* Each card's foot: the name on its line, PLAY and PICK under it, so the
     two cards keep one height whatever their names. */
  .duel-foot { flex-direction: column; align-items: stretch; }
}
.ev-map {
  flex: none;
  position: relative;
  width: 260px;
  height: 140px;
  padding: 0;
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: radial-gradient(120% 90% at 50% 60%, var(--panel), var(--bezel) 70%);
  cursor: pointer;
}
.ev-map canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
.ev-map:hover { border-color: var(--silk-mute); }
.ev-map:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
.ev-map-cap { position: absolute; left: var(--s2); bottom: var(--s1); font-family: var(--font-silk); font-size: var(--t-label); font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--silk-dim); }
.ev-map-cap kbd { font-family: var(--font-mono); letter-spacing: 0; text-transform: none; padding: 0 var(--s1); border: 1px solid var(--hairline); border-radius: 3px; }
.duel-row {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: stretch;
  gap: var(--s3);
}
.duel-or { align-self: center; font-family: var(--font-silk); font-size: var(--t-label); letter-spacing: 0.16em; text-transform: uppercase; color: var(--silk-dim); }
.duel-card {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: var(--s3);
  min-height: 0;
  min-width: 0;
  cursor: pointer;
}
.duel-card.live-sel .duel-well { border-color: var(--phos-a-dim); box-shadow: 0 0 12px var(--phos-a-12); }
.duel-hint { font-size: var(--t-label); }
/* The card's well: the sound's face, large, with its glow and its floor. */
.duel-well {
  position: relative;
  min-height: 160px;
  overflow: hidden;
  border: 1px solid var(--hairline);
  border-radius: var(--r3);
  background: radial-gradient(120% 90% at 50% 60%, var(--panel), var(--bezel) 62%);
  box-shadow: inset 0 0 0 1px var(--black-60), inset 0 30px 60px -30px var(--black-75);
}
.duel-well > .face-slot { position: absolute; inset: var(--s5) var(--s4) var(--s3); }
.duel-well > .face-slot.hidden { display: none; }
.duel-side { position: absolute; left: var(--s3); top: var(--s3); font-family: var(--font-silk); font-size: var(--t-label); font-weight: 600; letter-spacing: 0.16em; color: var(--silk-dim); z-index: 2; }
/* While the sound renders, its sweep over the face; nothing once it has. */
.duel-well > .scope { position: absolute; inset: 0; width: 100%; height: 100%; min-height: 0; background: none; box-shadow: none; pointer-events: none; }
.duel-well > .mini-rack { position: absolute; inset: var(--s6) var(--s3) var(--s3); min-height: 0; }
.duel-well > .readout { position: absolute; left: var(--s3); right: var(--s3); bottom: var(--s2); z-index: 2; }
.duel-well > .style-badge { position: absolute; left: var(--s7); top: var(--s3); z-index: 2; }
.duel-corner { position: absolute; top: var(--s3); right: var(--s3); display: flex; gap: var(--s2); z-index: 3; }
.duel-corner .inspect-btn { background: var(--bezel); }
.duel-foot { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3); min-width: 0; }
.duel-who { min-width: 0; display: grid; gap: var(--s1); }
.duel-fam { font-family: var(--font-silk); font-size: var(--t-body); color: var(--silk-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.duel-cat { font-size: var(--t-label); font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--silk); margin-right: var(--s1); }
.duel-foot .duel-controls { flex: none; }
.inspect-btn {
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--silk-dim);
  background: none;
  border: 1px solid var(--hairline);
  border-radius: 3px;
  padding: 3px var(--s2);
  cursor: pointer;
}
.inspect-btn:hover:not(:disabled) { color: var(--phos-a); border-color: var(--phos-a-dim); }
/* Off while the table has no pair (main.js setDuelControlsEnabled): it does
   not light under the pointer. */
.inspect-btn:disabled { cursor: default; }
.inspect-btn kbd { margin-left: var(--s1); padding: 0 var(--s1); border: 1px solid var(--hairline); border-radius: 3px; }

.scope, .crt {
  display: block;
  width: 100%;
  border-radius: var(--r1);
  background: radial-gradient(ellipse at 50% 40%, var(--crt-green) 0%, var(--bezel) 85%);
  box-shadow:
    inset 0 0 18px var(--black-90),
    inset 0 0 2px 1px var(--black),
    0 1px 0 var(--white-04);
}
.scope { flex: 1; min-height: 90px; }
.readout {
  font-size: var(--t-value);
  line-height: 1.5;
  color: var(--phos-a-dim);
  min-height: 2.9em;
  max-height: 2.9em;
  overflow: hidden;
  word-break: break-all;
}
.duel-controls { display: flex; gap: var(--s2); }
/* The teaching meter, under the question: no box, the pips and the line. */
.duel-mid { flex: none; display: block; transition: color var(--d-state) ease; }
.duel-mid.learned .teach-copy { text-shadow: 0 0 12px var(--phos-b-35); }
/* The foot: ANOTHER PAIR, the deal rule and the forecast; EVOLVE POOL and
   what each generation did. */
.ev-foot { flex: none; display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s4); }
.ev-foot .duel-deal { flex-wrap: wrap; min-width: 0; }
.ev-foot .duel-pred { text-align: left; }
.ev-evolve { display: grid; justify-items: end; gap: var(--s1); }
.ev-lineage-btn {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--silk-dim);
  background: none;
  border: 0;
  padding: var(--s1);
  cursor: pointer;
}
.ev-lineage-btn::before { content: "›"; display: inline-block; margin-right: var(--s1); transition: transform var(--d-state) var(--e-settle); }
.ev-lineage-btn[aria-expanded="true"]::before { transform: rotate(90deg); }
.ev-lineage-btn:hover, .ev-lineage-btn[aria-expanded="true"] { color: var(--silk); }
.ev-lineage-btn:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
/* EVOLVE POOL before the model has learned from your picks: dashed amber,
   a guess (ADR-012), still pressable. */
#evolve-btn.untaught { background: transparent; border: 1px dashed var(--phos-b-deep); box-shadow: none; }
@media (prefers-reduced-motion: reduce) { .ev-lineage-btn::before { transition: none; } }
.prompt {
  text-align: center;
  font-family: var(--font-voice);
  font-style: italic;
  font-size: var(--t-voice);
  line-height: 1.6;
  color: var(--silk-dim);
}

.hw-btn {
  flex: 1;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--silk);
  padding: var(--s2);
  border-radius: var(--r1);
  border: 1px solid var(--black);
  background: linear-gradient(180deg, var(--btn-hi), var(--btn-lo));
  box-shadow:
    var(--lift),
    0 3px 0 var(--shadow-ink),
    0 5px 8px var(--black-50);
  cursor: pointer;
  transition: transform var(--d-press) ease, box-shadow var(--d-press) ease;
}
.hw-btn:active {
  transform: translateY(2px);
  box-shadow: inset 0 1px 0 var(--white-06), 0 1px 0 var(--shadow-ink);
}
/* Hover is a promise of what a press does: quieter than the press, never a
   change of size. Only where a pointer can hover — on a booth touchscreen the
   last touch would otherwise stay "hovered". The view tabs, TASTE tabs, pads,
   knobs and duel cards get the same treatment further down. */
@media (hover: hover) {
  .hw-btn:hover:not(:disabled) { background: linear-gradient(180deg, var(--btn-hover-hi), var(--btn-hover-lo)); }
}
.hw-btn:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
.hw-btn kbd {
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--silk-dim);
  border: 1px solid var(--hairline);
  border-radius: 3px;
  padding: 1px var(--s1);
  margin-left: var(--s1);
}
.hw-btn.play { color: var(--phos-a); }
.hw-btn.play.playing { box-shadow: inset 0 0 12px var(--phos-a-25), 0 3px 0 var(--shadow-ink); }
/* Its buffer is still rendering (the scope above says so). Dimmed, not
   disabled: a press still asks for that buffer first and plays it on arrival,
   and from the press it wears the bank's dotted ring (`.pending`,
   main.js `awaitRender`), so the press shows it was taken. */
.hw-btn.play.rendering { opacity: 0.5; }
.hw-btn.play.pending { opacity: 1; outline: 1px dotted var(--phos-b); outline-offset: -2px; color: var(--phos-b); }
.hw-btn.choose { color: var(--phos-b); }
.hw-btn.small { flex: none; font-size: var(--t-label); padding: var(--s2) var(--s3); color: var(--phos-b); }
.hw-btn:disabled { opacity: 0.45; cursor: default; }

.lineage-strip {
  position: absolute;
  left: var(--s5);
  right: var(--s5);
  bottom: calc(var(--s7) + var(--s5));
  z-index: 6;
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  padding: var(--s3);
  background: var(--surface-pop);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  box-shadow: var(--shadow-pop);
}
.lineage-strip.hidden { display: none; }
.lineage-strip .panel-label { display: flex; align-items: center; gap: var(--s2); }
.lineage-x { margin-left: auto; }
.lineage-body { display: flex; gap: var(--s4); align-items: stretch; }
#lineage-spark {
  flex: 0 0 240px;
  height: 66px;
  border-radius: var(--r1);
  background: radial-gradient(ellipse at 50% 35%, var(--crt-amber) 0%, var(--bezel) 85%);
  box-shadow: inset 0 0 14px var(--black-85);
}
.lineage-log {
  flex: 1;
  font-size: var(--t-body);
  line-height: 1.65;
  color: var(--phos-b-dim);
  overflow: hidden;
  max-height: 66px;
}
.lineage-log b { color: var(--phos-b); font-weight: 400; }
.lineage-log .gen-tag { color: var(--silk-dim); margin-right: var(--s1); }

@media (hover: hover) {
  .tab:not(.active):hover { color: var(--silk); border-color: var(--silk-mute); }
  .duel-card:not(.live-sel):hover .duel-well { border-color: var(--silk-mute); }
}

/* ---- TASTE and LEARNING: a level's head (taste.js) ----
   The eyebrow, the headline and the one line under it, to prototype v2's
   specimen (docs/notes/vision-2026-09/prototype). The shell (Plan-005
   task 2) may take it for every level. */
/* EVOLVE's cap line: the level, and its generations. */
.ev-cap { flex: none; display: flex; align-items: baseline; gap: var(--s4); padding: 0 var(--s1); }
.lv-eyebrow {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--silk-dim);
}
.lv-title {
  margin: var(--s1) 0 0;
  font-family: var(--font-silk);
  font-size: var(--t-display);
  font-weight: 400;
  line-height: 1.05;
  color: var(--silk);
}
.lv-sub {
  margin: var(--s1) 0 0;
  min-height: 1.45em;
  font-family: var(--font-silk);
  font-size: var(--t-body);
  line-height: 1.45;
  color: var(--silk-dim);
}

/* ---- TASTE: the map ---- */
.ts {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--s3);
  padding: var(--s2) var(--s3) 0;
}
.ts-well {
  position: relative;
  min-height: 0;
  border: 1px solid var(--hairline);
  border-radius: var(--r3);
  background: radial-gradient(ellipse at 50% 35%, var(--crt-amber) 0%, var(--bezel) 85%);
  overflow: hidden;
}
.ts-well:focus-within { border-color: var(--silk-mute); }
/* No double-tap zoom: a tap opens the sound under it. */
.ts-map { position: absolute; inset: 0; width: 100%; height: 100%; touch-action: manipulation; }
.ts-map:focus-visible { outline: none; }
.ts-map.hit { cursor: pointer; }
.ts-legend {
  position: absolute;
  top: var(--s3);
  right: var(--s3);
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s1) var(--s2);
  border-radius: var(--r1);
  background: var(--bezel-72);
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--phos-b-dim);
  opacity: 0;
  transition: opacity var(--d-state);
  pointer-events: none;
}
.ts-legend.on { opacity: 1; }
.tl-halo { display: inline-flex; align-items: center; gap: var(--s1); }
.tl-halo i { display: inline-block; border-radius: 50%; border: 1px solid var(--phos-b-45); background: var(--phos-b-18); }
.tl-halo .sm { width: 8px; height: 8px; }
.tl-halo .lg { width: 16px; height: 16px; background: var(--phos-b-30); border-color: var(--phos-b-80); }
/* Before the first fit every halo is a guess: one dashed ring. */
.ts-legend.guess .tl-halo .sm { display: none; }
.ts-legend.guess .tl-halo .lg { background: transparent; border-style: dashed; border-color: var(--phos-b-60); }
.ts-plate {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 3;
  width: 240px;
  padding: var(--s3);
  border-radius: var(--r2);
  background: var(--panel-94);
  border: 1px solid var(--plate-edge);
  box-shadow: var(--shadow-pop);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--d-state) var(--e-settle);
}
.ts-plate.on { opacity: 1; pointer-events: auto; }
.ts-pl-name {
  font-family: var(--font-silk);
  font-size: var(--t-title);
  font-weight: 500;
  line-height: 1.15;
  color: var(--silk);
  overflow-wrap: anywhere;
}
.ts-pl-like { margin-top: var(--s1); font-family: var(--font-mono); font-size: var(--t-value); color: var(--phos-b); }
.ts-pl-like.guess { color: var(--phos-b-dim); }
.ts-pl-acts { display: flex; gap: var(--s1); margin-top: var(--s3); }
.ts-foot { margin: 0; min-height: 1.45em; font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim); }
/* SOUND / TASTE, at the map's top left (prototype v2's .tog). Its inset and
   its height on a touch screen (44 px, below) are taste-geom.js's
   MAP_TOG_FOOT: the map draws no sound above that, where a press is the
   switch's. */
.ts-tog {
  position: absolute;
  top: var(--s3);
  left: var(--s3);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  height: 30px;
  padding-inline: var(--s3);
  border-radius: var(--r1);
  border: 1px solid var(--hairline);
  background: var(--bezel-72);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
}
.ts-tog .led { width: 6px; height: 6px; border-radius: 50%; background: var(--plate-edge); transition: background var(--d-state), box-shadow var(--d-state); }
.ts-tog .ts-seg { transition: color var(--d-state); }
.ts-tog[aria-pressed="false"] [data-seg="taste"], .ts-tog[aria-pressed="true"] [data-seg="sound"], .ts-tog .ts-sep { color: var(--silk-dim); }
.ts-tog[aria-pressed="false"] [data-seg="sound"], .ts-tog[aria-pressed="true"] [data-seg="taste"] { color: var(--silk); }
.ts-tog[aria-pressed="true"] { border-color: var(--plate-edge); }
.ts-tog[aria-pressed="true"] .led { background: var(--silk); box-shadow: 0 0 8px var(--white-35); }
/* Resting under the model view, which holds the map on its TASTE side. */
.ts-tog:disabled { cursor: default; opacity: 0.7; }
/* Taste over time: a slim track along the map's foot, once there are two
   moments to move between. Its inset and height are taste-geom.js's
   MAP_TRACK_ROOM: the map draws no sound below that. */
.ts-time {
  position: absolute;
  left: var(--s3);
  right: var(--s3);
  bottom: var(--s3);
  height: 46px;
  z-index: 2;
  display: none;
  align-items: center;
  gap: var(--s3);
  padding: 0 var(--s3) 0 var(--s1);
  border-radius: var(--r2);
  background: var(--bezel-72);
  border: 1px solid var(--hairline);
}
.ts-time.on { display: flex; }
.ts-tplay {
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
  color: var(--silk-dim);
  font-size: var(--t-value);
  cursor: pointer;
}
.ts-tplay:hover, .ts-tplay.on { color: var(--silk); background: var(--panel-hi); }
.ts-track { flex: 1; min-width: 0; height: 36px; cursor: pointer; touch-action: none; border-radius: var(--r1); }
.ts-track canvas { display: block; width: 100%; height: 100%; }
.ts-tlabel { flex: none; min-width: 13ch; text-align: right; white-space: nowrap; font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim); }

/* ---- LEARNING: the model room ---- */
.md {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--s4);
  padding: var(--s2) var(--s3);
  overflow-y: auto;
}
.md-body { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: var(--s4); min-height: 0; }
.md-panel {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  min-height: 0;
  padding: var(--s3) var(--s4);
  background: var(--panel);
  border: 1px solid var(--hairline);
  border-radius: var(--r3);
}
.md-ph { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; column-gap: var(--s3); min-height: 24px; }
.md-cap {
  margin: 0;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--silk-dim);
}
.md-none { margin: 0; font-family: var(--font-silk); font-size: var(--t-body); color: var(--silk-dim); }
/* The styles: pick one to see its weights, name it, hear what it rates
   highest. */
.md-chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
.md-chip {
  display: flex;
  align-items: center;
  gap: var(--s1);
  padding-right: var(--s2);
  background: var(--chip);
  border: 1px solid var(--plate-edge);
  border-radius: var(--r3);
}
.md-chip:has(.md-chip-pick[aria-checked="true"]) { border-color: var(--phos-b-dim); }
.md-chip-pick {
  display: flex;
  align-items: center;
  gap: var(--s1);
  padding: var(--s1) 0 var(--s1) var(--s2);
  background: none;
  border: 0;
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--silk-dim);
  cursor: pointer;
}
.md-chip-pick i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.md-chip-name {
  background: transparent;
  border: none;
  outline: none;
  color: var(--silk);
  font: var(--t-body)/1.4 var(--font-mono);
  width: auto;
  max-width: 30ch;
}
.md-chip-play { background: none; border: none; color: var(--phos-a); cursor: pointer; font-size: var(--t-label); padding: 0 2px; }
.md-chip-play:disabled { color: var(--silk-mute); cursor: default; }
/* A ▶ pressed before its sound is here (TASTE's card and a style's
   exemplar): the bank's dotted ring until it plays (main.js `awaitRender`). */
.md-chip-play.pending, .util-btn.pending { outline: 1px dotted var(--phos-b); outline-offset: -2px; color: var(--phos-b); }
/* A mark sits in a slot every row and chip keeps, left of its label, so the
   label never moves whether the mark shows or not (Plan-005 task 1). */
.md-mark {
  flex: none;
  display: inline-block;
  width: 1.1em;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--phos-b-dim);
}
.md-chip-pick[aria-checked="true"] .md-mark::before { content: "●"; color: var(--phos-b); }
.md-bars {
  display: grid;
  grid-auto-rows: minmax(18px, 24px);
  align-content: start;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--plate-edge) transparent;
}
.md-row, .md-scale {
  display: grid;
  grid-template-columns: 1.1em minmax(0, 190px) minmax(0, 1fr) 52px;
  align-items: center;
  gap: var(--s2);
  padding: 0 var(--s1);
}
.md-row { border-radius: var(--r1); }
.md-name { display: flex; align-items: baseline; gap: var(--s2); min-width: 0; white-space: nowrap; }
.md-word { flex: none; font-family: var(--font-silk); font-size: var(--t-body); color: var(--silk); }
.md-row.guess .md-word { color: var(--silk-dim); }
.md-tech { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim); }
.md-track { position: relative; height: 10px; }
.md-zero { position: absolute; left: 50%; top: -3px; bottom: -3px; width: 1px; background: var(--plate-edge); }
.md-bar { position: absolute; top: 1px; height: 8px; border-radius: 2px; background: var(--phos-b); box-shadow: 0 0 8px -2px var(--phos-b-60); }
.md-bar.neg { background: var(--phos-b-dim); }
/* A guess (its whisker crosses zero, taste-geom `pullMark`): a hollow
   outline at GUESS_ALPHA, its whisker at full strength. */
.md-bar.hollow { background: transparent; box-shadow: none; border: 1px solid var(--phos-b); opacity: 0.45; }
.md-whisker { position: absolute; top: 4.5px; height: 1px; background: var(--silk); pointer-events: none; }
/* A weight moves to its new value, after a pick or a REPLAY step. */
.md-bar, .md-whisker { transition: left var(--d-move) var(--e-settle), width var(--d-move) var(--e-settle); }
/* Where it was before the replayed pick: dashed, while the bar moves. */
.md-ghost {
  position: absolute;
  top: 0;
  height: 10px;
  border: 1px dashed var(--phos-b-dim);
  border-radius: 2px;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--d-state);
}
.md-ghost.on { opacity: 1; }
.md-row.lit { background: var(--panel-hi); }
.md-row:focus-visible { outline: 1px solid var(--silk-mute); outline-offset: -1px; }
.md-row.moved .md-word { color: var(--phos-b); }
.md-replay-at { margin-left: auto; font-family: var(--font-mono); font-size: var(--t-value); color: var(--phos-b-dim); }
.md-replay { display: inline-flex; align-items: center; gap: var(--s1); }
.md-replay.on { color: var(--phos-b); border-color: var(--phos-b-dim); }
.md-replay:disabled { opacity: 0.45; cursor: default; }
.md-replay kbd {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--silk-dim);
  border: 1px solid var(--hairline);
  border-radius: 3px;
  padding: 0 var(--s1);
}
.md-val { font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim); text-align: right; font-variant-numeric: tabular-nums; }
.md-more { display: none; align-self: flex-start; }
.md-scale { font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim); }
.md-scale span { grid-column: 3; grid-row: 1; }
.md-scale span:nth-child(1) { justify-self: start; }
.md-scale span:nth-child(2) { justify-self: center; }
.md-scale span:nth-child(3) { justify-self: end; }
/* The map keeps room to be read, and the forecasts their own height: with
   the math open, the room scrolls rather than one panel drawing over the
   next (a panel's `min-height: 0` let its row shrink under its content). */
.md-right { display: grid; grid-template-rows: minmax(200px, 1fr) auto auto; gap: var(--s4); min-height: 0; }
.md-right .md-fcp { min-height: auto; }
.md-mapp { min-height: 0; }
.md-map { position: relative; flex: 1; min-height: 140px; border-radius: var(--r2); background: var(--bezel); }
.md-map canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
.md-maplegend { font-family: var(--font-mono); font-size: var(--t-value); color: var(--phos-b-dim); text-align: right; }
.md-fc { display: flex; align-items: baseline; gap: var(--s3); }
.md-big { flex: none; font-family: var(--font-mono); font-size: var(--t-title); font-weight: 500; color: var(--phos-b); }
.md-big span { font-size: var(--t-body); color: var(--silk-dim); }
.md-fcnote { margin: 0; font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim); }
.md-strip { position: relative; min-width: 0; height: 48px; }
.md-strip canvas { display: block; width: 100%; height: 100%; }
.md-skill { margin: 0; font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim); }
.md-skill:empty { display: none; }
.md-foot { display: grid; gap: var(--s2); }
.md-footrow { display: flex; align-items: center; gap: var(--s3); }
.md-disclose, .md-more {
  background: none;
  border: 0;
  padding: var(--s1) 0;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--silk-dim);
  cursor: pointer;
}
.md-disclose::before, .md-more::before { content: "›"; display: inline-block; margin-right: var(--s1); transition: transform var(--d-state); }
.md-disclose[aria-expanded="true"]::before, .md-more[aria-expanded="true"]::before { transform: rotate(90deg); }
.md-math { padding: var(--s3); background: var(--panel); border: 1px solid var(--hairline); border-radius: var(--r2); }
.md-math code { display: block; margin-bottom: var(--s2); font-family: var(--font-mono); font-size: var(--t-value); color: var(--phos-b); }
.md-math p { margin: 0 0 var(--s1); font-family: var(--font-silk); font-size: var(--t-body); line-height: 1.45; color: var(--silk-dim); max-width: 62ch; }
.md-json {
  width: 100%;
  height: 120px;
  padding: var(--s2);
  background: var(--bezel);
  color: var(--silk-dim);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  font-family: var(--font-mono);
  font-size: var(--t-value);
  resize: vertical;
}
@media (max-width: 1180px) {
  .md-row, .md-scale { grid-template-columns: 1.1em minmax(0, 150px) minmax(0, 1fr) 48px; }
  .md-tech, .md-scale span:nth-child(2) { display: none; }
}
@media (max-width: 860px) {
  /* One column that scrolls (`.md`), each panel at its own height: a panel
     let shrink to the screen's height left the map (200 px, below) drawn
     over the two panels under it on a short phone (#134). The map's panel
     is the body's through `.md-right`'s contents, so both are named. */
  .md { grid-template-rows: auto auto; align-content: start; }
  .md-body { display: flex; flex-direction: column; }
  .md-body > *, .md-right > * { flex: none; }
  .md-right { display: contents; }
  .md-mapp { order: 1; }
  .md-weighs { order: 2; }
  .md-fcp { order: 3; }
  .md-foot { order: 4; }
  .md-map { height: 200px; flex: none; }
  .md-bars { overflow: visible; }
  .md-bars.collapsed .md-row.extra { display: none; }
  .md-more { display: inline-flex; }
}

/* ===================== keyboard dock ===================== */
/* Plan-008, to the prototype: a side column (KEYS, and Z and X around the
   octave), the keybed across the width, and at the right end a compact
   cluster, VOL, MIDI, ● REC and KEYS ⋯, which opens everything else the
   keybed does (main.js, `setKeysPop`). */
.keybar {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: var(--s4);
  min-width: 0;
  padding: var(--s2) var(--s5) var(--s2) var(--s4);
  border-top: 1px solid var(--hairline);
  background: var(--panel);
}
.keys-side {
  flex: none;
  width: 240px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--s1);
}
.keys-side .oct { display: flex; align-items: center; gap: var(--s2); color: var(--silk-dim); font-size: var(--t-value); }
#oct-label { font-size: var(--t-value); color: var(--silk); min-width: 2.2em; text-align: center; }
.oct-word { font-family: var(--font-silk); color: var(--silk-dim); margin-left: var(--s2); }
/* Z and X are printed keys, and they are buttons too. */
.kcap {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  line-height: 1;
  color: var(--silk-dim);
  background: none;
  border: 1px solid var(--kbd-edge);
  border-bottom-width: 2px;
  border-radius: 3px;
  padding: 2px var(--s1) 1px;
  min-width: 18px;
  text-align: center;
  cursor: pointer;
}
.kcap:hover { color: var(--silk); border-color: var(--silk-dim); }
.kcap:active { transform: translateY(1px); border-bottom-width: 1px; }

.keys-cluster {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--s3);
}
.keys-cluster .vol { display: flex; align-items: center; gap: var(--s1); }
.keys-cluster .vol .pc-val { min-width: 42px; }
#vol { accent-color: var(--phos-a); width: 96px; }
.midi-wrap { position: relative; }
.keys-more { position: relative; }
/* KEYS ⋯'s settings float above the dock, from its right end, and never
   move the keybed. */
.keys-pop {
  position: absolute;
  right: 0;
  bottom: calc(100% + 18px);
  z-index: 45;
  display: grid;
  gap: var(--s3);
  justify-items: start;
  padding: var(--s3);
  background: var(--surface-pop);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  box-shadow: var(--shadow-pop);
  white-space: nowrap;
}
/* A narrower window gives the keybed the room: the side column takes only
   its words, then the word "octave" and the volume's length go. */
@media (max-width: 1240px) {
  .keys-side { width: auto; }
  #vol { width: 72px; }
}
@media (max-width: 1080px) {
  .oct-word { display: none; }
  .keys-cluster { gap: var(--s2); }
}
.kp-row { display: flex; align-items: center; gap: var(--s2); }
.kp-row .perf-glide input[type="range"] { width: 120px; }
.kp-row .pc-cap { margin-right: var(--s1); }

.piano {
  flex: 1;
  position: relative;
  display: flex;
  border-radius: var(--r1);
  overflow: hidden;
  border: 1px solid var(--black);
  box-shadow: inset 0 2px 8px var(--black-70);
  touch-action: none;
}
/* Nothing you play should highlight. `user-select: none` alone left the keys
   selectable in every WebKit before 17.4 — which is most iPads in use — so a
   glissando dragged a blue selection across the hint letters and left the
   caret parked on the keybed. It is set on the whole dock, not just the keys:
   a drag that *starts* on the keybar background and crosses the keys selects
   just as readily. Text you might legitimately want — the patch name you are
   renaming, anything typed — opts back in below.

   Deliberately not applied app-wide: readouts, the lineage log and LEARNING
   are prose, and a synth is not a reason to make prose uncopyable. */
.keybar, .piano, .rack-scroll, .tray, .bank-item, .stage-toolbar, .duel-controls {
  -webkit-user-select: none;
  user-select: none;
}
input, textarea, .bi-rename {
  -webkit-user-select: text;
  user-select: text;
}
/* The felt strip above the keybed — the one detail every real keyboard has. */
.piano::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 3px;
  z-index: 3;
  background: linear-gradient(180deg, var(--phos-a-deep), var(--black));
  pointer-events: none;
}
.pkey {
  position: relative;
  flex: 1;
  background: linear-gradient(180deg, var(--pkey-hi) 0%, var(--pkey-mid) 88%, var(--pkey-lo) 100%);
  box-shadow: inset 0 1px 0 var(--white-35);
  border-right: 1px solid var(--pkey-edge);
  border-radius: 0 0 3px 3px;
  cursor: pointer;
}
.pkey:last-child { border-right: none; }
.pkey { transition: background var(--d-state) ease-out; }
.pkey.down { background: linear-gradient(180deg, var(--pkey-down-hi) 0%, var(--pkey-down-lo) 100%); transition: background 30ms linear; } /* token-exempt: a played key lights at once; 90 ms reads as lag under a note */
.pkey .hint {
  position: absolute;
  bottom: 5px;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--pkey-hint);
  pointer-events: none;
}
.pkey.down .hint { color: var(--pkey-down-hint); }
.bkey {
  position: absolute;
  top: 0;
  width: 55%;
  height: 58%;
  left: 70%;
  z-index: 2;
  background: linear-gradient(180deg, var(--bkey-hi) 0%, var(--rack) 90%);
  border: 1px solid var(--black);
  border-top: none;
  border-radius: 0 0 3px 3px;
  cursor: pointer;
  box-shadow: 0 3px 5px var(--black-60);
}
.bkey { transition: background var(--d-state) ease-out; }
.bkey.down { background: linear-gradient(180deg, var(--bkey-down-hi) 0%, var(--bkey-down-lo) 90%); transition: background 30ms linear; } /* token-exempt: a played key lights at once; 90 ms reads as lag under a note */
.bkey .hint {
  position: absolute;
  bottom: 4px;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--silk-dim);
  pointer-events: none;
}
/* Accidentals group like a real keybed — C#/D# converge, F#/G#/A# spread —
   but gently: pushed to true acoustic offsets, the outer accidentals
   overhung E and B so far their heads read squashed. */
.bkey.pc1  { left: 64%; }
.bkey.pc3  { left: 74%; }
.bkey.pc6  { left: 60%; }
.bkey.pc8  { left: 69%; }
.bkey.pc10 { left: 78%; }
/* Every C carries its octave label so the keybed is readable at a glance. */
.pkey .hint b { font-weight: 600; color: var(--pkey-hint-key); }
/* The span the computer keys reach — warm ivory against the plain keys'
   cooler bone, strong enough to read across the room. No `filter` here: a
   filter creates a stacking context per key, which drops the overhanging
   black keys behind their neighbours. */
.pkey.mapped { background: linear-gradient(180deg, var(--pkey-mapped-hi) 0%, var(--pkey-mapped-mid) 88%, var(--pkey-mapped-lo) 100%); }
.pkey:not(.mapped) { background: linear-gradient(180deg, var(--pkey-unmapped-hi) 0%, var(--pkey-unmapped-mid) 88%, var(--pkey-unmapped-lo) 100%); }
/* A pressed key ALWAYS shows green — declared after .mapped so the press
   wins the cascade. Without this, mapped keys pressed from the ASDF row
   gave no visual at all and the keybed read as mis-mapped. */
.pkey.down, .pkey.mapped.down {
  background: linear-gradient(180deg, var(--pkey-down-hi) 0%, var(--pkey-down-lo) 100%);
  transition: background 30ms linear; /* token-exempt: a played key lights at once; 90 ms reads as lag under a note */
}

/* ===================== boot overlay ===================== */
.boot {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--rack-96);
}
.panel {
  position: relative;
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  padding: var(--s4);
  background:
    repeating-linear-gradient(115deg, var(--white-012) 0 1px, transparent 1px 3px),
    linear-gradient(180deg, var(--panel-hi), var(--panel) 60%);
  box-shadow: var(--lift), 0 10px 24px var(--black-45);
}
.screws i {
  position: absolute;
  width: 9px; height: 9px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--nut-hi), var(--pop-lo) 70%);
  box-shadow: inset 0 0 2px var(--black), 0 1px 0 var(--white-06);
}
.screws i::after {
  content: "";
  position: absolute;
  left: 1px; right: 1px; top: 4px;
  height: 1px;
  background: var(--shadow-ink);
  transform: rotate(28deg);
}
.screws i:nth-child(1) { top: 6px; left: 6px; }
.screws i:nth-child(2) { top: 6px; right: 6px; }
.screws i:nth-child(3) { bottom: 6px; left: 6px; }
.screws i:nth-child(4) { bottom: 6px; right: 6px; }
.boot-card { width: min(520px, 92vw); text-align: center; padding: var(--s6) var(--s6) var(--s5); }
.boot-title { font-size: var(--t-body); font-weight: 700; letter-spacing: 0.3em; }
/* The tagline and the descriptor travel together on the first screen
   (www/brand/voice.md, "The line"). */
.boot-tag { margin: var(--s2) 0 0; font-size: var(--t-body); color: var(--silk); }
.boot-desc { margin: var(--s1) auto 0; max-width: 40ch; font-size: var(--t-body); color: var(--silk-dim); line-height: 1.4; }
.boot-sub { margin-top: var(--s2); font-size: var(--t-body); color: var(--silk-dim); letter-spacing: 0.06em; }
.boot-meter {
  margin: var(--s5) auto var(--s2);
  height: 10px;
  border-radius: var(--r1);
  background: var(--bezel);
  box-shadow: inset 0 1px 3px var(--black);
  overflow: hidden;
}
.boot-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, var(--phos-a-dim), var(--phos-a));
  box-shadow: 0 0 10px var(--phos-a-55);
  transition: width var(--d-move) ease;
}
.boot-status { font-size: var(--t-value); color: var(--phos-a); }

/* ---- the three banks ---- */
/* One "?" for both heads. The look was scoped to the bank's head, so the node
   bank's "?" rendered as a bare browser button, a grey square. */
.bank-tour-btn {
  width: 20px; height: 20px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--hairline);
  background: var(--bezel);
  color: var(--silk-dim);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  line-height: 1;
  cursor: pointer;
  flex: 0 0 auto;
}
.bank-tour-btn:hover { color: var(--phos-b); border-color: var(--phos-b-dim); }

/* The walkthrough sits over the rail it is describing. */
.bank-tour {
  position: absolute;
  left: 8px; right: 8px; bottom: 8px;
  /* Capped: it used to take 35% of the list at 252px and 50% at 200px — a
     coach mark covering the thing it is pointing at. */
  max-height: 46%;
  overflow-y: auto;
  z-index: 40;
  border: 1px solid var(--phos-b-dim);
  border-radius: var(--r1);
  background: var(--surface-pop);
  box-shadow: var(--shadow-pop);
  padding: var(--s2) var(--s3) var(--s2);
}
.bank-tour .tour-step {
  font-size: var(--t-value);
  color: var(--phos-b-dim);
  letter-spacing: 0.1em;
}
.bank-tour .tour-title {
  font-family: var(--font-silk);
  font-size: var(--t-body);
  font-weight: 600;
  color: var(--phos-b);
  margin: 2px 0 var(--s1);
}
.bank-tour .tour-body {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  line-height: 1.5;
  color: var(--silk);
}
.bank-tour .tour-body b { font-family: var(--font-silk); font-weight: 600; }
.bank-tour .tour-nav {
  display: flex;
  gap: var(--s1);
  margin-top: var(--s2);
  align-items: center;
}
.bank-tour .tour-nav #tour-next { margin-left: auto; }
.bank-tour .tour-nav button:disabled { opacity: 0.35; cursor: default; }
/* The chip the current step is about. */
/* The catalog's walkthrough reuses the bank tour's panel wholesale — same
   component, same rules, anchored to its own rail. Only the highlight differs,
   because what it points at is a group or a row rather than a chip. */
.nb-tour { left: 10px; right: 10px; max-height: 52%; }
.nb-tour-lit {
  outline: 2px solid var(--phos-b);
  outline-offset: 3px;
  border-radius: var(--r1);
}
.nb-head .bank-tour-btn {
  margin-left: 0;
  /* `.bank-tour-btn` sets a width but not a basis, and this header is a
     space-between flex row — so the circle was shrinking to 11px and going
     oval. The bank's own head gets away with it because it has slack. */
  flex: 0 0 20px;
}

/* ---- duel card flip ---- */
.mini-rack {
  flex: 1;
  min-height: 90px;
  position: relative;
  border-radius: var(--r1);
  background: linear-gradient(180deg, var(--rack-bed-hi), var(--rack-bed-lo));
  box-shadow: inset 0 0 18px var(--black-85), inset 0 0 2px 1px var(--black);
  overflow: hidden;
}
.mini-rack svg { display: block; width: 100%; height: 100%; }
.mini-rack .promote {
  position: absolute;
  right: 8px;
  bottom: 8px;
}

/* ---- structural-edit context menu ---- */
.ctx-menu {
  position: fixed;
  z-index: 60;
  min-width: 190px;
  max-height: 70vh;
  overflow-y: auto;
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  background: var(--surface-pop);
  box-shadow: var(--shadow-pop);
  padding: var(--s1);
}
.ctx-menu .cm-head {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk-dim);
  padding: var(--s1) var(--s2) 3px;
}
.ctx-menu .cm-item {
  display: block;
  width: 100%;
  text-align: left;
  /* Sentence-case silk like every other popover item — mono is for values,
     not commands. */
  font-family: var(--font-silk);
  font-size: var(--t-body);
  color: var(--silk);
  background: none;
  border: none;
  border-radius: var(--r1);
  padding: var(--s2);
  cursor: pointer;
}
.ctx-menu .cm-item:hover { background: var(--phos-a-09); color: var(--phos-a); }
.ctx-menu .cm-item.danger:hover { background: var(--led-red-12); color: var(--led-red); }
.mod-menu-btn { font-family: var(--font-mono); font-size: var(--t-rack-label); fill: var(--silk-dim); cursor: pointer; }
.mod-menu-btn:hover { fill: var(--phos-a); }

/* ---- the play body: the well, and the shelf under it ---- */
.play-body { flex: 1; min-height: 0; display: flex; gap: var(--s2); }
.play-canvas { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--s2); }
.play-canvas .rack-frame { flex: 1; }

/* ================= the catalog (C2a: `pt-cat`, on demand in the well) =================
   Over the well's left, from under its top line to over its foot: the
   search, what is in this patch, the groups, the legend. Opened by ADD
   MODULE and /, closed by × and Esc. */
.nodebank {
  position: absolute;
  left: var(--s3);
  top: var(--s3);
  bottom: calc(var(--s3) + 40px);
  z-index: 11;
  width: 248px;
  display: flex;
  align-items: stretch;
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: var(--panel-94);
  box-shadow: var(--shadow-pop);
  overflow: hidden;
}
.nodebank[hidden] { display: none; }
.nodebank .nb-body { width: 100%; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
.nb-collapse {
  margin-left: var(--s1);
  width: 24px;
  height: 24px;
  border: 1px solid transparent;
  border-radius: var(--r1);
  background: none;
  color: var(--silk-dim);
  cursor: pointer;
  font-size: var(--t-label);
  transition: color var(--d-press) ease-out;
}
.nb-collapse:hover, .nb-collapse:focus-visible { color: var(--silk); border-color: var(--hairline); }
@media (pointer: coarse) { .nb-collapse { width: 40px; height: 40px; } }
/* θ, what the model thinks of a module, is the model view's (⌥). */
body:not(.model-view) #nodebank .ni-theta { display: none; }

/* The header is a bezel band, so the catalog reads as an instrument's
   silkscreen rather than as a sidebar title. */
.nb-head {
  position: sticky;
  top: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  /* Same band as .bank-head, to the pixel. */
  padding: var(--s2) var(--s3);
  background: var(--bezel);
  border-bottom: 1px solid var(--hairline);
}
.nb-head .mono { font-size: var(--t-value); color: var(--silk-dim); }

/* ---- search: the index. Machined like .perf-sel, never a native widget. ---- */
.nb-search { position: relative; margin: 0 var(--s3); }
.nb-q {
  width: 100%;
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--phos-a-dim);
  background: linear-gradient(180deg, var(--plate-hi), var(--plate-lo));
  border: 1px solid var(--black);
  border-radius: var(--r1);
  box-shadow: var(--lift), 0 1px 0 var(--shadow-ink);
  padding: var(--s1) var(--s5) var(--s1) var(--s2);
}
.nb-q::placeholder { color: var(--silk-dim); }
.nb-q:focus { color: var(--phos-a); outline-offset: 1px; }
.nb-q::-webkit-search-cancel-button { -webkit-appearance: none; }
.nb-slash {
  position: absolute;
  right: 7px; top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--silk-mute);
  pointer-events: none;
}

/* ---- groups: signal-flow order, not enum order ---- */
.nb-group { margin: 0 var(--s3); }
.nb-group.empty { display: none; }
/* At 41 modules the rail is three viewports tall, so the group you are inside
   has to stay named while you scroll it — otherwise the only landmarks in the
   catalog are the ones that have just left the screen. Parks directly under
   the header band, which is sticky at 0.
   
   The offset is measured from the header at build time rather than written
   here: it was hard-coded at 37px and the `?` button changed the header's
   height to 44, which would have left a 7px gap that nothing points at. */
.nb-sect {
  position: sticky;
  top: var(--nb-head-h, 40px);
  z-index: 2;
  margin: var(--s2) 0 var(--s1);
  background: linear-gradient(180deg, var(--recess-hi) 70%, var(--recess-hi-00));
  padding-bottom: 2px;
}
/* A group header is NOT a chip, and it must not read as one. The caret sits in
   its own 14px gutter, the row has no plate, no bevel, no lift and no press —
   the only thing hover changes is the label's brightness. Everything below it
   is a pressable object with a shelf to travel into; a header that shared that
   language invited the same click, and clicking one while a module was in your
   hand folded a group away while pick mode stayed silently armed. */
.nb-fold {
  display: grid;
  grid-template-columns: 14px auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0 var(--s1);
  width: 100%;
  background: none;
  border: none;
  border-bottom: 1px solid var(--hairline);
  padding: 0 0 3px;
  cursor: pointer;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--phos-a-dim);
}
.nb-group.amber .nb-fold { color: var(--phos-b-dim); }
.nb-fold:hover { color: var(--phos-a); }
.nb-group.amber .nb-fold:hover { color: var(--phos-b); }
/* Inert while something is in your hand: the rail's job for the length of a
   placement is to narrow, not to rearrange itself under the pointer. The row
   still answers when clicked (see the handler) — a control that goes quiet
   teaches nothing about why. */
#nb-groups.armed .nb-fold,
#nb-groups.armed .nb-fold:hover { color: var(--silk-mute); cursor: default; }
.nb-caret {
  width: 7px; height: 5px;
  flex: 0 0 auto;
  transition: transform var(--d-state) ease-out;
}
.nb-caret path { fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; }
.nb-group.folded .nb-caret { transform: rotate(-90deg); }
.nb-group.folded .nb-list, .nb-group.folded .nb-why { display: none; }
/* The section note is the one place lowercase prose is allowed in a header
   band — it says what the group is FOR, which the single word cannot. */
.nb-note {
  flex: 1;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--silk-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.nb-n { font-size: var(--t-value); color: var(--silk-dim); letter-spacing: 0; }
.nb-list { display: flex; flex-direction: column; gap: 3px; padding: var(--s1) 0 2px; }

/* ---- the chip: glyph, name, port signature, belief ---- */
.nb-item {
  display: grid;
  /* glyph · name · port signature · belief — one 30px row. A second row for
     the belief bar would be honest but 19 modules × 44px is 836px of scroll,
     and the catalog has to be scannable in one look. */
  grid-template-columns: 22px minmax(0, 1fr) auto 34px;
  align-items: center;
  gap: var(--s1);
  width: 100%;
  padding: var(--s1) var(--s2);
  text-align: left;
  background: linear-gradient(180deg, var(--card-hi), var(--card-lo));
  border: 1px solid var(--black);
  border-radius: var(--r1);
  /* Light from 315°, a black seam, and a shelf to travel into — the same
     physics every other pressable object in the app has. Without it the rail's
     nineteen most-repeated elements read as web cards on an instrument. */
  box-shadow: var(--lift), 0 2px 0 var(--shadow-ink);
  cursor: pointer;
  transition: border-color var(--d-press) ease-out, color var(--d-press) ease-out,
              transform var(--d-press) ease-out, box-shadow var(--d-press) ease-out;
}
.nb-item.hidden { display: none; }
.ni-name {
  /* The silk face at body size. Law 2 (tracking) applies to case, not to face:
     this is lowercase, so it gets none. The rail was set in a single 10px role
     throughout, which put the catalog's headline below the rack's own
     silkscreen of the same word in size, weight and face. */
  font-family: var(--font-silk);
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--silk);
  letter-spacing: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The glyph answers one question — what does this do to a wave — nineteen
   times, so the set cannot drift as it grows. Drawn in the STROKE tier at
   rest: hover intensifies the phosphor rather than revealing it. */
.nb-glyph { width: 20px; height: 14px; overflow: visible; display: block; }
.nb-glyph .gl, .nb-glyph .gl-ghost, .nb-glyph .gl-rule {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke var(--d-press) ease-out;
}
.nb-glyph .gl { stroke: var(--phos-a-deep); stroke-width: 1.5; }
.nb-glyph .gl-ghost { stroke: var(--phos-a-deep); stroke-width: 1; opacity: 0.5; }
.nb-glyph .gl-rule { stroke: var(--silk-mute); stroke-width: 0.9; stroke-dasharray: 2 2; }
/* A solid mark, not a reference rule: an arrowhead drawn with a 2-2 dash
   renders as two disconnected stubs. */
.nb-glyph .gl-mark, .sp-glyph .gl-mark {
  fill: none; stroke: var(--silk-mute); stroke-width: 0.9;
  stroke-linecap: round; stroke-linejoin: miter;
}
.nb-item.mod .nb-glyph .gl,
.nb-item.mod .nb-glyph .gl-ghost { stroke: var(--phos-b-deep); }

/* The port signature: both phosphors at rest, and the only place in the app
   that states a module's shape before you have committed to it. */
.ni-pips { display: inline-flex; align-items: center; gap: 2px; }
.ni-pips .pip {
  width: 5px; height: 5px;
  border-radius: 50%;
  /* The `-deep` tier is calibrated against the rack void (#0a0c0a). On the
     raised chip it loses a third of its contrast, so the port signature —
     the thing that puts both phosphors to work at rest — sits at the dim tier
     and still intensifies rather than appears on hover. */
  border: 1px solid var(--phos-a-dim);
  transition: border-color var(--d-press) ease-out;
}
.ni-pips .pip.mod { border-color: var(--phos-b-dim); border-style: dashed; }
.ni-pips .pip-arrow { width: 8px; height: 6px; color: var(--silk-mute); flex: 0 0 auto; }
.ni-pips .pip-arrow path { fill: none; stroke: currentColor; stroke-width: 1; stroke-linecap: round; }

/* A belief, never a decoration: the bar is θ under the dominant style and the
   whisker is ±σ. Below the support threshold there is no bar at all — a short
   bar and "no evidence" must not look alike. */
/* The belief column only exists once the model has been fitted at all — 19
   identical placeholders on a fresh session is noise pretending to be data. */
.ni-theta { display: none; }
.has-belief .ni-theta { position: relative; width: 34px; height: 9px; display: block; }
/* The zero line, drawn only where there is a bar to be left or right OF it. */
.has-belief .ni-theta:not(.thin)::before {
  content: "";
  position: absolute;
  left: 17px; top: 0; bottom: 0;
  width: 1px;
  background: var(--silk-mute);
  opacity: 0.55;
}
/* No evidence (not measured, not fitted, too few patches): a dash. Never a
   short bar — "the model barely likes this" and "the model has never seen
   this" must not look alike. */
.has-belief .ni-theta.thin::after {
  content: "";
  position: absolute;
  left: 13px; right: 13px; top: 4px;
  height: 1px;
  background: var(--silk-mute);
  opacity: 0.45;
}
/* Bar and whisker are placed inline, in px from the cell's left edge, by
   `nbPaintTheta` (taste-geom `pullMark`): the whisker spans the ±σ interval
   around the mean, not from zero, so a guess's whisker crosses the rule. */
.ni-theta .tb-bar, .ni-theta .tb-whisk {
  position: absolute;
  top: 3px;
  height: 3px;
  border-radius: 1px;
}
/* A guess: the same bar, hollow — a 1px outline at the guess alpha. Its
   colour is the style's, written inline as the border. */
.ni-theta.guess .tb-bar {
  top: 2px; height: 5px;
  box-sizing: border-box;
  border: 1px solid var(--phos-b);
  background: none;
  opacity: 0.45;
}
/* The whisker is the uncertainty, so it has to be readable — it was drawn in
   the stroke tier at 1px, which is the least visible thing in the rail
   carrying the most important qualification in it. On a guess it is the
   reading that matters, so it is drawn at full strength. */
.ni-theta .tb-whisk {
  top: 4px;
  height: 1px;
  background: var(--phos-b-dim);
}
.ni-theta.guess .tb-whisk { background: var(--silk); }
/* An end cut at the cell's edge is marked as cut, never drawn shorter. */
.ni-theta .tb-whisk.cut-lo::before,
.ni-theta .tb-whisk.cut-hi::after {
  content: "";
  position: absolute;
  top: -2px;
  border-top: 2.5px solid transparent;
  border-bottom: 2.5px solid transparent;
}
.ni-theta .tb-whisk.cut-lo::before { left: -1px; border-right: 3px solid currentColor; }
.ni-theta .tb-whisk.cut-hi::after { right: -1px; border-left: 3px solid currentColor; }
.ni-theta .tb-whisk { color: var(--phos-b-dim); }
.ni-theta.guess .tb-whisk { color: var(--silk); }

/* ---- states: the palette was the only clickable hardware with no travel ---- */
.nb-item:hover { border-color: var(--phos-a-dim); }
.nb-item:hover .ni-name { color: var(--phos-a); }
.nb-item:hover .nb-glyph .gl { stroke: var(--phos-a); }
.nb-item:hover .ni-pips .pip { border-color: var(--phos-a); }
.nb-item.mod:hover { border-color: var(--phos-b-dim); }
.nb-item.mod:hover .ni-name { color: var(--phos-b); }
.nb-item.mod:hover .nb-glyph .gl { stroke: var(--phos-b); }
.nb-item.mod:hover .ni-pips .pip.mod { border-color: var(--phos-b); }
.nb-item:active {
  transform: translateY(2px);
  box-shadow: inset 0 1px 0 var(--white-05);
}
.nb-item.armed {
  border-color: var(--phos-a);
  box-shadow: inset 0 0 0 1px var(--phos-a-deep), 0 0 10px var(--phos-a-18);
}
.nb-item.armed .ni-name { color: var(--phos-a); }
.nb-item.armed .nb-glyph .gl { stroke: var(--phos-a); }
.nb-item.mod.armed {
  border-color: var(--phos-b);
  box-shadow: inset 0 0 0 1px var(--phos-b-deep), 0 0 10px var(--phos-b-18);
}
.nb-item.mod.armed .ni-name { color: var(--phos-b); }
.nb-item.mod.armed .nb-glyph .gl { stroke: var(--phos-b); }
.nb-item.unavailable { opacity: 0.4; cursor: not-allowed; }
.nb-item.unavailable:hover { border-color: var(--hairline); }
.nb-item.unavailable:hover .ni-name { color: var(--silk); }
.nb-item.unavailable:hover .nb-glyph .gl { stroke: var(--phos-a-deep); }
/* The one place the app broke its own colour law: a modulation control with a
   green focus ring. */
.nb-item.mod:focus-visible { outline-color: var(--phos-b); }

/* Unavailable is explained in place, never silent. */
.nb-why, .nb-empty, .nb-none {
  margin: var(--s1) var(--s3) 0;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  line-height: 1.55;
  color: var(--silk-dim);
}
.nb-none { font-family: var(--font-silk); font-size: var(--t-body); }
.nb-inline {
  background: none; border: none; padding: 0;
  font: inherit;
  color: var(--phos-a);
  text-decoration: underline dotted;
  cursor: pointer;
}

/* The armed line: what is in your hand, and how to put it down. */
.nb-status {
  position: sticky;
  bottom: 0;
  z-index: 3;
  margin: var(--s1) 0 0;
  padding: var(--s2) var(--s3);
  font-size: var(--t-value);
  line-height: 1.5;
  color: var(--phos-a);
  background: var(--bezel);
  border-top: 1px solid var(--phos-a-deep);
}
.nb-status:empty { display: none; }
.nb-status b { font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; }
.sp-dim { color: var(--silk-dim); }

/* The colour law as a legend — in the stroke tier it actually legends, and no
   longer hidden from assistive tech, since it is the app's only statement of
   what green and amber mean. */
.nb-legend { display: flex; flex-direction: column; gap: 3px; margin: var(--s3) var(--s3) 0; }
.nb-legend .lg-row {
  display: flex; align-items: center; gap: var(--s2);
  font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim);
}
.nb-legend .lg-swatch { width: 22px; height: 0; border-top: 3px solid var(--phos-a-deep); border-radius: 2px; }
.nb-legend .lg-swatch.mod { width: 22px; border-top: 1.8px dashed var(--phos-b-deep); }

/* ---- IN THIS PATCH: the rail as a legend for the canvas beside it ---- */
.nb-inpatch { margin: var(--s1) var(--s3) 0; }
.nb-inpatch.hidden { display: none; }
.nb-inpatch .nb-sect {
  display: flex; align-items: baseline; gap: var(--s1);
  border-bottom: 1px solid var(--hairline);
  padding-bottom: 3px;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--silk-dim);
}
.nb-inpatch .nb-n { margin-left: auto; }
.nb-chips { display: flex; flex-wrap: wrap; gap: 3px; padding: var(--s1) 0 2px; }
.nb-chip {
  display: inline-flex; align-items: center; gap: var(--s1);
  padding: 2px var(--s1) 2px var(--s1);
  background: var(--white-02);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--silk-dim);
  cursor: pointer;
  transition: border-color var(--d-press) ease-out, color var(--d-press) ease-out;
}
.nb-chip .nb-glyph { width: 14px; height: 10px; }
/* An empty socket in the in-patch list (p4). It used to print the substitute
   node's name — "vco" — beside a canvas drawing a dashed EMPTY plate, so the
   two surfaces disagreed about a destructive act the player had just made. It
   borrows the plate's own language instead: the dashed stroke, the muted
   silkscreen colour, and no belief cell, because the model has no opinion
   about absence. */
.nb-chip.empty {
  color: var(--silk-mute);
  border-style: dashed;
  background: none;
}
.nb-chip.empty .nb-glyph .gl { stroke: var(--silk-mute); stroke-width: 1.2; }
.nb-chip.empty:hover { color: var(--silk-dim); border-color: var(--silk-mute); }
.nb-chip.empty .ni-theta { display: none; }
/* The belief cell rides on the in-patch chip too — narrower, because the chip
   is a pill in a wrapping row rather than a full-width catalog row, but the
   same three marks and the same refusal to draw a bar without evidence. */
.nb-chips.has-belief .ni-theta { width: 24px; margin-left: 2px; }
.nb-chips.has-belief .ni-theta:not(.thin)::before { left: 12px; }
.nb-chips.has-belief .ni-theta.thin::after { left: 8px; right: 8px; }
/* The bar and whisker are placed inline against this cell's own 11 px each
   side of zero, on the rail's one scale (`nbPaintTheta`), with an end past
   the edge cut and marked there — never clamped into a shorter belief. */
.nb-chip:hover { border-color: var(--phos-a-dim); color: var(--phos-a); }
.nb-chip.mod:hover { border-color: var(--phos-b-dim); color: var(--phos-b); }

/* ---- the spec card: one sentence, the ports, and what the model hears ---- */
.nb-spec {
  position: fixed;
  z-index: 60;
  width: 244px;
  padding: var(--s3) var(--s3) var(--s3);
  border-radius: var(--r1);
  background: var(--surface-pop);
  box-shadow: var(--shadow-pop);
  pointer-events: none;
}
.nb-spec.hidden { display: none; }

/* ---- the readout's parts (`#pt-read`, renderSpecDock) ---- */
.sd-id { display: flex; align-items: flex-start; gap: var(--s2); min-width: 0; }
.sd-idtext { min-width: 0; }
.sd-name { display: block; margin-bottom: 3px; }
.sd-ports { font-size: var(--t-value); line-height: 1.6; color: var(--phos-a-dim); }
.sd-body { min-width: 0; }
.sd-body .sp-blurb { margin: 0 0 var(--s1); }
.sd-strip { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s4); font-size: var(--t-value); line-height: 1.6; }
.sd-model {
  font-size: var(--t-value);
  line-height: 1.6;
  color: var(--silk-dim);
  border-left: 1px solid var(--hairline);
  padding-left: var(--s4);
  min-width: 0;
}
/* One line, and it names the thing in your hand — the strip stops being a
   description the moment the question becomes "where does this go". */
.sd-line { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; min-width: 0; }
.sd-line b { font-family: var(--font-silk); font-size: var(--t-body); color: var(--phos-a); letter-spacing: 0.02em; }
.sd-verb {
  font-size: var(--t-label); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--silk-dim);
}
.sd-blurb {
  font-family: var(--font-silk); font-size: var(--t-body);
  color: var(--silk-dim); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sd-hint { font-size: var(--t-value); color: var(--silk-dim); margin-left: auto; white-space: nowrap; }
.sd-hint kbd { font-size: var(--t-label); color: var(--silk-dim); }

/* ---- priced sockets (WS-2 §5) ----
   The number sits in a recessed well so it reads as an instrument readout
   rather than as more prose, and its sign carries the colour. Green is the
   signal channel everywhere else in this app, so a *prediction* deliberately
   does not get it: the price is the model talking, and the model's colour on
   this instrument is amber. */
.sd-price {
  display: inline-flex; align-items: baseline; gap: var(--s1);
  padding: 2px var(--s2);
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  background: var(--black-25);
  font-size: var(--t-value);
  white-space: nowrap;
  cursor: help;
}
.pr { font-weight: 500; letter-spacing: 0.02em; }
.pr.up { color: var(--phos-b); }
.pr.down { color: var(--led-red); }
.pr.pr-flat { color: var(--silk-dim); }
.pr.pr-mute { color: var(--silk-mute); }
.pr-dim { color: var(--silk-dim); }
.pr-note {
  color: var(--silk-dim); text-transform: uppercase;
  letter-spacing: 0.14em; font-size: var(--t-label);
  border-bottom: 1px dotted var(--hairline);
}
.nb-price { display: block; margin-top: 3px; font-family: var(--font-mono); font-size: var(--t-value); }

/* ---- pre-placement audition (WS-2 §6) ---- */
.pv { display: inline-flex; align-items: center; gap: var(--s2); }
.pv-play {
  width: 22px; height: 22px; flex: 0 0 22px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--hairline); border-radius: 50%;
  background: var(--black-25);
  color: var(--phos-a);
  font-size: 9px; /* token-exempt: a glyph sized to its 22px button */
  line-height: 1; cursor: pointer;
  padding: 0 0 0 1px;
}
.pv-play:hover:not(:disabled) { color: var(--white); border-color: var(--phos-a-deep); background: var(--phos-a-10); }
.pv-play:disabled { color: var(--silk-mute); cursor: not-allowed; }
.pv-play.playing { color: var(--white); box-shadow: 0 0 8px var(--phos-a-45); }
/* Tall enough to be a measurement. At 22px the trace, its zero line and the
   two full-scale references all landed within a few pixels of each other and
   the audition read as a flat line whatever it actually was. */
.pv-scope {
  width: 156px; height: 38px;
  border: 1px solid var(--hairline); border-radius: 3px;
  background: var(--black-35);
}
.pv-label { font-size: var(--t-value); color: var(--silk-dim); white-space: nowrap; }
/* Rendering is a half-second of somebody else's CPU: say so by dimming the
   thing that is about to change rather than by adding a spinner. */
.pv.busy .pv-scope { opacity: 0.45; }
.pv.busy .pv-label { color: var(--phos-b-deep); }
.pv.dead .pv-scope { opacity: 0.3; }
.sp-glyph.small { width: 24px; height: 17px; }
.sp-head { display: flex; align-items: center; gap: var(--s2); margin-bottom: var(--s1); }
.sp-glyph { width: 34px; height: 24px; overflow: visible; }
.sp-glyph .gl { fill: none; stroke: var(--phos-a); stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
.sp-glyph .gl-ghost { fill: none; stroke: var(--phos-a-deep); stroke-width: 1; stroke-linecap: round; }
.sp-glyph .gl-rule { fill: none; stroke: var(--silk-mute); stroke-width: 0.8; stroke-dasharray: 2 2; }
.sp-glyph.mod .gl { stroke: var(--phos-b); }
.sp-glyph.mod .gl-ghost { stroke: var(--phos-b-deep); }
/* A module's sentence is plain: the voice face (Newsreader italic) is the
   model's alone (www/brand/voice.md, ADR-013), and a module description is
   not the model speaking. */
.sp-blurb {
  margin: 0 0 var(--s2);
  font-family: var(--font-silk);
  font-size: var(--t-body);
  line-height: 1.5;
  color: var(--silk);
}
.sp-ports, .sp-params, .sp-model, .sp-heard {
  font-size: var(--t-value);
  line-height: 1.6;
  color: var(--silk-dim);
}
.sp-ports { color: var(--phos-a-dim); }
.sp-params { color: var(--silk-dim); }
.sp-model { margin-top: var(--s1); border-top: 1px solid var(--hairline); padding-top: var(--s1); }
.sp-heard { color: var(--silk-dim); }
.sp-heard b { color: var(--phos-b-dim); font-weight: 400; }
.sp-dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin: 0 var(--s1) -1px 0;
}
.sp-belief { color: var(--phos-b-dim); }

/* Screen-reader-only live region for the arm/place announcements. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- tray (staged, unwired modules) ----
   One line. It was a 64px shelf of two-line cards, and the moment anything was
   unplugged the rack gave up those 64px and refitted smaller — at 1280×800
   the frame fell to 118px. Everything a held module needs is on one line: its
   jack (the drag source), its name, what it will bring with it (clipped, the
   whole strip in its tooltip) and ✕. */
.tray {
  flex: 0 0 36px;
  display: flex;
  align-items: center;
  gap: var(--s2);
  border: 1px dashed var(--tray-edge);
  border-radius: var(--r1);
  background: var(--shadow-ink-60);
  padding: 3px var(--s2);
}
/* Set across, like every other panel label: a one-line strip has no height
   for the word to run down. */
.tray-label {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--silk-dim);
  align-self: center;
}
.tray-items { flex: 1; display: flex; gap: var(--s2); overflow-x: auto; align-items: center; }
.tray-hint { font-family: var(--font-silk); font-size: var(--t-label); color: var(--silk-dim); }
/* An empty tray is a slot, not a 74px dashed hole in the rack. It opens when
   a module is staged. */
.tray:not(:has(.tray-item)) {
  flex-basis: 26px;
  border-style: solid;
  border-color: var(--tray-empty-edge);
  align-items: center;
}
.tray:not(:has(.tray-item)) .tray-hint { opacity: 0.55; }
/* A staged module is a small faceplate, not a name tag: its jack, its name,
   and the parameters it will bring with it — in a row, so the strip stays one
   line. The head's three parts join the row (`display: contents`) with the
   parameters slotted in before the ✕. */
.tray-item {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--s2);
  border: 1px dashed var(--nut-hi);
  border-radius: var(--r1);
  background: linear-gradient(180deg, var(--lifted), var(--module-lo));
  padding: 3px var(--s2);
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--silk);
  max-width: 320px;
}
.tray-item .ti-head { display: contents; }
.tray-item .ti-params { order: 1; min-width: 0; }
.tray-item .t-x { order: 2; }
.tray-item .ti-name {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.tray-item .ti-params {
  color: var(--silk-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tray-item .t-jack {
  width: 13px; height: 13px;
  border-radius: 50%;
  border: 2px solid var(--phos-a);
  background: radial-gradient(circle at 40% 35%, var(--crt-green), var(--black));
  cursor: grab;
  box-shadow: 0 0 6px var(--phos-a-35);
}
.tray-item.mod .t-jack {
  border-color: var(--phos-b);
  box-shadow: 0 0 6px var(--phos-b-35);
}
.tray-item .t-x {
  background: none; border: none; color: var(--silk-dim);
  font-size: var(--t-label); cursor: pointer; padding: 0 2px;
}
.tray-item .t-x:hover { color: var(--led-red); }
/* On its way into the patch: it stays on the shelf until the engine has taken
   it (a shelf that empties on a promise is how a held chain got lost), but it
   is inert while it waits, so it cannot be placed twice. */
.tray-item.pending { opacity: 0.5; }
.tray-item.pending .t-jack { cursor: progress; box-shadow: none; }
.tray-item.pending .t-x { cursor: default; }

/* ---- jacks & wires ---- */
.jack { cursor: crosshair; }
.jack circle {
  fill: var(--tray-jack);
  stroke: var(--phos-a-deep);
  stroke-width: 1.6;
}
.jack.modjack circle { stroke: var(--phos-b-deep); }
.jack text {
  font-family: var(--font-mono);
  font-size: var(--t-rack-micro);
  fill: var(--silk-dim);
  pointer-events: none;
}
/* A lit socket takes the pointer by its circles' boxes, not their strokes: the
   stroke changes with the socket's state (hot, previewed, dashed), and a
   pointer resting on the ring's edge went in and out with it, about thirty
   times a second, redrawing the module in hand each time. */
.jack.legal circle { pointer-events: bounding-box; }
.jack.legal circle {
  stroke: var(--phos-a);
  filter: drop-shadow(0 0 4px var(--phos-a-90));
  animation: jack-pulse 700ms ease-in-out infinite; /* token-exempt: a loop's period, not a transition */
}
/* A source EVICTS what is in the socket; a processor splices in front of it.
   Those two drops used to look identical right up until one of them had
   already thrown a chain away. Amber is the warning, and it is on the target
   before the pointer gets there. */
.jack.legal.replaces circle {
  stroke: var(--phos-b);
  filter: drop-shadow(0 0 4px var(--phos-b-90));
}
.jack.legal.replaces text { fill: var(--phos-b); }
/* The keyboard's cursor through the lit sockets. */
.jack.hot circle:nth-of-type(1) { fill: var(--phos-a-16); }
.jack.hot circle { stroke-width: 2; animation: none; }
.jack.legal.replaces.hot circle:nth-of-type(1) { fill: var(--phos-b-16); }
/* The socket the audition strip is about while the pointer is elsewhere —
   usually on its way to the strip's ▶. The strip names it in words; this is
   where it is. Dashed, so it cannot be mistaken for the keyboard's cursor. */
.jack.previewed circle { stroke-width: 2; stroke-dasharray: 3 2; animation: none; }
/* Something is in your hand: the whole canvas says so. */
.rack-scroll.placing { cursor: crosshair; }
.rack-scroll.placing #rack-svg { cursor: crosshair; }
.jack.modjack.legal circle {
  stroke: var(--phos-b);
  filter: drop-shadow(0 0 4px var(--phos-b-90));
}
@keyframes jack-pulse { 50% { stroke-width: 3; } }
.wiring .jack:not(.legal) { opacity: 0.35; }
/* The output a cable is currently coming out of, while it is out. */
.wiring .jack[data-outkey].hot { opacity: 1; }
.wiring .jack[data-outkey].hot circle { stroke: var(--phos-a); stroke-width: 2.4; }

/* ---- an empty socket ----
   The grammar has no hole: every input is filled, so an unplugged socket is
   still a real node underneath. The plate is what says otherwise — dashed,
   unpopulated, and asking for something. */
.mod-plate.placeholder {
  fill: var(--white-012);
  stroke: var(--silk-dim);
  stroke-width: 1.2;
  stroke-dasharray: 6 5;
}
.mod-title.empty { fill: var(--silk-dim); }
.plate-hint {
  font-family: var(--font-mono);
  font-size: var(--t-rack-micro);
  fill: var(--silk-dim);
  pointer-events: none;
}

/* ---- pick mode ----
   Arming used to be invisible on the canvas. Dimming is the cheapest way to
   say "not these", and it leaves the targets at full contrast rather than
   painting a highlight over half the rack. */
#rack-svg.picking g[data-key].dimmed { opacity: 0.28; }
#rack-svg.picking g[data-key] { transition: opacity var(--d-state) cubic-bezier(0.2, 0, 0.6, 1); }
/* Not a target: it is drawn over the cable the socket cuts, and must never
   take the pointer off the socket under it. */
.pick-caret { pointer-events: none; }
.pick-caret path {
  stroke: var(--phos-a);
  stroke-width: 2;
  stroke-linecap: round;
  fill: none;
  filter: drop-shadow(0 0 5px var(--phos-a-80));
  animation: caret-pulse 900ms ease-in-out infinite; /* token-exempt: a loop's period, not a transition */
}
@keyframes caret-pulse { 50% { opacity: 0.35; } }
.pick-halo {
  fill: none;
  stroke: var(--phos-a);
  stroke-width: 1.4;
  stroke-dasharray: 5 4;
  opacity: 0.75;
  pointer-events: none;
}
/* Amber is the app's one word for "something here goes away". */
.pick-halo.replaces { stroke: var(--phos-b); opacity: 0.95; stroke-width: 1.8; }

/* ---- the ghost plate ----
   The module in your hand, drawn where it will land, at rack scale. Greyed and
   desaturated so it can never be mistaken for a module that is already in the
   patch, and dashed so its edge reads as a proposal rather than a panel. It is
   not a target for anything — the sockets under it still are. */
/* Nearly opaque, and desaturated to the bone. An insertion genuinely lands in
   the 28-unit gutter between two plates, so a translucent ghost there reads as
   a smear across both of them; a near-solid grey card reads as an object lying
   on the panel, which is what a proposal should look like. Grey plus a dashed
   edge is what keeps it from being mistaken for a module already in the patch,
   since every real plate in the rack is lit green or amber. */
.pick-ghost {
  pointer-events: none;
  opacity: 0.92;
  /* The cast is what says "above the panel, not part of it". */
  filter: grayscale(0.85) drop-shadow(0 6px 12px var(--black-75));
  animation: ghost-in var(--d-state) cubic-bezier(0.2, 0, 0.6, 1);
}
/* Opacity only: a CSS transform on an SVG group replaces its `transform`
   attribute while it runs, so a scale here drew the ghost at the rack's
   origin, and a ghost redrawn under a still pointer stayed there. */
@keyframes ghost-in { from { opacity: 0; } }
.pick-ghost .mod-plate {
  fill: var(--ghost-plate);
  stroke: var(--phos-a);
  stroke-width: 1.3;
  stroke-dasharray: 6 4;
}
.pick-ghost.modside .mod-plate { stroke: var(--phos-b); }
/* Stood clear of the plates it would cover: a dashed lead to where it goes. */
.pick-ghost.pick-lead { fill: none; stroke: var(--phos-a-45); stroke-width: 1.2; stroke-dasharray: 4 4; filter: none; animation: none; }
.pick-ghost .knob-body { filter: none; }
@media (prefers-reduced-motion: reduce) {
  .pick-ghost { animation: none; }
}

.pick-chip {
  position: absolute;
  z-index: 10;
  left: var(--s4);
  top: var(--s3);
  max-width: calc(100% - var(--s7));
  display: flex;
  align-items: center;
  gap: var(--s1);
  padding: 3px var(--s1) 3px var(--s2);
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  background: var(--surface-pop);
  box-shadow: 0 4px 14px var(--black-55);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  letter-spacing: 0.06em;
  color: var(--silk);
  white-space: nowrap;
  pointer-events: auto;
}
.pick-chip .pick-chip-text { text-transform: uppercase; }
/* The price, on the plate. Separated by a rule rather than a gap so the chip
   reads as one object with two facts in it — what happens, and what it is
   expected to be worth. */
.pick-chip .pick-chip-price {
  border-left: 1px solid var(--hairline);
  padding-left: var(--s1);
  letter-spacing: 0;
  font-size: var(--t-value);
}
.pick-chip .pick-chip-price.hidden { display: none; }
.pick-chip kbd {
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--silk-dim);
  border: 1px solid var(--hairline);
  border-radius: 3px;
  padding: 0 3px;
}
.pick-chip-x {
  background: none; border: none; padding: 0 2px; cursor: pointer;
  color: var(--silk-dim); font-size: var(--t-label); line-height: 1;
}
.pick-chip-x:hover { color: var(--led-red); }

/* The chooser's second line: what this row will do to the patch, in the
   patch's own words, before it happens. */
.ctx-menu .cm-item.cm-two { line-height: 1.35; }
.ctx-menu .cm-item .cm-sub {
  display: block;
  margin-top: 2px;
  max-width: 30ch;
  white-space: normal;
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--silk-dim);
}
.ctx-menu .cm-item:hover .cm-sub { color: var(--silk-mute); }

/* ---- the structure menu's header, borrowed from the node bank's spec card ----
   The menu and the card describe the same objects, so they say so in the same
   language: the module's own transfer-function glyph, then its silkscreen. */
.ctx-menu .cm-title {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s1) var(--s2) var(--s1);
  border-bottom: 1px solid var(--hairline);
  margin-bottom: var(--s1);
}
.ctx-menu .cm-title-name {
  font-family: var(--font-silk);
  font-size: var(--t-body);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--silk);
}
/* Physical distance is the point: `delete` is not one more row in the list,
   and a hand traveling to it should have to notice that it did. */
.ctx-menu .cm-item.cm-sep {
  margin-top: var(--s2);
  padding-top: var(--s2);
  border-top: 1px solid var(--hairline);
}
.ctx-menu .cm-item.danger { color: var(--silk); }
.ctx-menu .cm-item.danger::before {
  content: "";
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 50%;
  margin-right: var(--s2);
  vertical-align: 1px;
  background: var(--led-red);
  box-shadow: 0 0 5px var(--led-red-50);
}
/* A verb that cannot apply here still prints — the list of what a module can
   do should not change shape under the hand — but it is dimmed and it says
   why on hover, which is this app's standing treatment for a refusal. */
.ctx-menu .cm-item[disabled] {
  color: var(--silk-dim);
  opacity: 0.45;
  cursor: not-allowed;
}
.ctx-menu .cm-item[disabled]:hover { background: none; color: var(--silk-dim); }
/* Clamped at 70vh, or to the room it opens in (main.js `showMenu`, the well
   for the structure menu): end the list in a fade, not in a clean cut that
   reads as "that is all of them". */
.ctx-menu.cm-clamped {
  mask-image: linear-gradient(to bottom, var(--black) calc(100% - 26px), transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, var(--black) calc(100% - 26px), transparent 100%);
}

/* A module plate is a tabstop now; it needs a ring like every other one. */
.plate-focus { fill: none; stroke: none; pointer-events: none; }
.mod-group:focus { outline: none; }
.mod-group:focus-visible .plate-focus {
  stroke: var(--phos-a);
  stroke-width: 1.5;
  stroke-dasharray: 5 4;
}
/* Why a fragment is on the shelf, when it did not simply come out of a socket. */
.ti-why {
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--phos-b);
  letter-spacing: 0.04em;
  margin-left: var(--s1);
}

#wire-overlay {
  position: fixed;
  inset: 0;
  z-index: 55;
  pointer-events: none;
  width: 100vw;
  height: 100vh;
}
#wire-overlay path {
  fill: none;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-dasharray: 7 5;
}
#wire-overlay path.audio { stroke: var(--phos-a-85); filter: drop-shadow(0 0 5px var(--phos-a-60)); }
#wire-overlay path.mod { stroke: var(--phos-b-85); filter: drop-shadow(0 0 5px var(--phos-b-60)); }

/* ---- responsive ---- */
/* A and B stay side by side at every width — the comparison IS the
   interaction. The previous rule dropped B onto a third grid row inside an
   `overflow: hidden` view, so the cards overprinted the controls. */
@media (max-width: 1280px) {
  .util-btn { padding: var(--s1) var(--s2); }
}
@media (max-width: 1080px) {
  .bank { width: 228px; flex-basis: 228px; } /* 200px of rows and the faces' column */
  .bank-head { padding: var(--s3) var(--s2) var(--s2); }
  .btab { letter-spacing: 0.08em; }
  .duel-mid { gap: var(--s3); }
}
/* Short laptops: the deck is the elastic part, so give the fixed chrome less.
   The breakpoint was 860px, which put the plan's own second test size — 1280×900,
   a MacBook — on the *tall* side of a rule written for exactly that machine.
   940 covers it. Nothing in the block changed; it simply now applies where it
   was always meant to. */
@media (max-height: 940px) {
  /* (The dock is the prototype's 78 px at every height now; it gave ground
     here when it was 128.) */
  /* A truncated hint is worse than no hint — the group word already carries
     the meaning, and the note is the part that can be dropped. */
  .nb-note { display: none; }
  /* Same rule for the search field: at the clamp minimum the placeholder is
     the teaching, and 18px of keycap is what pushes it into an ellipsis. The
     shortcut is still in ADD MODULE's title and ⌘K's Add a module. */
  .nb-slash { display: none; }
  .nb-q { padding-right: var(--s2); }
}

@media (prefers-reduced-motion: reduce) {
  .nb-item, .nb-chip, .nb-glyph .gl, .nb-glyph .gl-ghost, .ni-pips .pip, .nb-caret {
    transition: none !important;
  }
  .nb-item:active { transform: none; }
  .jack.legal circle { animation: none; }
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* ---------- performance controls (keybar) ---------- */
.perf-ctl { display: flex; align-items: center; gap: var(--s1); flex-wrap: wrap; }
/* Selects and the tempo readout wear the same machined material as the
   buttons beside them — nothing in the keybar is a native widget. */
.perf-sel, .perf-num {
  color: var(--phos-a-dim);
  background: linear-gradient(180deg, var(--plate-hi), var(--plate-lo));
  border: 1px solid var(--black);
  border-radius: 3px;
  box-shadow: var(--lift), 0 1px 0 var(--shadow-ink);
  font: var(--t-value)/1.4 var(--font-mono);
  padding: 3px var(--s1);
  cursor: pointer;
}
.perf-sel:hover, .perf-num:hover { color: var(--phos-a); }
.perf-num { width: 44px; text-align: center; }
/* The spinners are the most native pixels a number input has. */
.perf-num::-webkit-inner-spin-button,
.perf-num::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.perf-glide { display: flex; align-items: center; gap: 3px; font-size: var(--t-value); color: var(--phos-a-dim); }
.perf-glide input[type="range"] { width: 54px; }
.midi-ind { font-size: var(--t-value); color: var(--silk-dim); }
.midi-ind.on { color: var(--phos-a); text-shadow: 0 0 6px var(--phos-a-60); }
#rec-btn.lit { color: var(--led-red); border-color: var(--led-red); text-shadow: 0 0 8px var(--led-red-80); }

/* ---------- a style's badge, and the duel's prediction ---------- */
.style-badge { display: inline-flex; align-items: center; gap: var(--s1); font-size: var(--t-value); color: var(--silk-dim); }
.style-badge i { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
/* A card's style and its guess before you pick are the model view's (⌥):
   the style keeps its place at rest, so the name never moves when it shows;
   the guess hangs under the card's head, over its figure. */
.style-badge { visibility: hidden; opacity: 0; transition: opacity var(--d-state), visibility var(--d-state); }
body.model-view .style-badge { visibility: visible; opacity: 1; }
.duel-guess {
  position: absolute;
  top: calc(var(--s3) + 30px);
  right: var(--s3);
  padding: 2px var(--s2);
  border: 1px solid var(--phos-b-deep);
  border-radius: 999px;
  background: var(--rack);
  font-size: var(--t-value);
  color: var(--phos-b);
  white-space: nowrap;
  pointer-events: none;
  z-index: 2;
}
.duel-guess:empty, body:not(.model-view) .duel-guess { display: none; }
.duel-pred { font-size: var(--t-value); color: var(--silk-dim); text-align: center; min-height: 12px; }
.duel-pred.hit { color: var(--phos-a); }
.duel-pred.miss { color: var(--phos-b); }

/* ---------- living modulation (tier 4) ---------- */
.wire.mod.pulse { animation: modpulse linear infinite; }
.jack.modjack.pulse circle { animation: jackpulse 1.2s ease-in-out infinite; } /* token-exempt: a loop's period, not a transition */
@keyframes modpulse { 0%,100% { stroke-opacity: 0.45; } 50% { stroke-opacity: 1; } }
@keyframes jackpulse { 0%,100% { fill-opacity: 0.55; } 50% { fill-opacity: 1; } }

/* ---------- duel staging ---------- */
.duel-card.deal { animation: deal var(--d-move) cubic-bezier(0.2, 0.9, 0.3, 1.1); }
@keyframes deal {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to { opacity: 1; transform: none; }
}

/* ---------- quick duel strip (PLAY) ---------- */
.play-duel {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s1) var(--s3); font-size: var(--t-value);
  border-top: 1px solid var(--play-duel-edge); background: var(--play-duel-glass);
}
.play-duel .pd-label { font-size: var(--t-label); letter-spacing: 0.12em; text-transform: uppercase; }
.pd-side {
  background: var(--pd-side); border: 1px solid var(--plate-edge); border-radius: 3px;
  color: var(--phos-a); font: var(--t-value)/1.4 var(--font-mono);
  padding: 2px var(--s2); cursor: pointer; max-width: 198px; /* 180 px of name, and the face's 14 and its 4 px */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pd-side:hover { border-color: var(--phos-a); }
/* A name still on its way: dimmed text, not a green "…" that reads as the
   patch's name. */
.pd-side .pd-wait { color: var(--silk-dim); }

/* ---------- ⌘K: the one list (shell.js) ----------
   Over everything at a level, under a dialog that opens from it: a field,
   then what it finds in three groups, the row chosen lit. */
.cmdk-scrim { position: fixed; inset: 0; z-index: 60; background: var(--scrim-82); }
.cmdk-scrim.hidden, .cmdk.hidden { display: none; }
.cmdk {
  position: fixed;
  left: 50%;
  top: 14vh;
  z-index: 61;
  width: min(640px, calc(100vw - 32px));
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  max-height: 72vh;
  border: 1px solid var(--hairline);
  border-radius: var(--r3);
  background: var(--panel);
  box-shadow: var(--shadow-pop);
  animation: cmdk-in var(--d-state) var(--e-settle);
}
@keyframes cmdk-in { from { opacity: 0; transform: translateX(-50%) translateY(-6px); } }
.cmdk-q {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s4) var(--s4);
  border-bottom: 1px solid var(--hairline);
  color: var(--silk-mute);
}
.cmdk-q input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: none;
  color: var(--silk);
  font-family: var(--font-silk);
  font-size: var(--t-body);
}
.cmdk-q input:focus-visible { outline: none; }
.cmdk-q input::placeholder { color: var(--silk-mute); }
.cmdk-list { list-style: none; margin: 0; padding: var(--s1); overflow-y: auto; min-height: 0; flex: 1 1 auto; }
.cmdk-list.none { display: none; }
.cmdk-grp {
  padding: var(--s3) var(--s3) var(--s1);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk-mute);
}
.cmdk-it {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: 40px;
  padding: 0 var(--s3);
  border-radius: var(--r2);
  color: var(--silk);
  font-size: var(--t-body);
  cursor: pointer;
}
.cmdk-it[aria-selected="true"] { background: var(--plate-hi); }
.cmdk-it mark { background: none; color: var(--phos-a); }
.cmdk-ico { flex: none; width: 24px; display: grid; place-items: center; color: var(--silk-dim); font-size: var(--t-value); }
.cmdk-ico .face-slot { display: block; }
.cmdk-lab { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmdk-hint { flex: none; margin-left: auto; display: flex; align-items: center; gap: var(--s1); color: var(--silk-mute); font-size: var(--t-value); }
.cmdk-foot {
  display: flex;
  align-items: center;
  gap: var(--s4);
  padding: var(--s2) var(--s4);
  border-top: 1px solid var(--hairline);
  color: var(--silk-mute);
  font-size: var(--t-label);
}
.cmdk-fine { margin-left: auto; }
.cmdk-fine a { color: var(--silk-mute); }
.cmdk-fine a:hover { color: var(--phos-a); }
@media (pointer: coarse) { .cmdk-it { min-height: 48px; } }

/* Keys are named in the readout face everywhere, not the browser's monospace. */
kbd { font-family: var(--font-mono); }

/* ---------- touch pass ----------
   A coarse pointer is a real input, not a degraded mouse. Everything here is
   inside `@media (pointer: coarse)` on purpose: a mouse must see exactly the
   instrument it saw before, so nothing below may leak into the desktop
   cascade. Two laws:

   1. WHAT HOVER REVEALS, TOUCH CANNOT REACH. A finger has no hover state, so
      every affordance gated behind `:hover` is, on a tablet, a feature that
      does not exist. Those are revealed here instead — dimmed rather than
      lit, so they read as available rather than active.
   2. A GESTURE MUST BE CLAIMED BEFORE THE BROWSER CLAIMS IT. Panning wins
      ties, so a control that owns a drag says so (`touch-action`), and a
      frame that must scroll says that too. See `claimGesture` in main.js —
      CSS alone doesn't hold in every engine for SVG children. */
@media (pointer: coarse) {
  .util-btn, .inspect-btn, .hw-btn { min-height: 30px; min-width: 30px; }
  /* Only the invisible hit circle grows. `.jack circle` hit all three — the
     knurled nut (6.5), the dark bore (3.4) and the hit target (5.5) — and a
     CSS geometry property beats the presentation attribute, so every jack on
     every touch device rendered as one flat 8px disc with its bore as wide
     as its nut. The jack is the thing you aim a cable at; it has to keep
     looking like a socket while getting easier to hit. */
  .jack circle:last-of-type { r: 9; }
  .perf-glide input[type="range"], #vol { min-height: 26px; }

  /* Law 1 — the hover-gated affordances, made reachable. The knob lock dot
     is the third one and it is NOT here: `.lock-dot` is re-declared further
     down this file, and a later rule of equal specificity wins no matter
     what a media query wraps it in. It sits with its own kin instead. */
  /* A bank row's actions and a preset's ▶, on every row at rest: a tap on
     a row opens it (and goes to PATCH), so hover-to-reveal would leave a
     finger no way to hear, rate, save or cut a sound without opening it.
     They sit on a line of their own under the name, as a mouse's do, but
     in the row's flow and a finger's size, the whole width of the row's
     content (the buttons give up width before a cut would be left out of
     reach, down to 31 px each for a bred sound's five in the narrowest
     bank), so no name is covered or cut, and the stars, as tall as the
     buttons, take the line they leave as it is: a row is as tall with them
     out as without. What sits over the row's end (the seed and
     may-be-replaced words, the model view's percentage, IN POOL) stays
     level with the name. */
  .bank-item { gap: 2px; }
  .bank-item .bi-acts {
    position: static;
    bottom: auto;
    transform: none;
    visibility: visible;
    opacity: 1;
    background: none;
    padding-left: 0;
  }
  .bank-item .bi-acts button { flex: 0 1 44px; min-width: 0; min-height: 44px; }
  .bank-item .bi-acts .stars { flex: 1 1 0; min-width: 0; }
  .bank-item .bi-acts { gap: 0; }
  body.model-view .bank-item:not(:hover):not(.rating) .bi-acts { visibility: visible; opacity: 1; }
  .preset-item { padding-right: calc(var(--s2) + 52px); } /* token-exempt: the width IN POOL takes, kept on every preset row here, where it is level with the name */
  .bank-item .bi-flag,
  .bank-item .bi-pct,
  .preset-item .pb-in { top: var(--s1); bottom: auto; transform: none; height: 20px; }
  .bank-item:hover .bi-flag, .bank-item.under .bi-flag, .bank-item.kbd .bi-flag, .bank-item.rating .bi-flag { visibility: visible; }
  /* The ▶ is on its own line here, so IN POOL keeps the row's end. */
  .preset-item:is(:hover, .under, :focus-within, .kbd, :has(.bi-hear.playing)) .pb-in { right: var(--s1); background: none; }

  /* Law 2 — gesture ownership.
     The rack frame no longer scrolls: it is a camera, and the drag and the
     pinch are both handled in JS against the viewBox. Handing the browser
     `pan-x pan-y` here would give it a scroll that does not exist and eat
     the one-finger pan before the app ever sees it. */
  .rack-scroll { touch-action: none; }
  .bank-list, .nb-body, .tray-items { overscroll-behavior: contain; }

  /* A knob held for half a second is a gesture, not a request for the iOS
     callout menu and a selection of the label underneath it. */
  .rack-scroll, .piano, .duel-card, .tray { -webkit-touch-callout: none; }
  #rack-svg { user-select: none; -webkit-user-select: none; }

  /* Double-tap to rename has to beat double-tap to zoom. */
  .bank-item .bi-name { touch-action: manipulation; }

  /* The grey flash the OS paints over a tapped element belongs to a web page,
     not to a faceplate — the app draws its own :active states. */
  * { -webkit-tap-highlight-color: transparent; }
}

/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  .wire.pulse, .wire.mod.pulse,
  .jack.modjack.pulse circle, .duel-card.deal { animation: none !important; }
}

/* =====================================================================
   Pass 7 — the surfaces added to make the instrument legible and alive.
   ===================================================================== */

/* ---------- focus ----------
   Only .hw-btn had a focus ring, on a near-black palette where the UA default
   is invisible, and two rules actively removed outlines. Everything gets one.
   The :where() floor catches any HTML control the list below misses (the
   warm-start ▶, the hood rows, a HELD item's ✕ all showed the browser's own
   ring), at zero specificity so every control-specific rule still wins. SVG
   is left out: the rack draws its own focus (.plate-focus, knob bodies). */
:where(button, a[href], summary, label[tabindex], div[tabindex], canvas[tabindex], [role="button"]):focus-visible {
  outline: 2px solid var(--phos-a);
  outline-offset: 2px;
}
/* A level takes focus when its stop on the rail is clicked, so its keys work
   on arrival (shell.js). It is a landmark, not a control: no ring. */
.view[tabindex="-1"]:focus { outline: none; }
.util-btn:focus-visible,
.tab:focus-visible,
.inspect-btn:focus-visible,
.nb-item:focus-visible,
.nb-fold:focus-visible,
.nb-chip:focus-visible,
.nb-inline:focus-visible,
.nb-collapse:focus-visible,
.cm-item:focus-visible,
.warm-item:focus-visible,
.bank-item:focus-visible,
.bank-list:focus-visible,
.star:focus-visible,
.bi-kill:focus-visible,
.bi-hear:focus-visible,
.bi-save:focus-visible,
.bi-star:focus-visible,
.bi-cmp:focus-visible,
.bank-tour button:focus-visible,
.bank-tour-btn:focus-visible,
.nextstep:focus-visible,
.pd-side:focus-visible,
.btab:focus-visible,
.wi-play:focus-visible,
.pf-hood-row:focus-visible,
.tray-item .t-x:focus-visible,
.md-chip-pick:focus-visible,
.md-chip-play:focus-visible,
.toast-undo:focus-visible,
input:focus-visible,
select:focus-visible,
[data-addr]:focus-visible {
  outline: 2px solid var(--phos-a);
  outline-offset: 2px;
  /* No border-radius here: the ring must follow each element's own shape.
     A shared 3px radius squared the .nextstep pill and snapped .hw-btn on
     every keyboard step. */
}
[data-addr]:focus-visible .knob-body { stroke: var(--phos-a); stroke-width: 2; }

/* ---------- next step ----------
   Nothing in the app ever answered "what now?". This always does: over
   PATCH's acts, at the head's top right, in a line the head always keeps
   (empty, it holds its height), so nothing moves when it changes. */
.nextstep {
  /* One line in a cell that can be narrower than the longest callout; an
     ellipsis, never a two-line pill that grows the header. */
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-silk);
  font-size: var(--t-body);
  color: var(--phos-b);
  background: var(--phos-b-07);
  border: 1px solid var(--phos-b-deep);
  border-radius: 999px;
  padding: var(--s1) var(--s3);
  cursor: pointer;
  transition: background var(--d-state) ease, border-color var(--d-state) ease;
}
.nextstep:hover { background: var(--phos-b-14); border-color: var(--phos-b); }
.nextstep.inert { color: var(--silk-dim); border-color: var(--hairline); background: none; cursor: default; }

/* ---------- the live scope ----------
   Sits in the rack's *frame*, so the circuit you are looking at is the thing
   that appears to be running — and so the trace stays put when the camera
   moves. Parented inside the viewport it was percentage-positioned against
   the scroll content: it rode over modules and slid out from under itself on
   every pan. */
/* The bezel. A trace drawn straight onto the panel is a decal; a trace behind
   glass in a housing is an instrument. Inset highlight top-left agrees with
   the 315° lamp everything else on the rack is lit by, and the inner shadow
   is the glass sitting below the bezel's lip. */
.scope-shell {
  position: absolute;
  z-index: 4;
  pointer-events: none;
  border-radius: var(--r1);
  border: 1px solid var(--hairline);
  background: var(--bezel);
  box-shadow: var(--lift), inset 0 0 12px var(--black-90), 0 4px 14px var(--black-50);
  overflow: hidden;
}
.scope-shell.hidden { display: none; }
/* The bezel does not fade — it is hardware bolted to the frame, and hardware
   that dissolves when the patch goes quiet is not hardware. What fades is the
   *trace*, down to the floor the player set. */
.scope-shell .live-scope { opacity: var(--scope-floor, 0.35); transition: opacity var(--d-move) ease; }
.scope-shell.live .live-scope { opacity: 1; }
/* …with one exception, and it is the reserve's other half. The fit keeps the
   patch out from under the bezel when it can afford to; when it cannot (the
   reserve would cost the patch a fifth of its size) or a *pan* puts plates
   under it, the promise ("the trace never rides over a module") can only be
   kept by the glass. It used to duck to 30% and stay there, a dim bezel drawn
   straight through ENV / OUT's sustain and release knobs — neither out of the
   way nor readable. It goes altogether now, and comes back the moment the
   plates are out from under it: the scope is an optional, aria-hidden
   picture of the sound, and a module you are reading is neither. */
.scope-shell { transition: opacity var(--d-state) ease; }
.scope-shell.ducked { opacity: 0; }
/* Four corners, three sizes. The scope is in the rack *frame*, so none of
   this moves when the camera does. */
.scope-shell.corner-br { right: 8px; bottom: 8px; }
.scope-shell.corner-bl { left: 8px; bottom: 8px; }
.scope-shell.corner-tr { right: 8px; top: 8px; }
.scope-shell.corner-tl { left: 8px; top: 8px; }
/* Every dimension carries the same factor, so the bezel keeps its proportions
   when the fit has to shrink it (`scopeCapBezel`): the size the player asked
   for is the size they get until it would cost the patch its reserve, and then
   it is scaled down rather than the reserve being dropped. `--scope-scale` is
   1 unless the frame is too short to hold the requested bezel beside a
   readable patch. */
.scope-shell { --scope-scale: 1; }
.scope-shell.size-s {
  width: calc(24% * var(--scope-scale)); height: calc(22% * var(--scope-scale));
  min-width: calc(150px * var(--scope-scale)); min-height: calc(70px * var(--scope-scale));
}
.scope-shell.size-m {
  width: calc(36% * var(--scope-scale)); height: calc(30% * var(--scope-scale));
  min-width: calc(200px * var(--scope-scale)); min-height: calc(90px * var(--scope-scale));
}
.scope-shell.size-l {
  width: calc(50% * var(--scope-scale)); height: calc(42% * var(--scope-scale));
  min-width: calc(260px * var(--scope-scale)); min-height: calc(120px * var(--scope-scale));
}
.live-scope { display: block; width: 100%; height: 100%; }
/* The caption names the tap. "What the instrument does" and "what I hear" are
   two different signals and the app used to pick one silently. */
.scope-cap {
  position: absolute;
  left: 7px; bottom: 4px;
  font-size: var(--t-label); /* a page label on the glass, not drawn through the rack's camera */
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--silk-dim);
  pointer-events: none;
  /* A spectrum fills the bottom of the screen, which is exactly where the
     caption is: the halo keeps it readable without a plate behind it. */
  text-shadow: 0 0 4px var(--bezel), 0 0 8px var(--bezel);
}

/* ---------- the scope's settings panel ----------
   Under ⌘K's button at the menu bar's right end, fixed to the window: the
   menu bar clips what hangs inside it (it keeps to one row), and a panel
   inside it was cut to the bar's height, its first field scrolled out of
   reach as it took the focus. A dialog on the ladder (55, the MIDI panel's
   step), so under ⌘K's scrim (60): one left open when the list opens is
   covered by it, and a click there closes the list, as anywhere else. */
.scope-panel {
  position: fixed;
  right: var(--s4); top: calc(var(--menubar-h) + 6px);
  z-index: 55;
  width: 300px;
  max-height: 70vh;
  overflow-y: auto;
  padding: var(--s3);
  border-radius: var(--r1);
  border: 1px solid var(--hairline);
  background: var(--panel);
  box-shadow: var(--shadow-pop);
  text-align: left;
}
.scope-panel.hidden { display: none; }
.sp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--s2); }
.sp-title {
  font-family: var(--font-silk); font-size: var(--t-label);
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--silk-dim);
}
.sp-grid { display: flex; flex-direction: column; gap: var(--s1); }
.sp-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  font-size: var(--t-label); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--silk-dim); cursor: pointer;
}
.sp-row select, .sp-row input[type="range"] {
  flex: 0 0 130px;
  font-family: var(--font-mono); font-size: var(--t-value);
  color: var(--silk); background: var(--bezel);
  border: 1px solid var(--hairline); border-radius: 3px;
  padding: 2px var(--s1);
}
.sp-row.sp-check { justify-content: flex-start; }
.sp-row.sp-check input { flex: none; }
.sp-val { font-family: var(--font-mono); font-style: normal; color: var(--phos-a-dim); margin-left: var(--s1); }
.sp-note {
  margin-top: var(--s2);
  font-family: var(--font-silk);
  font-size: var(--t-label); color: var(--silk-dim); line-height: 1.5;
}

/* ---------- the image-export dialog (WS-7 §3) ----------
   Deliberately the *same* shell as the scope panel — same surface, same
   shadow, same row grammar — because a second popover with its own look would
   say these are two different kinds of thing, and they are not: both are a
   handful of settings hung off the header's ⋯. Only the width and the two
   pieces the scope panel has no equivalent of are new. */
.image-panel { width: 268px; }
/* `flex: 0 0 130px` alone does not hold a `<select>` to 130px: a flex item's
   `min-width` is `auto`, so the longest option ("selection — nothing
   selected") sets a floor and that one row grows wider than the four beside
   it. The rows have to agree or the panel reads as four controls and an
   accident. */
.image-panel .sp-row select { min-width: 0; }
/* The measured result, before the button rather than after the download. Its
   own recessed strip so it reads as a readout and not as another control. */
.ix-dims {
  margin-top: var(--s3);
  padding: var(--s1) var(--s2);
  border: 1px solid var(--hairline); border-radius: 3px;
  background: var(--bezel);
  font-size: var(--t-value); color: var(--phos-a-dim);
  text-align: center;
}
.ix-dims.busy { color: var(--silk-dim); }
/* The only filled control in the panel, because it is the only one that does
   anything — everything above it just describes what it will do. */
.ix-go {
  margin-top: var(--s2);
  width: 100%;
  padding: var(--s2) 0;
  border: 1px solid var(--phos-b-deep); border-radius: 3px;
  background: linear-gradient(180deg, var(--ix-go-hi), var(--ix-go-lo));
  color: var(--phos-b);
  font-family: var(--font-silk); font-size: var(--t-label);
  letter-spacing: 0.16em; text-transform: uppercase;
  cursor: pointer;
}
.ix-go:hover { border-color: var(--phos-b); background: linear-gradient(180deg, var(--ix-go-hover-hi), var(--ix-go-hover-lo)); }
.ix-go:disabled { opacity: 0.4; cursor: default; }
.sp-row select:disabled { opacity: 0.45; }
.sp-row option:disabled { color: var(--silk-mute); }

/* ---------- the drop target ----------
   A veil rather than a rim light on the frame: a dragged file has no obvious
   home in a three-pane app, and the honest answer is "anywhere". */
.drop-veil {
  position: fixed; inset: 0; z-index: 90;
  display: flex; align-items: center; justify-content: center;
  background: var(--bezel-72);
  backdrop-filter: blur(2px);
  /* The veil must never eat the dragleave that dismisses it. */
  pointer-events: none;
}
.drop-veil.hidden { display: none; }
.dv-card {
  padding: var(--s5) var(--s6);
  border: 1px dashed var(--phos-a-deep); border-radius: var(--r2);
  background: var(--surface-pop); box-shadow: var(--shadow-pop);
  text-align: center;
}
.dv-mark { color: var(--phos-a); line-height: 1; font-size: 26px; } /* token-exempt: a glyph, the drop target's mark */
.dv-line {
  margin-top: var(--s2);
  font-family: var(--font-silk); font-size: var(--t-title);
  letter-spacing: 0.1em; color: var(--silk);
}
.dv-sub { margin-top: var(--s1); font-size: var(--t-value); color: var(--silk-dim); letter-spacing: 0.1em; }

/* ---------- the offscreen export stage ----------
   `buildRack` draws into a live document or it draws nothing: `fitLabels`
   needs `getBBox`, and the whole style-inlining pass reads `getComputedStyle`,
   both of which want a laid-out element in a real stylesheet's scope. So the
   export renders on stage and off screen, rather than in a detached fragment
   where every colour would come back as an initial value. */
.export-stage {
  position: fixed; left: -20000px; top: 0;
  width: 1200px; height: 900px;
  opacity: 0; pointer-events: none;
}

/* ---------- rack material ----------
   A bevel is a lit edge *and the roll-off behind it*. One 1px white line is a
   border; a 2px highlight over a 6px gradient falling away from it is an edge
   with thickness, and the eye reads the second as an object. Bottom shade and
   left edge complete the 315° lamp the plate, knob, screw and nut all agree
   on. */
.plate-lit { fill: var(--white-13); pointer-events: none; }
.plate-roll { pointer-events: none; }
.plate-shade { fill: var(--black-75); pointer-events: none; }
.plate-edge { fill: var(--white-06); pointer-events: none; }
.plate-screw { opacity: 0.85; pointer-events: none; }
.mod-plate { transition: stroke var(--d-move) cubic-bezier(0.2, 0, 0.6, 1); }
/* Note-on lights the amp plate: picture tied to sound, at the module where
   the sound leaves the patch. */
.mod-plate.struck { stroke: var(--phos-a); transition: stroke 30ms linear; } /* token-exempt: a struck plate lights at once, with the note */

.knob-ticks line { stroke: var(--knob-tick); stroke-width: 1; }
.knob-track { fill: none; stroke: var(--hairline); stroke-width: 2.5; stroke-linecap: round; }
/* The value arc is the single change that turns a clock hand into a control
   you can read across the room. */
/* The glow used to be unconditional, which meant ten knobs glowing at all
   times: a blur region per knob for the compositor to paint every frame, and
   — worse — a signal that meant nothing because it was always on. It now
   means "this one is moving": under the hand, being dragged, or being driven
   by a cable. */
.knob-arc {
  fill: none;
  stroke: var(--phos-a);
  stroke-width: 2.5;
  stroke-linecap: round;
  pointer-events: none;
}
.knob-arc.modside { stroke: var(--phos-b); }
g:hover > .knob-arc, .dragging > .knob-arc, .knob-arc.modulated {
  filter: drop-shadow(0 0 3px var(--phos-a-55));
}
g:hover > .knob-arc.modside, .dragging > .knob-arc.modside, .knob-arc.modside.modulated {
  filter: drop-shadow(0 0 3px var(--phos-b-55));
}
/* A knob a cable is actually driving pulses at rest — the one place on the
   plate where "something else is turning this" has to be legible without
   tracing the amber run back to its source. */
.knob-arc.modulated { animation: arcbreathe 2.4s ease-in-out infinite; } /* token-exempt: a loop's period, not a transition */
@keyframes arcbreathe { 50% { opacity: 0.55; } }

.j-nut { fill: url(#jackNut); pointer-events: none; }
.j-bore { fill: var(--jack-bore); pointer-events: none; }
.j-spec { fill: none; stroke: var(--white-18); stroke-width: 1; pointer-events: none; }
/* The outermost circle is the jack's hit target — it draws the green ring and
   is otherwise unfilled. Unfilled is the problem: SVG hit-testing defaults to
   `visiblePainted`, which means *painted* pixels only, so with no fill the
   whole middle of a jack was dead and the only thing you could grab or drop a
   cable on was the 1.6px ring itself. Scanning across a jack's 11px diameter
   found exactly two 2px bands that responded — the left and right edges of
   the stroke — with eight dead pixels between them. Patching by hand meant
   hitting a sliver, and the nut and bore drawn inside it are `pointer-events:
   none`, so they could not help. `all` hit-tests the disc whether or not it
   is painted, which is what a socket has always looked like it did. */
.jack circle:last-of-type { fill: none; pointer-events: all; }

/* Ten knobs meant ten amber dots glowing at all times, for nothing. Only a
   knob that is actually locked speaks; the rest appear on hover or focus. */
.lock-dot { opacity: 0; transition: opacity var(--d-state) ease; }
.lock-dot.on { opacity: 1; }
g:hover > .lock-dot, [data-addr]:focus-visible .lock-dot { opacity: 1; }
/* Nothing hovers a knob on a tablet, so "appear on hover" means "never" —
   and locking is half of what the rack is for. Dimmed, not lit: the dot has
   to read as available without competing with the ones actually locked. */
@media (pointer: coarse) { .lock-dot { opacity: 0.4; } }

/* Audio cables only travel while audio does — and each one travels at the
   speed of the level actually present at that point in the chain (the
   per-cable duration is set in JS). Uniform perpetual motion is noise;
   differential motion is a meter you can read without playing a note. */
#rack-svg.flowing .wire.audio {
  stroke-dasharray: 1 15;
  animation: flow 760ms linear infinite; /* token-exempt: a loop's period, not a transition */
}
/* Crossfade a mixer all the way over and the branch you turned down does not
   slow down — it stops, and goes solid, which is what "nothing is going
   through here" looks like on a real panel. */
#rack-svg.flowing .wire.audio.still {
  animation: none;
  stroke-dasharray: none;
}
@keyframes flow { to { stroke-dashoffset: -16; } }

/* ---------- duel cards ---------- */
.duel-name.chosen { color: var(--phos-a); text-shadow: 0 0 10px var(--phos-a-50); }
.duel-name {
  font-size: var(--t-title);
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--silk);
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  min-width: 0;
}
.dn-id { font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim); }
.dn-sig { font-size: var(--t-value); color: var(--phos-a-dim); }
/* The same bookkeeping on the duel cards: on request only. */
html:not(.engineer) .dn-id,
html:not(.engineer) .dn-sig { display: none; }
.duel-controls .hw-btn.promote { flex: 0 0 auto; color: var(--phos-a-dim); font-size: var(--t-label); }
.duel-controls .hw-btn.promote:hover { color: var(--phos-a); }

/* ---------- teaching meter ----------
   The first six votes used to produce no pixel change anywhere. */
.teach { display: flex; align-items: center; gap: var(--s3); }
.teach-pips { display: flex; gap: var(--s1); }
.teach-pips i {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--amber-off);
  box-shadow: inset 0 0 2px var(--black);
  transition: background var(--d-state) ease, box-shadow var(--d-state) ease;
}
.teach-pips i.lit {
  background: var(--phos-b);
  box-shadow: 0 0 8px var(--phos-b-70);
}
.teach-copy {
  font-family: var(--font-silk);
  font-size: var(--t-body);
  line-height: 1.45;
  color: var(--silk-dim);
  text-align: left;
}
/* The sentence that teaches the whole product lands whole, not as three
   ragged lines beside 900px of empty bar. */
@media (min-width: 1200px) {
  .teach-copy { min-width: 0; white-space: nowrap; }
}
/* EVOLVE POOL and, while it breeds, its stop. */
.evolve-wrap { justify-self: end; display: inline-flex; align-items: center; gap: var(--s2); }
.evolve-wrap .evolve-stop.hidden { display: none; }
/* While it breeds the button is its own progress bar: an amber fill for the
   share of the generation absorbed, the count over it, full strength rather
   than the dimmed look of a disabled button — it is working, not unavailable.
   The wrapper hears the pointer (the "may be replaced" marks). */
#evolve-btn.breeding {
  position: relative;
  overflow: hidden;
  opacity: 1;
  min-width: 150px;
  pointer-events: none;
  color: var(--phos-b);
  background: linear-gradient(180deg, var(--plate-hi), var(--plate-lo));
}
#evolve-btn.breeding .eb-fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: var(--phos-b-22);
  box-shadow: inset -1px 0 0 var(--phos-b);
  transition: width var(--d-move) ease;
}
#evolve-btn.breeding .eb-text { position: relative; }
/* What the last walk came back as, beside its count: plain, not a label. */
#evolve-btn.breeding .eb-said {
  position: relative;
  margin-left: var(--s2);
  font-family: var(--font-mono);
  font-size: var(--t-value);
  letter-spacing: 0;
  text-transform: none;
  color: var(--silk);
}
@media (prefers-reduced-motion: reduce) { #evolve-btn.breeding .eb-fill { transition: none; } }

/* ---- a child buds from its seed (main.js `budFrom`, `fadeBeside`) ---- */
.bud {
  position: fixed;
  z-index: 90;
  pointer-events: none;
  transform-origin: left center;
  white-space: nowrap;
  font-family: var(--font-silk);
  font-size: var(--t-body);
  font-weight: 500;
  line-height: 1.2;
  color: var(--phos-a);
  background: var(--bezel);
  border-radius: 2px;
  box-shadow: 0 0 0 3px var(--bezel), 0 0 14px var(--phos-a-34);
}
/* A child the pool would not take: no name (the engine dropped it), only
   why, in plain words, and gone. */
.bud.refused {
  font-family: var(--font-mono);
  font-size: var(--t-value);
  font-weight: 400;
  color: var(--silk-dim);
  box-shadow: 0 0 0 3px var(--bezel), 0 0 0 4px var(--hairline);
}

/* ---- Compare: a bred sound beside its seed ---- */
.compare {
  position: fixed;
  z-index: 60;
  width: 340px;
  max-width: calc(100vw - 2 * var(--s3));
  padding: var(--s3);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: var(--surface-pop);
  box-shadow: var(--shadow-pop);
}
.compare:focus { outline: none; }
.compare .cmp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
}
.compare .cmp-title {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk);
}
.compare .cmp-x {
  min-width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  font-size: var(--t-body);
  color: var(--silk-dim);
}
.compare .cmp-x:hover { color: var(--silk); }
.compare .cmp-fig {
  display: block;
  width: 100%;
  height: 112px;
  margin-top: var(--s2);
  border-radius: var(--r1);
  background: var(--bezel);
}
.compare .cmp-say {
  margin: var(--s2) 0 0;
  font-size: var(--t-body);
  color: var(--silk);
}
/* Every change, one a line; past about ten lines the list scrolls, so
   Compare stays beside the row it was opened from. */
.compare .cmp-diff {
  display: grid;
  gap: 2px;
  max-height: 13.5em;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: var(--s2) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--t-value);
  color: var(--silk-dim);
}
/* The model's own words: what it rated each when it bred them. */
.compare .cmp-rated {
  margin: var(--s2) 0 0;
  font-family: var(--font-voice);
  font-style: italic;
  font-size: var(--t-voice);
  line-height: 1.35;
  color: var(--phos-b);
}
.compare .cmp-play {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s3);
}
/* Lit while its phrase plays, as ▶ SAMPLE is (`.hw-btn.play.playing`). */
.compare .cmp-hear { flex: none; color: var(--phos-a); }
.compare .cmp-gone { font-size: var(--t-value); color: var(--silk-dim); }
.duel-mid.learning .teach-copy,
.duel-mid.learned .teach-copy { color: var(--phos-b); }
/* "● learning from your last 6 picks…": the full row breathes while the refit
   is on its way or running, and stops when it lands. */
.duel-mid.learning .teach-pips i.lit,
.play-duel.learning .teach-pips i.lit { animation: pip-learning 1.4s ease-in-out infinite; } /* token-exempt: a loop's period, not a transition */
@keyframes pip-learning {
  50% { background: var(--phos-b-deep); box-shadow: none; }
}
@media (prefers-reduced-motion: reduce) {
  .duel-mid.learning .teach-pips i.lit,
  .play-duel.learning .teach-pips i.lit { animation: none; }
}
.duel-pred { text-align: center; }
.duel-centre { display: flex; flex-direction: column; align-items: center; gap: var(--s1); }

/* ---------- toasts & the alarm strip ---------- */
/* One lane, placed by `positionToastLane` in the free spot nearest the
   stage's bottom right, clear of every control where the level leaves one
   (toasts.js `placeLane`; web-runtime.md § Toasts for where it does not).
   It used to stack up from the bottom centre — straight over PICK A / PICK B,
   so the recovery affordance obscured the app's core teaching action. The
   z-index is deliberately BELOW anything holding a pressed state or a teaching
   strip (see the rule below), so this class of collision cannot come back by
   accident: even a mispositioned lane would pass under them. */
.toasts {
  position: fixed;
  top: 64px;
  right: 16px;
  z-index: 30;
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  align-items: flex-end;
  pointer-events: none;
}
/* The primary teaching affordances, and every control that holds a state,
   sit above transient chrome — always — and below anything that opens over
   them. The ladder: toasts 30 < this lift 35 < popovers and tours (40–45:
   the bank tour, the arp drawer, the map tip, the booth banner) < dialogs
   (55+, ⌘K's list among them). At 50 this lift put PERFORM's FREEZE pad over
   the old ⋯ menu's last item (a click on it froze) and bank rows' save
   buttons over the bank tour. */
.play-duel, .duel-mid, [aria-pressed] { position: relative; z-index: 35; }
/* …which lifts a warm-start card (it holds its pick in aria-pressed) over its
   own ▶, so the button painted under the card and a click on it cast a pick
   instead of playing. The ▶ sits one above. */
.warm-cell .wi-play { z-index: 36; }
.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--s3);
  max-width: min(360px, 62vw);
  font-size: var(--t-body);
  color: var(--silk);
  background: linear-gradient(180deg, var(--toast-hi), var(--panel));
  border: 1px solid var(--hairline);
  border-left: 2px solid var(--phos-a-deep);
  border-radius: var(--r1);
  padding: var(--s2) var(--s3);
  box-shadow: var(--cast);
  animation: toast-in var(--d-state) cubic-bezier(0.2, 0.9, 0.3, 1.05);
}
/* On TASTE a toast stands over the map, whose sounds reach to its corners,
   and the lane has nowhere clear of it to go: there the toast lets the
   pointer through to the sound under it, and only its buttons and links
   take a click (#412). */
body[data-level="taste"] .toasts .toast { pointer-events: none; }
body[data-level="taste"] .toasts .toast :is(button, a) { pointer-events: auto; }
/* …and the +N counter, whose hover says how many more are waiting. */
body[data-level="taste"] .toasts .toast .toast-stack { pointer-events: auto; }
.toast.out { opacity: 0; transform: translateY(-6px); transition: opacity var(--d-move) ease, transform var(--d-move) ease; }
@keyframes toast-in { from { opacity: 0; transform: translateY(-8px); } }
.toast-msg { flex: 1; min-width: 0; }
/* What the lane is holding back. A queue with no counter is a queue that
   looks like dropped messages. */
.toast-stack {
  flex: 0 0 auto;
  font-size: var(--t-value);
  color: var(--silk-dim);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  padding: 1px var(--s1);
}
.toast-stack.hidden { display: none; }
.toast-undo {
  font-size: var(--t-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--phos-b);
  background: none;
  border: 1px solid var(--phos-b-deep);
  border-radius: 3px;
  padding: 3px var(--s2);
  cursor: pointer;
}
.toast-undo:hover { border-color: var(--phos-b); box-shadow: 0 0 10px var(--phos-b-25); }

/* A condition, not a remark: this stays until it is resolved. */
.alarm {
  position: fixed;
  left: 50%;
  top: calc(var(--menubar-h) + var(--s2));
  transform: translateX(-50%);
  z-index: 58;
  display: flex;
  align-items: center;
  gap: var(--s3);
  max-width: 72vw;
  font-size: var(--t-body);
  color: var(--alarm-ink);
  background: linear-gradient(180deg, var(--alarm-hi), var(--phos-b));
  border-radius: var(--r1);
  padding: var(--s2) var(--s4);
  box-shadow: 0 8px 26px var(--black-55);
}
.alarm .al-icon { font-size: var(--t-body); }
.alarm .toast-undo { color: var(--alarm-ink); border-color: var(--black-35); }

/* ---------- first-run elicitation ---------- */
.warm {
  position: fixed; inset: 0; z-index: 59;
  background: var(--scrim-90);
  backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center;
}
.warm-card { width: min(760px, 92vw); padding: var(--s6) var(--s5) var(--s5); text-align: center; }
.warm-sub {
  font-family: var(--font-silk);
  font-size: var(--t-body);
  line-height: 1.5;
  color: var(--silk-dim);
  max-width: 52ch;
  margin: var(--s3) auto var(--s5);
}
/* `minmax(0, 1fr)`: a column's floor is not its longest word, or a narrow
   window's cards push the third column off the card. */
.warm-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s2); }
.warm-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  text-align: left;
  padding: var(--s3);
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  background: linear-gradient(180deg, var(--card-hi), var(--card-lo));
  cursor: pointer;
  transition: border-color var(--d-state) ease, box-shadow var(--d-state) ease;
}
.warm-item:hover { border-color: var(--silk-dim); }
.warm-item.picked {
  border-color: var(--phos-b);
  box-shadow: 0 0 16px var(--phos-b-22), inset 0 0 10px var(--phos-b-08);
}
.wi-name { font-size: var(--t-body); font-weight: 500; color: var(--silk); min-width: 0; max-width: 100%; overflow-wrap: break-word; }
.wi-sig { font-size: var(--t-label); color: var(--phos-a-dim); min-width: 0; max-width: 100%; overflow-wrap: break-word; }
/* The ▶ overlays the pick card's bottom-right rather than sitting inside it:
   one cell, two targets, no nested buttons. Padding on `.warm-item` keeps the
   name clear of it. */
.warm-cell { position: relative; display: flex; }
.warm-cell .warm-item { flex: 1; min-width: 0; padding-right: 40px; } /* token-exempt: the ▶'s column, which overlays the card: 28px wide, 6px in from the edge, 6px clear of the words */
.wi-play {
  position: absolute;
  right: 6px;
  bottom: 6px;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  font-size: var(--t-body);
  line-height: 1;
  color: var(--phos-a-dim);
  background: var(--black-35);
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  cursor: pointer;
}
.wi-play:hover { color: var(--phos-a); border-color: var(--phos-a-dim); }
.wi-play.playing { color: var(--phos-a); box-shadow: inset 0 0 10px var(--phos-a-30); }
/* Waiting on the engine, which on a cold boot is genuinely busy. It breathes,
   so a ▶ that has been heard reads as waiting rather than as a dead click — a
   first render can take a few seconds on a busy machine. */
.wi-play.loading {
  color: var(--phos-b);
  border-color: var(--phos-b-dim);
  animation: listen 1.1s ease-in-out infinite; /* token-exempt: a loop's period, not a transition */
}
@media (prefers-reduced-motion: reduce) { .wi-play.loading { animation: none; } }
/* On a touch screen the ▶ is a finger's size, 44 px (the touch rule further
   down holds every ▶ to it), and the card keeps its words clear of all of
   it. The room kept was a 34 px ▶'s, so a 44 px one sat over the end of a
   blurb (#527). */
@media (pointer: coarse) {
  .wi-play { width: 44px; height: 44px; }
  .warm-cell .warm-item { padding-right: 56px; } /* token-exempt: the ▶'s column on touch: 44px wide, 6px in from the edge, 6px clear of the words */
}
/* A phone: two columns, and the ▶ takes a row
   under the words rather than a column beside them, which left a 98 px card
   40 px of text. The card scrolls if the nine run past the screen. */
@media (max-width: 560px) {
  .warm { align-items: flex-start; overflow-y: auto; }
  .warm-card { margin: auto; padding: var(--s5) var(--s4) var(--s4); }
  .warm-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .warm-cell .warm-item { padding-right: var(--s3); }
  .warm-cell .warm-item { padding-bottom: 40px; } /* token-exempt: the ▶'s row on a phone: 28px tall, 6px up from the edge, 6px clear of the words */
}
@media (max-width: 560px) and (pointer: coarse) {
  .warm-cell .warm-item { padding-bottom: 56px; } /* token-exempt: the ▶'s row on a phone: 44px tall, 6px up from the edge, 6px clear of the words */
}
.warm-foot { display: flex; justify-content: center; gap: var(--s3); margin-top: var(--s5); align-items: center; }
.warm-tour { margin-right: auto; color: var(--phos-a); font-size: var(--t-body); text-decoration: none; }
.warm-tour:hover, .warm-tour:focus-visible { text-decoration: underline; }
/* The warm start scrolls whenever its card is taller than the window, a
   phone upright or on its side, and stays centred while it fits: an auto
   margin takes only the room there is. Centred by the flexbox, a card taller
   than a phone on its side had its title above the window and SKIP below it,
   and neither could be scrolled to. */
#warmstart { align-items: flex-start; overflow-y: auto; }
#warmstart > .warm-card { margin: auto; }
/* Where the card can run past the window (a phone, upright or on its side,
   or a short window), its foot stays at the window's foot while the nine
   scroll above it (#527): SKIP and TEACH IT sat under the fold, with nothing
   to say they were there. The foot takes the card's bottom padding as its
   own, so it covers the cards to the window's edge, opaque as the panel is
   there, and a fade at its top says they go on under it. It stands over
   every card (35: each holds its pick in `[aria-pressed]`, picked or not)
   and its ▶ (36).
   The dialog keeps that room clear for the focus: a card or ▶ that Tab
   moves to below the fold scrolls up clear of the foot and its fade, ring
   and all (the browser stopped it at the window's foot, under the foot),
   and one that Shift+Tab moves to above the top comes down far enough to
   show its ring. The room is the tallest foot's, a finger's 44 px button on
   a short screen (s3 above it, s5 below; a phone's is s4 below), with the
   fade's s3 and the ring's 4 px (2 out, 2 wide). The gap above the foot is
   the fade's and the ring's, s4, so the last row's ring clears the fade
   once the cards have scrolled to their end. */
@media (max-width: 560px), (max-height: 600px) {
  #warmstart { scroll-padding: var(--s1) 0 calc(var(--s3) + 44px + var(--s5) + var(--s3) + var(--s1)); } /* token-exempt: the foot's height on a short touch screen (its padding and a finger's 44 px button), its fade and the focus ring's 4 px */
  #warmstart > .warm-card { padding-bottom: 0; }
  #warmstart .warm-foot {
    position: sticky;
    bottom: 0;
    z-index: 37;
    margin-top: var(--s4);
    padding: var(--s3) 0 var(--s5);
    background: var(--panel);
  }
  #warmstart .warm-foot::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: var(--s3);
    background: linear-gradient(to top, var(--panel), transparent);
    pointer-events: none;
  }
}
@media (max-width: 560px) {
  #warmstart .warm-foot { padding-bottom: var(--s4); }
}
/* Three picked: TEACH IT is the next step, and it lights as a pad does
   (`.pf-pad.lit`), an amber edge and glow, where until the third pick it is
   dim (main.js `renderWarmStart`). Pressed, it is `.hw-btn`'s press. */
#warm-go.lit:not(:active) {
  border-color: var(--phos-b);
  box-shadow: var(--lift), 0 0 12px var(--phos-b-30), 0 3px 0 var(--shadow-ink);
}

/* ---------- the commit duel ---------- */
/* Deliberately the warm-start's card, not a new one: it is the same act — the
   instrument asking a question it will learn from — and a second dialect for
   it would teach the player that these are different kinds of question. Two
   cells, a letter each, and the two things you can do to each of them side by
   side: hear it, choose it. */
.cduel-card { width: min(680px, 92vw); }
.cduel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); }
.cduel-cell {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s3);
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  background: linear-gradient(180deg, var(--card-hi), var(--card-lo));
  text-align: left;
}
.cd-head { display: flex; align-items: baseline; gap: var(--s2); }
.cd-letter {
  font-family: var(--font-mono);
  font-size: var(--t-value);
  letter-spacing: 0.18em;
  color: var(--phos-b);
}
.cd-name { font-size: var(--t-body); color: var(--silk); }
/* The waveform is the only thing on the card that is *of* the sound, so it
   gets the plate's own recess rather than sitting on the card's face. */
.cd-scope {
  width: 100%;
  height: 54px;
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  background: var(--black-30);
}
.cd-row { display: flex; gap: var(--s2); align-items: center; }
.cd-play { flex: 1; }
.cd-pick { flex: 1; }
.cd-hint { font-size: var(--t-value); }
@media (max-width: 700px) {
  .cduel-grid { grid-template-columns: 1fr; }
}

/* ---------- boot ---------- */
.boot { transition: opacity var(--d-move) cubic-bezier(0.4, 0, 0.2, 1); }
.boot.done { opacity: 0; pointer-events: none; }
.boot-word {
  font-size: var(--t-title);
  font-weight: 500;
  letter-spacing: 0.145em;
  /* Tracked type centres half a letter-space left of true: letter-spacing
     follows the last letter too. One letter-space of start margin puts the
     word back on the card's axis. */
  margin-inline-start: 0.145em;
  color: var(--silk);
}
.boot-word b { font-weight: 500; color: var(--phos-b); text-shadow: 0 0 14px var(--phos-b-70); }
/* The wait draws the search happening, on the same field the taste map uses. */
.boot-field {
  display: block;
  width: 100%;
  height: 190px;
  margin: var(--s4) 0 var(--s2);
  border-radius: var(--r1);
  background: radial-gradient(ellipse at 50% 40%, var(--crt-amber) 0%, var(--bezel) 85%);
  box-shadow: inset 0 0 18px var(--black-90), inset 0 0 2px 1px var(--black);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* ---------- bank rows ---------- */
/* Everything that dresses a bank row lives in the `.bank` block near the top
   of this file. It used to be split across places 1600 lines apart, and the
   halves disagreed: a `.bi-u { width: 54px }` down here silently overrode the
   full-width meter declared up there. Splitting one component's rules across
   the file is how contradictions like those survive review. */

@media (prefers-reduced-motion: reduce) {
  .toast, .duel-mid, .mod-plate, .lock-dot, .live-scope, .boot { transition: none !important; }
  #rack-svg.flowing .wire.audio { animation: none !important; stroke-dasharray: none; }
}

/* The bank's roving keyboard cursor (the list itself is the tab stop) is
   drawn in the bank's block: `.bank-item.kbd`, `.kept-row.kbd`. */

/* ---------- local explanation ----------
   Utility is linear within a style lens, so these contributions are exact.
   "The model likes this" is a claim; "it likes this because it's bright and
   has a long tail, under your Dark Drones lens" is one you can check by ear. */
/* ---------- worth per kind, under the model view (`paintWorth`) ----------
   One chip per family the patch holds, in the well's top right, under its
   top line: what one more of the family is worth to the model. Solid amber
   for a settled lean, dashed for a guess. */
.pt-worth {
  position: absolute;
  z-index: 5;
  right: var(--s4);
  top: calc(var(--s3) + 32px);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--s1);
  pointer-events: none;
}
.pt-worth[hidden] { display: none; }
.pt-worth-chip {
  pointer-events: auto;
  padding: var(--s1) var(--s2);
  border: 1px solid var(--phos-b-40);
  border-radius: var(--r1);
  background: var(--panel-94);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  letter-spacing: 0.04em;
  color: var(--silk-dim);
  white-space: nowrap;
}
.pt-worth-chip.guess { border-style: dashed; }
.pt-worth-chip .pw-fig { font-size: var(--t-value); font-weight: 400; color: var(--phos-b); }
.pt-worth-chip.down .pw-fig { color: var(--silk-dim); }
.pt-worth-chip.guess .pw-fig { color: var(--phos-b-dim); }
.pt-worth-chip .pw-note { color: var(--silk-mute); }
/* No family settled, so no edge drawn: the model says so, in its voice. */
.pt-worth-none {
  pointer-events: auto;
  font-family: var(--font-voice);
  font-style: italic;
  font-size: var(--t-voice);
  color: var(--silk-dim);
  white-space: nowrap;
}

/* ---------- the belief line: PATCH's subtitle under the model view ---------- */
/* What the model makes of the patch in hand, in its voice (`renderBelief`):
   "it'd like this 62% · leaning · was 58% ▲". At rest the subtitle says what
   the patch is made of and this is hidden; under the model view (hold ⌥, or
   MODEL) the subtitle becomes it. One line, always: the head keeps its
   height, so the well under it never moves. */
.pt-sub .belief { display: none; }
/* It stands in for the counts (4 modules · 1 modulator, in signal order)
   and nothing else: a bred sound's seed, opening…, 1 edit waiting, 2 locked
   and silent stay in sight after it. */
body.model-view .pt-sub .belief { display: flex; order: -1; flex: none; max-width: 70%; }
body.model-view #view-patch #rack-meta .pt-made { display: none; }
body.model-view #view-patch #rack-meta .pm:first-child:not(.pt-made)::before { content: "· "; }
#rack-meta .pm-sep { font-style: normal; }
.belief {
  align-items: baseline;
  gap: var(--s2);
  flex-wrap: nowrap;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-family: var(--font-voice);
  font-style: italic;
  font-size: var(--t-voice);
  line-height: 1.45;
  color: var(--silk-dim);
  transition: opacity var(--d-state) ease;
}
.belief > * { flex: 0 0 auto; }
/* The number itself is the loudest thing on the line: it is what changes. */
.belief .bl-u { color: var(--silk); font-weight: 500; }
.belief .bl-voice, .belief .bl-was { color: var(--silk-dim); }
/* The word beside the percentage (a hunch · leaning · fairly sure): the
   model saying how sure it is, so amber. */
.belief .bl-sure { color: var(--phos-b); }
.belief .bl-none { color: var(--silk-dim); }
.belief .bl-arrow { font-style: normal; }
.belief .bl-arrow.up { color: var(--phos-a); }
.belief .bl-arrow.down { color: var(--silk-mute); }
/* Stale is drawn, never guessed: an edit is in flight and this number is about
   the tree before it. Dimmed rather than hidden, because a number that
   disappears and comes back is a flicker under every knob turn. */
.belief.stale { opacity: 0.45; }
.belief .bl-stale { color: var(--phos-b-dim); }
/* The parts that add up to it, the style judging it and the utility: the
   readout's under the model view with nothing selected (`beliefParts`). */
.pr-belief b.up { color: var(--phos-b); font-weight: 400; }
.pr-belief b.down { color: var(--silk-dim); font-weight: 400; }
.pr-belief .pr-parts, .pr-belief .pr-u { font-size: var(--t-value); }
.pr-belief .pr-u { color: var(--silk-mute); }
.pr-belief .ex-lens { font-family: var(--font-voice); font-style: italic; color: var(--silk-dim); }
.pr-belief .ex-lens b { color: var(--phos-b); font-weight: 400; }

/* The ceilings evolution can search. Quiet until it is nearly spent: one
   short, its number amber; at the ceiling, the whole count amber (a patch as
   big as breeding searches, which is news, not danger); red only while an
   edit refused at that ceiling is said (#530, main.js `budgetRefusal`). */
.budget {
  display: flex;
  align-items: baseline;
  gap: var(--s1);
  font-size: var(--t-label);
  color: var(--silk-dim);
  white-space: nowrap;
}
.budget .bg-sep { opacity: 0.45; }
.budget .bg-cell b { color: var(--silk-dim); font-weight: 400; }
.budget .bg-cell.tight b { color: var(--phos-b); }
.budget .bg-cell.full { color: var(--phos-b); }
.budget .bg-cell.full b { color: var(--phos-b); }
.budget .bg-cell.refused,
.budget .bg-cell.refused b { color: var(--led-red); }

/* How the pair on the table was chosen (`renderDealRule`): quiet silk under
   the default, where it is the same fact about every pair; amber only for a
   check dealt at random under a rule that otherwise chooses. */
.duel-deal { display: flex; align-items: center; gap: var(--s2); }
.duel-rule { font-size: var(--t-value); color: var(--silk-dim); white-space: nowrap; cursor: help; }
.duel-rule:empty { display: none; }
.duel-rule.check { color: var(--phos-b); }
/* The centre column is as wide as its widest line, and the meter's copy
   takes what is left: on a tablet (768px) a one-line rule beside skip left
   the copy 42px wide, a word to a line. Narrower than any desktop window the
   app opens in, the rule wraps above skip (at 1000px it fits beside it, and
   stacked it cost the cards 22px of height). */
@media (max-width: 900px) {
  .duel-deal { flex-direction: column; gap: 3px; }
  .duel-rule { white-space: normal; max-width: 16ch; text-align: center; line-height: 1.3; }
}

/* ---------- round-2 corrections ---------- */

/* Native range inputs render as OS widgets 40px from hand-drawn knobs.
   Drawn as faders instead, lit from the same 315° as everything else. */
input[type="range"] {
  appearance: none;
  -webkit-appearance: none;
  height: 4px;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--phos-a-deep), var(--hairline));
  cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 12px;
  height: 20px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--btn-hi), var(--pop-lo));
  border: 1px solid var(--black);
  box-shadow: var(--lift), 0 2px 4px var(--black-60);
}
input[type="range"]::-moz-range-thumb {
  width: 12px;
  height: 20px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--btn-hi), var(--pop-lo));
  border: 1px solid var(--black);
  box-shadow: var(--lift), 0 2px 4px var(--black-60);
}
.perf-sel, .perf-num { appearance: none; -webkit-appearance: none; }
.perf-sel {
  padding-right: var(--s4);
  background-image: linear-gradient(45deg, transparent 50%, var(--silk-dim) 50%),
                    linear-gradient(135deg, var(--silk-dim) 50%, transparent 50%),
                    linear-gradient(180deg, var(--plate-hi), var(--plate-lo));
  background-position: right 7px center, right 3px center, 0 0;
  background-size: 4px 4px, 4px 4px, 100% 100%;
  background-repeat: no-repeat;
}


/* Tick rings turn to moiré at zoom 1 — they are a texture for the zoomed-in
   case, not information. */
.knob-ticks line { stroke: var(--knob-tick); stroke-width: 1; opacity: 0.55; }

/* The value arc carried a full-strength glow on every knob, which made
   parameter values brighter than the cables carrying the signal. It was
   halved here and is now off entirely at rest (see `.knob-arc` above): a glow
   that is always on is not a signal, it is a texture, and it cost a blur
   region per knob on every paint. */
.knob-arc { filter: none; }

/* A duel that is settling must not look live. */
.duel-card { position: relative; }
.duel-card.dealing > :not(.deal-why) { opacity: 0.55; }
.duel-card.dealing .duel-controls { pointer-events: none; }
.play-duel.dealing .pd-side { opacity: 0.55; }
/* A side that left the pool while its pair stood (#312): its face dims, and
   its card's line says so until the next pick or ANOTHER PAIR. */
.duel-card.gone .duel-well { opacity: 0.55; }
/* …and one that is slow to settle says why, over the dimmed scope, at full
   strength: the reason is the one thing on the card that is current. */
.deal-why {
  display: none;
  position: absolute;
  left: var(--s3); right: var(--s3); top: 45%;
  text-align: center;
  font-size: var(--t-body);
  color: var(--silk);
  pointer-events: none;
}
.duel-card.dealing-slow .deal-why { display: block; }

/* The play-view teaching strip carries the same state as EVOLVE's — a
   stripped-down copy of the core interaction teaches the user there is
   nothing in it. */
.play-duel .teach-pips i { width: 7px; height: 7px; }
.pd-pred { margin-left: auto; font-size: var(--t-label); color: var(--silk-dim); }
.pd-pred:empty { display: none; }
.pd-pred.hit { color: var(--phos-a); }
.pd-pred.miss { color: var(--phos-b); }
/* Which candidate is sounding must be legible on the strip itself. */
.pd-side.live-sel {
  border-color: var(--phos-a);
  color: var(--phos-a);
  box-shadow: 0 0 8px var(--phos-a-25);
}
#pd-back { display: none; }
.play-duel.auditioning #pd-back { display: inline-block; }

/* =====================================================================
   Panel pass — stage identity, first-run, empty states, keybar drawers.
   ===================================================================== */

/* ---------- stage header: the patch is the headline ----------
   The patch being played/edited was set smaller than "PICKS 0". */
/* Transport sits against the name of the thing it plays (§10). */
.subject-block { display: flex; flex-direction: row; align-items: center; gap: var(--s2); min-width: 0; }
.subject-id { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.xport {
  width: 30px; height: 30px; flex: none;
  border-radius: 50%;
  border: 1px solid var(--phos-a-deep);
  background: radial-gradient(circle at 35% 30%, var(--xport-hi), var(--xport-lo) 78%);
  color: var(--phos-a);
  font-size: var(--t-value);
  line-height: 1;
  padding: 0 0 0 2px;
  cursor: pointer;
  box-shadow: var(--lift), 0 2px 0 var(--shadow-ink);
  transition: border-color var(--d-state) ease, box-shadow var(--d-state) ease, color var(--d-state) ease;
}
.xport:hover:not(:disabled) { border-color: var(--phos-a); box-shadow: var(--lift), 0 0 12px var(--phos-a-24); }
.xport:disabled { color: var(--silk-mute); border-color: var(--hairline); cursor: default; box-shadow: none; }
.xport.playing { color: var(--xport-lo); background: var(--phos-a); border-color: var(--phos-a); }
.xport:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
/* Pressed while an edit is still on its way to the engine: it plays once the
   edit lands, and says it heard you at once with the dotted amber ring a bank
   ▶ waiting for its render wears. An outline, which hover does not touch. */
.xport.pending { outline: 1px dotted var(--phos-b); outline-offset: 2px; color: var(--phos-b); }
/* Playing and waiting at once: the glyph keeps the playing ink, or it is amber
   on the green face. */
.xport.playing.pending { color: var(--xport-lo); }
/* Focused from the keyboard while it waits: the focus ring stays where it is,
   and the dotted amber cue moves onto the button's own edge. */
.xport.pending:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; border: 1px dotted var(--phos-b); }
.subject-name {
  font-size: var(--t-title);
  font-weight: 500;
  letter-spacing: 0.01em;
  text-transform: none;
  color: var(--silk);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 300px;
}
.subject-name.hearing { color: var(--phos-b); }
/* The caption line holds its height when it has nothing to say (it often
   does not, now that ids and signatures wait behind Show measurements), so
   the name never shifts on its row between one patch and the next. */
.subject-meta {
  font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim);
  line-height: 1.5; min-height: 1.5em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* No rack dimming while auditioning: the candidate is FULLY benched, so the
   rack legitimately shows the sound you are hearing. The amber header line
   carries the "this is a candidate" state. */

/* ---------- keybar drawers ---------- */
/* The arp row belongs to the ARP button: recessed and inert until it runs. */
.perf-ctl.idle { opacity: 0.45; }
.perf-ctl.idle > * { pointer-events: none; }
.perf-ctl .pc { display: flex; flex-direction: column; align-items: center; gap: 1px; }
.pc-cap {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--silk-dim);
}
.pc-val { font-family: var(--font-mono); font-size: var(--t-value); color: var(--phos-a-dim); min-width: 34px; text-align: center; }

/* One attention beat, used by the next-step chip to point at a surface. */
.pulse-once { animation: pulse-once 1.2s ease-in-out; } /* token-exempt: one attention beat, slower than any move */
@keyframes pulse-once {
  0%, 100% { box-shadow: none; }
  35% { box-shadow: 0 0 0 2px var(--phos-b), 0 0 22px var(--phos-b-40); }
}
.teach-link { cursor: pointer; }
.teach-link:hover { text-decoration: underline; }

/* ===================== the keybed, resized =====================
   The keybed is the one surface whose size is a *performance* decision, so it
   is the one surface the player gets to resize — in both directions, from two
   controls that do exactly one thing each. `⇕ tall` is height: 128px is a
   control strip, and the tall dock is a keyboard. `keys` is width: fewer
   octaves, wider keys. Neither touches the other, and both persist.

   Height comes from the same `--keybar-h` token the dock already sizes from,
   so the deck above simply gets less and re-zooms; nothing is positioned
   against a hard-coded footer height. Capped at 44vh so the rack never loses
   more than it can spare, and floored at 210px so the mode is worth entering
   on a short screen. */
.app.bigkeys { --keybar-h: clamp(210px, 44vh, 340px); }

/* Fewer, wider keys deserve bigger markings, and the strike-position velocity
   law (top of the key is soft, bottom is hard) is finally a real gesture at
   this height — so the hint sits clear of where the finger lands. */
.app.bigkeys .pkey { border-right-width: 2px; }
.app.bigkeys .pkey .hint { bottom: 10px; font-size: var(--t-body); }
.app.bigkeys .bkey .hint { font-size: var(--t-label); }
.app.bigkeys .bkey { width: 58%; }

/* No lit-state rule here on purpose: `.util-btn.lit` already says "engaged"
   for hold / uni / arp, and a fourth toggle in the same row that lit up a
   different colour would read as a different kind of thing. */

/* The width selector sits in the control grid, spanning the cells the fifth
   column leaves over, with its caption inline so the row stays two deep. */
.keys-span { display: flex; align-items: center; gap: var(--s1); }
.keys-span .pc-cap { font-size: var(--t-label); color: var(--silk-dim); text-transform: uppercase; letter-spacing: 0.1em; }
.keys-span .perf-sel { flex: 1; min-width: 0; }

/* ===================== faces ===================== */
/* A sound's face (Plan-005 task 3): the render's spectrum against the bank's,
   drawn by faces.js (in the sound's green, `--phos-a`) as an image in a slot
   its place always has. */
.face-slot { display: inline-block; flex: none; vertical-align: middle; }
.face-slot .face { display: block; pointer-events: none; }
/* What sounds now, over the face of a sound being played (live-face.js):
   a layer laid over the face's slot (or the map's well), the size of it,
   which never moves anything and takes no pointer. */
.face-live { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
/* PATCH's teach strip: the face, then the name. */
.pd-side .face-slot { margin-right: var(--s1); }
/* An EVOLVE card's face, before its name; the name keeps the head's free
   space, so the badge and the flip stay at the right. */
.panel-head > .face-slot + .duel-name { margin-right: auto; }
/* At 1000 to 1080 px the bank is wider by the faces' column (228 px, not
   200); PERFORM's side padding gives the 28 px back, so its eight captions
   keep the width they had (text_fits.spec.js). */
@media (max-width: 1080px) {
  .stage > .view.perform { padding-left: var(--s2); padding-right: var(--s2); } /* (0,3,0): over PERFORM's own padding, set further down */
}
/* A warm-start card: the face left of its two lines, in a column the card
   always has. */
.warm-cell .warm-item { padding-left: calc(var(--s3) + var(--s5) + var(--s2)); }
.warm-item > .face-slot {
  position: absolute;
  left: var(--s3);
  top: 50%;
  transform: translateY(-50%);
}

/* ===================== PERFORM ===================== */
/* To the specimen (Plan-008 C1, `a-perform-doors.png`): the sound on the
   left (its family, its name, its blurb, and the well holding its face),
   what you turn on the right (CONTROLS, the knobs, the hood strip and the
   pad row). Built by perform.js; nothing here opens a modal. */
.view.perform {
  overflow-y: auto;
  padding: var(--s5) var(--s5) var(--s4);
}
.pf-grid {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s6);
}
.pf-left {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: var(--s4);
  min-width: 0;
  min-height: 0;
}
.pf-right {
  display: flex;
  flex-direction: column;
  justify-content: safe center;
  gap: var(--s4);
  min-width: 0;
  min-height: 0;
}
/* The head: the family and "in hand" (with the moved bar at its end), the
   name at the display size with share, and the blurb. Each line keeps its
   height, so the well under it never moves from one sound to the next. */
.pf-head { display: grid; gap: var(--s2); min-width: 0; }
.pf-cap-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1) var(--s3); min-height: 30px; }
.pf-cap {
  white-space: nowrap;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk-dim);
}
.pf-title { display: flex; align-items: center; gap: var(--s3); min-width: 0; }
.pf-name {
  min-width: 0;
  font-family: var(--font-silk);
  /* The level's display size, as the specimen sets it, down to the title's
     where the column is narrow (a 1000 px window): one line, the whole name
     in its title when a long one is cut. */
  font-size: clamp(var(--t-title), 3.5vw, var(--t-display));
  font-weight: 400;
  line-height: 1.08;
  letter-spacing: -0.01em;
  color: var(--silk);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pf-share {
  flex: none;
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: 50%;
  display: grid;
  place-items: center;
}
.pf-share svg,
.pf-stage-btn svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.pf-blurb {
  min-height: 1.45em;
  font-family: var(--font-silk);
  font-size: var(--t-body);
  line-height: 1.45;
  color: var(--silk-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Moved: the sound has left home (perform.js `movedFromHome`), and KEEP or
   BACK settle it. In a slot the cap row always keeps: shown or hidden in
   place, nothing moves. */
.pf-moved { margin-left: auto; display: flex; align-items: center; gap: var(--s2); visibility: hidden; }
.pf-moved.on { visibility: visible; }
.pf-moved-w { font-size: var(--t-value); color: var(--silk-dim); }
.pf-mv {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--silk);
  background: var(--panel-hi);
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  padding: var(--s1) var(--s2);
  cursor: pointer;
}
.pf-mv:hover { border-color: var(--silk-dim); }
.pf-mv:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
.pf-mv.flash { animation: pf-flash var(--d-move) ease-out; }
/* A printed key, drawn from `data-key` so a pad's words stay its name. */
.pf-mv[data-key]::after,
.pf-pad[data-key]::before,
.pf-kbd {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.3;
  padding: 0 var(--s1);
  border: 1px solid var(--hairline);
  border-radius: 3px;
  color: var(--silk-dim);
}
.pf-mv[data-key]::after { content: attr(data-key); }
@media (pointer: coarse) {
  .pf-mv[data-key]::after, .pf-pad[data-key]::before, .pf-kbd { display: none; }
  /* Under a finger, KEEP and BACK are as tall as the pads on a short screen. */
  .pf-mv { min-height: 44px; padding-inline: var(--s4); }
}

/* The well: the sound's face, large, with its glow and the floor's
   reflection (main.js FACE_OPTS, as stage mode draws it), and B beside it
   while it holds an offer, in the model's amber. Its modes (perform.js
   `setWellMode`): the face, the XY pad, How it works. */
.pf-well {
  position: relative;
  min-height: 280px;
  overflow: hidden;
  border: 1px solid var(--hairline);
  border-radius: var(--r3);
  background: radial-gradient(120% 90% at 50% 60%, var(--panel), var(--bezel) 62%);
  box-shadow: inset 0 0 0 1px var(--black-60), inset 0 30px 60px -30px var(--black-75);
}
.pf-faces {
  position: absolute;
  inset: var(--s5) var(--s4) var(--s4);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s4);
  cursor: pointer;
}
/* B holds an offer (or one is growing): the two faces side by side, their
   floors on one line (both drawn on the same floor, at one scale). */
.pf-well.offered .pf-faces { grid-template-columns: minmax(0, 1fr) minmax(0, 0.84fr); bottom: calc(var(--s7) + var(--s2)); }
.pf-faces > .pf-face,
.pf-offer > .pf-face { position: relative; width: 100%; height: 100%; min-width: 0; }
.face-slot.face-well .face,
.face-slot.face-wellb .face,
.face-slot.face-evolve .face { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.pf-well[data-mode="xy"] .pf-faces,
.pf-well[data-mode="how"] .pf-faces { opacity: 0.22; pointer-events: none; }
/* B: its label over its face, what changed and how to hear it under the
   floor. Placed on the face's own floor line (FACE_OPTS: B's vessel stands
   on the floor at 79% of its picture, 46% tall). */
.pf-offer { position: relative; min-width: 0; transform-origin: 0 0; }
.pf-offer.empty { display: none; }
.pf-offer.moving { pointer-events: none; }
.pf-offer-label,
.pf-ghost-label {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(33% - var(--s5));
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-align: center;
  color: var(--phos-b);
}
.pf-offer-body,
.pf-ghost-body {
  position: absolute;
  left: 0;
  right: 0;
  top: 82%;
  display: grid;
  gap: 2px;
  font-family: var(--font-mono);
  font-size: var(--t-value);
  line-height: 1.35;
  text-align: center;
  color: var(--phos-b-dim);
}
/* What changed, cut at two lines (the whole is B's title); where it grew
   and how to hear it, whole. */
.pf-offer-line,
.pf-ghost-line {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.pf-offer-how, .pf-ghost-how { color: var(--silk-dim); }
@media (max-width: 1180px) {
  .pf-offer-body, .pf-ghost-body { top: 80%; }
  .pf-offer-line, .pf-ghost-line { -webkit-line-clamp: 1; }
}
/* Growing, or a word with no offer: said where B's face will be. */
.pf-offer:not(.ready) .pf-offer-body { top: 50%; transform: translateY(-50%); color: var(--phos-b); }
.pf-offer[data-moment="taken"] .pf-offer-label { color: var(--phos-a); }
@media (prefers-reduced-motion: reduce) {
  .pf-offer[data-moment="taken"] { background: var(--phos-a-24); }
}
/* Blend: under the two faces, from the sound in hand to B, while B holds an
   offer. */
.pf-blend-slot { position: absolute; left: 50%; bottom: var(--s3); transform: translateX(-50%); width: min(360px, 80%); display: none; z-index: 2; }
.pf-blend-slot.on { display: block; }
.pf-well:not([data-mode="face"]) .pf-blend-slot { display: none; }
.pf-blend {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "l in r" "v v v";
  align-items: center;
  gap: 2px var(--s2);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--silk-dim);
}
.pf-blend-l { grid-area: l; color: var(--phos-a-dim); }
.pf-blend-r { grid-area: r; color: var(--phos-b); }
.pf-blend-in { grid-area: in; width: 100%; accent-color: var(--phos-b); }
.pf-blend-in:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
.pf-blend-v { grid-area: v; text-align: center; letter-spacing: 0; text-transform: none; font-size: var(--t-value); color: var(--phos-b-dim); }
/* The live trace, small in the well's top left (the corner the buttons
   leave free): clear of B and of Blend, so it stays while you compare. The
   XY pad and How it works cover the well, and it gives way to them. */
.pf-scope { position: absolute; left: var(--s3); top: var(--s3); width: 120px; height: 30px; opacity: 0.55; pointer-events: none; }
.pf-well:not([data-mode="face"]) .pf-scope { display: none; }
/* The well's corner: XY and stage mode. */
.pf-corner { position: absolute; top: var(--s3); right: var(--s3); display: flex; gap: var(--s2); z-index: 3; }
.pf-corner .util-btn { display: inline-flex; align-items: center; gap: var(--s1); }
.pf-xy-btn[aria-pressed="true"] { color: var(--phos-a); border-color: var(--phos-a-deep); }
/* In booth mode the attract band lies over the top of the well. */
.pf-marquee { position: absolute; left: var(--s3); right: var(--s3); top: var(--s3); display: grid; justify-items: center; pointer-events: none; z-index: 4; }

/* CONTROLS' head: the cap, then Arrange and How it works, as disclosures. */
.pf-ctlhead { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); }
.pf-ctlcap {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk);
}
.pf-actions { display: flex; align-items: center; gap: var(--s3); }
.pf-disclose {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk-dim);
  background: none;
  border: 0;
  padding: var(--s1);
  cursor: pointer;
}
.pf-disclose::before { content: "›"; display: inline-block; margin-right: var(--s1); transition: transform var(--d-state) var(--e-settle); }
.pf-disclose[aria-expanded="true"]::before { transform: rotate(90deg); }
.pf-disclose:hover, .pf-disclose[aria-expanded="true"] { color: var(--silk); }
.pf-disclose:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
.pf-status { min-height: 1.4em; font-size: var(--t-value); color: var(--silk-dim); }

.pf-deck {
  display: grid;
  /* The panel's controls: three columns hold the six, as the specimen sets
     them, four a longer panel (perform.js buildDeck sets the count). */
  grid-template-columns: repeat(var(--pf-cols, 3), minmax(0, 1fr));
  gap: var(--s3) var(--s2);
}
.pf-knob {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--s1) 2px;
  border-radius: var(--r1);
  cursor: ns-resize;
  touch-action: none;
  user-select: none;
  outline: none;
}
.pf-knob:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
.pf-knob svg { width: 100%; max-width: 84px; height: auto; overflow: visible; }
/* The travel, as its two halves (perform.js makeKnob). Open: the way the
   control turns. Closed: a dotted hairline, with a stop at 12 o'clock. */
.pf-k-half { fill: none; stroke: var(--silk-mute); stroke-width: 3; }
.pf-k-half.closed { stroke-width: 1; stroke-dasharray: 1 4; stroke-linecap: round; }
.pf-k-stop { stroke: var(--silk); stroke-width: 2; display: none; }
.pf-knob.half-lo .pf-k-stop, .pf-knob.half-hi .pf-k-stop { display: inline; }
/* A search control being turned: where letting go starts to ask. */
.pf-k-notch { stroke: var(--phos-b); stroke-width: 2; display: none; }
.pf-knob.search.asking .pf-k-notch { display: inline; }
/* A drag into a half-closed control's stop bumps the pointer and back. */
.pf-knob.bump-lo .pf-k-turn { animation: pf-bump-lo var(--d-press) ease-out; }
.pf-knob.bump-hi .pf-k-turn { animation: pf-bump-hi var(--d-press) ease-out; }
@keyframes pf-bump-lo { 50% { transform: rotate(-3deg); } }
@keyframes pf-bump-hi { 50% { transform: rotate(3deg); } }
.pf-k-arc { fill: none; stroke: var(--phos-a); stroke-width: 3.5; stroke-linecap: round; }
/* Wander: where its zones begin (ideas, drift, roam), and a thin arc inside
   the ring filling toward its next move — the model's, so amber. */
.pf-k-zone { stroke: var(--silk-dim); stroke-width: 1.5; stroke-linecap: round; }
.pf-k-count { fill: none; stroke: var(--phos-b-deep); stroke-width: 1.5; stroke-linecap: round; }
/* Where a re-centred control was: a tick that fades over a second while the
   pointer glides home (perform.js recentre). None under reduced motion. */
.pf-k-ghost { stroke: var(--silk-dim); stroke-width: 2; stroke-linecap: round; opacity: 0; }
.pf-k-ghost.fade { animation: pf-ghost 1000ms ease-out forwards; } /* token-exempt: a ghost fading over a second, slower than any move */
@keyframes pf-ghost { from { opacity: 0.85; } to { opacity: 0; } }
.pf-k-body { fill: var(--knob-body); stroke: var(--black); stroke-width: 1; filter: drop-shadow(0 2px 3px var(--black-60)); }
.pf-k-ptr { stroke: var(--silk); stroke-width: 3; stroke-linecap: round; }
.pf-k-where { fill: var(--phos-b); }
.pf-k-name {
  margin-top: var(--s1);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk);
}
.pf-k-ends { font-size: var(--t-label); color: var(--silk-dim); }
.pf-k-sub {
  font-size: var(--t-label);
  color: var(--phos-a-dim);
  max-width: 100%;
  /* Whole, never cut: a knob list like "cutoff · resonance · drive" is the
     answer to "what does this turn?", and a caption that ends in an
     ellipsis reads as broken. It wraps instead, and the block keeps three
     lines' height, so the row does not jump when a caption changes length
     (Wander's does, every few seconds). */
  min-height: 3.9em;
  line-height: 1.3;
  text-align: center;
  /* Even lines read as a caption; a word left alone on the last as a
     mistake. */
  text-wrap: balance;
  overflow-wrap: break-word;
}
/* What velocity plays (Arrange › Velocity plays): a tick over the ring's
   corner, so no word in the cell moves for it. */
.pf-k-vel {
  position: absolute;
  top: var(--s1);
  left: var(--s1);
  display: none;
  font-size: var(--t-label);
  color: var(--phos-a-dim);
  border: 1px solid var(--phos-a-deep);
  border-radius: 3px;
  padding: 0 3px; /* token-exempt: a tag's inset under the 4px step */
  pointer-events: none;
}
.pf-knob.vel .pf-k-vel { display: block; }
/* Unreachable by this patch's knobs: amber, and the gesture asks for an offer. */
.pf-knob.search .pf-k-arc { stroke: var(--phos-b); }
.pf-knob.search .pf-k-half { stroke: var(--phos-b-deep); stroke-dasharray: 3 5; }
.pf-knob.search .pf-k-name { color: var(--phos-b-dim); }
.pf-knob.search .pf-k-sub { color: var(--phos-b-dim); }

.pf-knob.unwired .pf-k-ptr { stroke: var(--silk-mute); }
.pf-knob.unwired .pf-k-arc { stroke: var(--silk-mute); }
/* Listening: not measured yet (no wiring, or a control a Take carried over
   with nothing to turn). The ring stays neutral and quiet, never the dashed
   amber of "can't"; only its waiting sign (.pf-k-wait) is amber, and only
   while a measurement is out. */
.pf-knob.pending .pf-k-half { stroke-width: 1; opacity: 0.6; }
.pf-knob.pending .pf-k-name, .pf-knob.pending .pf-k-sub { color: var(--silk-dim); }
/* A guess (#290, ADR-012): a control playing on a wiring borrowed from a
   measured relative or predicted from the knob table, until this sound's own
   measurement lands. It turns, and its caption says what it turns; only its
   travel wears the not-measured look above, never the dashed amber of
   "can't", and settles when the measurement lands. */
.pf-knob.guess .pf-k-half { stroke-width: 1; opacity: 0.6; }

/* What velocity plays, a row of the Arrange panel. */
.pf-touch {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s3);
  padding: 0 var(--s2) 0 calc(var(--s3) + var(--s2) + var(--s2));
  font-size: var(--t-value);
  color: var(--silk-dim);
}
.pf-touch-l, .pf-touch-d { font-family: var(--font-silk); text-transform: uppercase; letter-spacing: 0.1em; font-size: var(--t-label); color: var(--silk-dim); }
.pf-touch-l { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } /* the section's heading says it on screen */
.pf-touch select { background: var(--bezel); color: var(--silk); border: 1px solid var(--hairline); border-radius: 3px; font: inherit; padding: 2px var(--s1); max-width: 100%; }
.pf-touch input[type=range] { width: 120px; }
.pf-touch-n { flex-basis: 100%; color: var(--phos-a-dim); }

/* The pad row: Wander, ringed in amber (the model's walk; a tap freezes
   it), then OFFER, PEEK, TAKE and PASS. */
.pf-pads {
  display: grid;
  grid-template-columns: 160px minmax(0, 1.15fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 0.95fr);
  align-items: center;
  gap: var(--s2);
}
.pf-pad {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 2px var(--s2);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--silk);
  min-height: 52px;
  padding: var(--s1) var(--s2);
  border-radius: var(--r2);
  border: 1px solid var(--black);
  background: linear-gradient(180deg, var(--btn-hi), var(--btn-lo));
  box-shadow: var(--lift), 0 3px 0 var(--shadow-ink), 0 5px 8px var(--black-50);
  cursor: pointer;
  touch-action: none;
  transition: transform var(--d-press) ease, box-shadow var(--d-press) ease;
}
/* The printed key after the name (`data-key`, drawn before it in the box's
   order). */
.pf-pad[data-key]::before { content: attr(data-key); order: 1; }
.pf-pad:active, .pf-pad.down { transform: translateY(2px); box-shadow: inset 0 1px 0 var(--white-06), 0 1px 0 var(--shadow-ink); }
@media (hover: hover) {
  .pf-pad:hover:not(:disabled) { background: linear-gradient(180deg, var(--btn-hover-hi), var(--btn-hover-lo)); }
  .pf-knob:not(.search):not(.pending):hover .pf-k-half:not(.closed) { stroke: var(--silk-dim); }
}
.pf-pad:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
.pf-pad.lit { color: var(--phos-b); box-shadow: var(--lift), 0 0 12px var(--phos-b-30), 0 3px 0 var(--shadow-ink); }
.pf-pad.flash { animation: pf-flash var(--d-move) ease-out; }
/* OFFER is the model's act, and the one gesture no other instrument has:
   amber, filled, as the specimen sets it. */
.pf-pad.primary {
  color: var(--evolve-ink);
  border-color: var(--phos-b);
  background: linear-gradient(180deg, var(--evolve-hi), var(--evolve-lo));
  box-shadow: var(--lift), 0 0 24px -6px var(--phos-b-60), 0 3px 0 var(--shadow-ink);
}
.pf-pad.primary[data-key]::before { color: var(--evolve-ink); border-color: var(--evolve-edge); }
@media (hover: hover) {
  .pf-pad.primary:hover:not(:disabled) { background: linear-gradient(180deg, var(--evolve-hi), var(--evolve-hi)); }
}
.pf-pad.pf-pass { background: none; border-color: transparent; box-shadow: none; color: var(--silk-dim); }
/* A pad with nothing to act on is *waiting* — at 0.38 it read as broken.
   Dashed, flat and legible; what PEEK, TAKE and PASS wait for is said once,
   under OFFER (perform.js `paintPads`). */
.pf-pad:disabled {
  cursor: default;
  transform: none;
  box-shadow: none;
  border: 1px dashed var(--plate-edge);
  background: transparent;
  color: var(--silk-dim);
}
/* A pad whose press does more than its name says (Offer, while B holds an
   offer, passes on it: NEXT · passes on B), or that speaks for the pads
   waiting on it (OFFER · then peek, take or pass), says so underneath. */
.pf-pad[data-sub]:not([data-sub=""])::after { content: attr(data-sub); }
.pf-pad[data-sub]:not([data-sub=""])::after {
  order: 2;
  flex-basis: 100%;
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
}
@keyframes pf-flash { from { color: var(--phos-a); box-shadow: 0 0 18px var(--phos-a-50); } }
/* Wander in the pad row: its dial, and beside it its name and the word for
   what it is doing (its ends, still and roam, are its tooltip's and its
   ticks'). The word keeps two lines' height, so the row never jumps. */
.pf-knob.pf-wander {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  grid-template-areas: "dial name" "dial line";
  align-items: center;
  justify-items: start;
  column-gap: var(--s2);
  row-gap: 0;
  padding: var(--s1);
}
.pf-wander svg { grid-area: dial; max-width: 44px; }
.pf-wander .pf-k-name { grid-area: name; align-self: end; margin-top: 0; color: var(--phos-b); }
.pf-wander .pf-k-ends { display: none; }
.pf-wander .pf-k-line { grid-area: line; align-self: start; }
.pf-wander .pf-k-sub { text-align: left; min-height: 3.9em; }
.pf-wander .pf-k-half { stroke: var(--phos-b-deep); }
.pf-wander .pf-k-arc { stroke: var(--phos-b); }
/* Frozen: the ring lit and the word under it. */
.pf-wander.held .pf-k-half { stroke: var(--phos-b); }
.pf-wander.held svg { filter: drop-shadow(0 0 6px var(--phos-b-45)); }
.pf-knob.held .pf-k-sub { color: var(--phos-b); }
/* Narrower, the row takes two lines: Wander across the first, the four pads
   under it (the same order). */
@media (max-width: 1360px) {
  .pf-pads { grid-template-columns: repeat(4, minmax(0, 1fr)); row-gap: var(--s1); }
  .pf-pads .pf-wander { grid-column: 1 / -1; justify-self: stretch; grid-template-columns: 36px auto minmax(0, 1fr); grid-template-areas: "dial name line"; }
  .pf-wander svg { max-width: 36px; }
  .pf-wander .pf-k-name, .pf-wander .pf-k-line { align-self: center; }
  .pf-wander .pf-k-sub { min-height: 2.6em; }
}

/* Under the hood: the strip between the controls and the pads. */
.pf-hood { flex: 0 1 auto; min-height: 5.5em; max-height: 9.5em; overflow-y: auto; overscroll-behavior: contain; }
.pf-ctlhead, .pf-status, .pf-deck, .pf-pads { flex: none; }
.pf-hood-h { font-family: var(--font-silk); text-transform: uppercase; letter-spacing: 0.1em; font-size: var(--t-label); color: var(--silk-dim); margin-bottom: var(--s1); }
.pf-hood-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 2px var(--s4); }
/* The value's column holds nine of the mono value's characters: a +2.7 dB,
   1.78 kHz, −12.0 dB whole. At the label's size a character is 7 px in
   Chromium on Linux, which sets each on a whole pixel, and 6.6 px in
   Firefox on Linux; at 5em the column cut nine in both, and eight to
   "1.78 k…" in Chromium. Two values are longer. The balance's far ends
   (a +12.3 dB) are ten, 70 px in Chromium: where the rows stand side by
   side, that room would come out of every name, so they get it only where
   a row has the column's width (max-width: 1180px, below). A sequencer's
   rate with SYNC on (4.00 Hz · sync, main.js's knobInfo) is fourteen, and
   is cut at every width. */
.pf-hood-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 44px minmax(0, 6em);
  align-items: center;
  gap: var(--s2);
  background: none;
  border: 0;
  padding: 2px 0;
  color: var(--silk-dim);
  font: inherit;
  font-size: var(--t-label);
  text-align: left;
  cursor: pointer;
}
.pf-hood-row:hover .pf-hood-name, .pf-hood-row:focus-visible .pf-hood-name { color: var(--silk); }
.pf-hood-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pf-hood-mod { font-family: var(--font-silk); text-transform: uppercase; letter-spacing: 0.08em; color: var(--silk); }
.pf-hood-track { position: relative; height: 6px; border-radius: 3px; background: var(--bezel); border: 1px solid var(--hairline); overflow: hidden; }
.pf-hood-fill { position: absolute; inset: 0 auto 0 0; background: var(--phos-a-deep); }
.pf-hood-home { position: absolute; top: -1px; bottom: -1px; width: 2px; margin-left: -1px; background: var(--silk-dim); }
.pf-hood-val { color: var(--phos-a-dim); text-align: right; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
#rack-svg .hood-pulse { animation: hood-pulse 1.4s ease-out; } /* token-exempt: one attention beat, slower than any move */
@keyframes hood-pulse { 0%, 40% { filter: drop-shadow(0 0 6px var(--phos-a)); } 100% { filter: none; } }
@media (prefers-reduced-motion: reduce) { #rack-svg .hood-pulse { animation: none; outline: 2px solid var(--phos-a); } }

/* How it works: a mode of the well, its words over the dimmed face. */
.pf-why-body {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Clear of the well's corner (XY and stage mode), on a veil over the
     dimmed face so the words read. */
  padding: calc(var(--s7) + var(--s2)) var(--s5) var(--s4);
  background: var(--scrim-82);
  font-family: var(--font-silk);
  font-size: var(--t-body);
  color: var(--silk-dim);
  line-height: 1.5;
}
.pf-why-body.hidden { display: none; }
.pf-why-body b { color: var(--silk); font-weight: 600; }
.pf-why-x { position: absolute; top: var(--s3); left: var(--s3); }

/* Under 1180 px the two columns narrow: smaller dials, the pads' sub-words
   and names in one size, Wander's dial smaller. */
@media (max-width: 1180px) {
  .pf-grid { gap: var(--s4); }
  .pf-mv[data-key]::after { display: none; }
  .view.perform { padding: var(--s4) var(--s3) var(--s3); }
  .pf-knob svg { max-width: 68px; }
  .pf-pad { letter-spacing: 0.1em; padding-inline: var(--s1); }
  .pf-hood-grid { grid-template-columns: minmax(0, 1fr); }
  /* A row the column's width: its value's column holds the balance's ten. */
  .pf-hood-row { grid-template-columns: minmax(0, 1fr) 44px minmax(0, 6.5em); }
}
@media (prefers-reduced-motion: reduce) {
  .pf-pad.flash, .pf-mv.flash { animation: none; }
  .pf-k-ghost.fade { animation: none; }
  .pf-knob.bump-lo .pf-k-turn, .pf-knob.bump-hi .pf-k-turn { animation: none; }
  .pf-disclose::before { transition: none; }
}

/* ---------- PERFORM: the palette, How it works, the offer's moments ---------- */
/* A control being measured (perform.js paintKnob): "listening…" in its own
   box over the line under the control, #live-wait's sign. The line keeps
   its box, hidden, so nothing around it moves. */
.pf-k-line { position: relative; width: 100%; }
.pf-k-wait {
  position: absolute;
  inset: 0 0 auto 0;
  font-size: var(--t-label);
  line-height: 1.3;
  text-align: center;
  color: var(--phos-b);
  white-space: nowrap;
}
.pf-k-wait:empty { display: none; }
.pf-knob.waiting .pf-k-sub { visibility: hidden; }
/* The model view's lean on a control (perform.js `paintLean`): which way your
   taste leans along it at the sound in hand, an arc from 12 o'clock toward
   the end it leans to, just outside the ring, its interval behind it, and
   the model's words over the line under the control in place of the
   caption. Amber, the model's color. A guess (its interval crosses zero)
   is dashed and faint with its interval at full strength (their opacities
   are taste-geom's `pullMark`, set on the paths). Only under the model view,
   and only once the engine has a lean: before the first fit nothing. */
.pf-k-lean { display: none; }
body.model-view .pf-knob.leaning .pf-k-lean { display: inline; }
.pf-k-lean-iv { fill: none; stroke: var(--phos-b); stroke-width: 1; stroke-linecap: round; }
.pf-k-lean-bar { fill: none; stroke: var(--phos-b); stroke-width: 2.5; stroke-linecap: round; }
.pf-k-lean-cut { stroke: var(--phos-b); stroke-width: 1.5; stroke-linecap: round; }
.pf-k-lean-at { fill: var(--phos-b); }
.pf-knob.lean-guess .pf-k-lean-bar { stroke-dasharray: 3 3; stroke-linecap: butt; }
.pf-knob.lean-guess .pf-k-lean-at { fill: none; stroke: var(--phos-b); stroke-width: 1; }
.pf-k-leanw {
  position: absolute;
  inset: 0 0 auto 0;
  display: none;
  font-family: var(--font-voice);
  font-style: italic;
  font-size: var(--t-body);
  line-height: 1.3;
  text-align: center;
  color: var(--phos-b);
}
.pf-knob.lean-guess .pf-k-leanw { color: var(--phos-b-dim); }
body.model-view .pf-knob.leaning .pf-k-leanw { display: block; }
body.model-view .pf-knob.leaning .pf-k-sub,
body.model-view .pf-knob.leaning .pf-k-wait { visibility: hidden; }

/* The palette: a panel over the deck, never a modal; a sheet on a narrow
   screen. Among the popovers on the ladder above (40–45). */
.pp {
  position: fixed;
  z-index: 44;
  width: min(440px, calc(100vw - 2 * var(--s4)));
  max-height: min(70vh, 640px);
  display: flex;
  flex-direction: column;
  padding: var(--s3) var(--s3) var(--s2);
  background: var(--surface-pop);
  border: 1px solid var(--hairline);
  border-radius: var(--r3);
  box-shadow: var(--lift), var(--cast);
}
.pp-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--s3); }
.pp-title { font-family: var(--font-silk); font-size: var(--t-label); letter-spacing: 0.14em; text-transform: uppercase; color: var(--silk); }
.pp-sub { margin: var(--s1) 0 0; font-size: var(--t-value); color: var(--silk-dim); }
.pp-body { overflow-y: auto; min-height: 0; margin-top: var(--s2); display: grid; gap: var(--s3); align-content: start; overscroll-behavior: contain; }
.pp-sec { display: grid; gap: 2px; }
.pp-h { font-family: var(--font-silk); font-size: var(--t-label); letter-spacing: 0.1em; text-transform: uppercase; color: var(--silk-dim); padding: 2px 0 2px calc(var(--s3) + var(--s2) + var(--s2)); }
/* A row: the mark's cell (every row keeps it, empty or not), the name and
   its ends, the waiting sign, then the row's buttons. The name starts at
   the same x in every row, and wraps rather than being cut. */
.pp-row {
  display: grid;
  grid-template-columns: var(--s3) minmax(0, 1fr) auto auto auto auto;
  align-items: center;
  gap: var(--s2);
  padding: var(--s1) var(--s2);
  border-radius: var(--r1);
}
.pp-row.on { background: var(--phos-a-06); }
.pp-mark { width: var(--s3); height: var(--s3); border-radius: 50%; box-sizing: border-box; }
.pp-mark[data-mark="turns"] { background: var(--phos-a); }
.pp-mark[data-mark="search"] { border: 2px dashed var(--phos-b); }
.pp-mark[data-mark="listening"] { border: 2px dotted var(--phos-b); }
.pp-mark[data-mark="unmeasured"] { border: 1px solid var(--silk-dim); }
.pp-txt { display: grid; gap: 2px; min-width: 0; }
.pp-name { font-family: var(--font-silk); font-size: var(--t-label); font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--silk); overflow-wrap: anywhere; }
.pp-ends { font-size: var(--t-value); color: var(--silk-dim); }
.pp-wait { font-size: var(--t-value); color: var(--phos-b); white-space: nowrap; }
.pp-b {
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  background: var(--panel-hi);
  color: var(--silk-dim);
  font-family: var(--font-mono);
  font-size: var(--t-body);
  cursor: pointer;
}
.pp-b:hover:not(:disabled) { color: var(--silk); border-color: var(--silk-dim); }
.pp-b:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 1px; }
.pp-b:disabled { opacity: 0.35; cursor: default; }
.pp-place { color: var(--phos-a); border-color: var(--phos-a-deep); grid-column: -2; }
.pp-hide:hover:not(:disabled) { color: var(--led-red); }
@media (pointer: coarse) { .pp-b { width: 44px; height: 44px; } }
@media (max-width: 700px) {
  .pp { left: 0 !important; right: 0; top: auto !important; bottom: 0; width: auto; max-height: 74vh; border-radius: var(--r3) var(--r3) 0 0; }
}

/* How it works: every control on the panel a tap apart, the one last
   touched open. */
.pf-how-list { display: flex; flex-wrap: wrap; gap: var(--s1); margin: var(--s3) 0 var(--s2); }
.pf-how-tab {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--silk-dim);
  background: none;
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  padding: var(--s1) var(--s2);
  cursor: pointer;
}
.pf-how-tab[aria-pressed="true"] { color: var(--phos-a); border-color: var(--phos-a-deep); }
.pf-how-tab:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 1px; }
.pf-how-one { max-width: 72ch; font-family: var(--font-silk); font-size: var(--t-body); line-height: 1.5; color: var(--silk); margin-bottom: var(--s3); }
.pf-how-one p { margin: var(--s1) 0 0; }
.pf-how-h { display: flex; align-items: baseline; gap: var(--s2); }
.pf-how-name { font-size: var(--t-label); font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; }
.pf-how-ends { font-size: var(--t-value); color: var(--silk-dim); }
.pf-how-how { color: var(--silk-dim); }
.pf-how-here { color: var(--phos-a-dim); }

/* The offer's moments (perform.js growOffer, takeMotion, foldMotion). B
   grows from the sound's face in the well; a copy of it fills green and goes
   into the face when taken, or folds back into it when passed. Under reduced
   motion nothing moves, and B shows the taken state as a fill for a moment. */
.pf-offer-ghost {
  position: fixed;
  z-index: 30;
  margin: 0;
  box-sizing: border-box;
  overflow: hidden;
  pointer-events: none;
  transform-origin: 0 0;
  border-radius: var(--r2);
}
.pf-offer-ghost > .pf-face { position: absolute; inset: 0; }
.pf-ghost-fill {
  position: absolute;
  inset: 0;
  background: var(--phos-a-24);
  transform-origin: 50% 100%;
  transform: scaleY(0);
}

/* Stage mode (⇧F): the sound under your hands, the whole screen. */
html.st-on { overflow: hidden; }
.st-stage {
  position: fixed;
  inset: 0;
  z-index: 95;
  background: var(--bezel);
  outline: none;
  overflow: hidden;
  touch-action: manipulation;
}
.st-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
/* To the specimen (`a-stage.png`): the wordmark and its lamp top left, ×
   top right, the name and "family · in hand" bottom left, the keys centred
   along the foot. */
.st-brand { position: absolute; left: var(--s6); top: var(--s6); display: flex; align-items: center; gap: var(--s2); color: var(--silk-dim); pointer-events: none; }
.st-brand .word { font-family: var(--font-silk); font-size: var(--t-label); font-weight: 500; line-height: 1; letter-spacing: 0.34em; margin-right: -0.34em; }
.st-brand .lamp { width: 7px; height: 7px; }
.st-hud { position: absolute; left: var(--s6); bottom: var(--s6); display: grid; gap: var(--s2); pointer-events: none; }
.st-name { font-family: var(--font-silk); font-size: var(--t-body); font-weight: 600; letter-spacing: 0.3em; text-transform: uppercase; color: var(--silk); }
.st-cat { font-size: var(--t-value); color: var(--silk-dim); }
.st-hint { position: absolute; left: 50%; bottom: var(--s6); transform: translateX(-50%); font-size: var(--t-value); color: var(--silk-dim); white-space: nowrap; pointer-events: none; }
.st-kbd { font-family: var(--font-mono); font-size: var(--t-label); color: var(--silk); padding: 0 var(--s1); border: 1px solid var(--plate-edge); border-radius: 3px; line-height: 1.3; }
.st-leave { position: absolute; right: var(--s5); top: var(--s5); width: 40px; height: 40px; padding: 0; border-radius: 50%; }
/* A refusal said while the stage is on stays in sight: urgent toasts and the
   alarm are lifted over it. A receipt is not news on stage: it is hidden,
   and expires unseen behind it. A refusal's words are also said in the
   stage's own line for a moment (perform.js), in amber. */
html.st-on .toasts { z-index: 96; }
html.st-on .toast:not(.urgent) { display: none; }
html.st-on .alarm { z-index: 97; }
.st-hint.said { color: var(--phos-b); }
.st-tick { position: absolute; transform: translateY(-50%); font-size: var(--t-value); color: var(--silk-dim); pointer-events: none; }
@media (max-width: 700px) {
  .st-hud { left: var(--s4); bottom: var(--s5); }
  .st-brand { left: var(--s4); top: var(--s4); }
  .st-hint { bottom: calc(2 * var(--s7) + var(--s4)); white-space: normal; text-align: center; width: calc(100% - 2 * var(--s4)); }
  .st-leave { right: var(--s3); top: var(--s3); }
}

/* ===================== MIDI panel ===================== */
button.midi-ind { background: none; border: 1px solid transparent; border-radius: 3px; cursor: pointer; font: inherit; color: inherit; padding: 1px var(--s1); }
button.midi-ind:hover, button.midi-ind:focus-visible { border-color: var(--hairline); }
.midi-panel {
  position: absolute;
  right: 12px;
  bottom: calc(100% + 8px);
  z-index: 55;
  width: min(420px, 92vw);
  padding: var(--s3);
  background: var(--surface-pop);
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  box-shadow: var(--lift), var(--cast);
  font-size: var(--t-body);
}
.midi-h { font-family: var(--font-silk); font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; font-size: var(--t-label); color: var(--silk-dim); margin-bottom: var(--s2); }
/* Why there is nothing to list: no Web MIDI, a prompt, a refusal, or no device. */
.midi-why { font-size: var(--t-body); line-height: 1.45; color: var(--silk); max-width: 34ch; margin: 0 0 var(--s2); text-wrap: pretty; }
.midi-why + .util-btn { margin-bottom: var(--s3); }
.midi-rows { display: grid; gap: var(--s1); }
.midi-row { display: grid; grid-template-columns: 1fr 1fr auto auto; gap: var(--s2); align-items: center; }
/* Eight rows of full-height keybar buttons made a panel taller than the knob
   deck it maps. Slimmer, still a 24px target. */
.midi-row .util-btn { padding-block: var(--s1); }
.midi-slot { color: var(--silk); font-family: var(--font-silk); }
.midi-src { color: var(--phos-a-dim); font-size: var(--t-value); }
.midi-foot { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s3); color: var(--silk-dim); font-size: var(--t-value); align-items: center; }

/* Level three at rest takes no room (notes/ui-hierarchy.md). */
.tray.empty, .tray[hidden] { display: none; }
/* The arp's settings follow its switch: gone while it does not run. They were
   recessed-but-present, which read as six controls that did nothing — and then
   pinned open for as long as it ran, which covered the bank's last row and the
   XY pad for a whole performance. Now a row of KEYS ⋯'s settings, shown while
   ARP or SYNC runs; KEYS ⋯ opens when either is switched on and from the
   chip, and folds on a click outside the dock or Escape — never on HOLD, the
   dock's other controls or a note, which are how an arpeggio is set up
   (main.js `setKeysPop`). */
#arp-ctl:not(.open) { display: none; }
/* What stays on the bar while it runs: one machined chip at the head of the
   cluster saying what it is set to. Its chevron points the way the settings
   open: up, and down to fold them. */
.arp-chip {
  text-align: left;
  white-space: nowrap;
  background-image: linear-gradient(135deg, transparent 50%, var(--silk-dim) 50%),
                    linear-gradient(45deg, var(--silk-dim) 50%, transparent 50%),
                    linear-gradient(180deg, var(--plate-hi), var(--plate-lo));
}
.arp-chip[aria-expanded="true"] {
  color: var(--phos-a);
  background-image: linear-gradient(45deg, transparent 50%, var(--silk-dim) 50%),
                    linear-gradient(135deg, var(--silk-dim) 50%, transparent 50%),
                    linear-gradient(180deg, var(--plate-hi), var(--plate-lo));
}
/* …inside KEYS ⋯, which floats above the dock rather than growing it. As a
   third row of the dock it widened a column by ~120 px and made the dock
   taller, so switching the arp on moved every key out from under the hand
   that was about to play it (C3 went from x≈318 to x≈435). The keybed never
   moves now. */
#arp-ctl {
  flex-wrap: nowrap;
  gap: var(--s2);
  padding: var(--s2) 0;
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
}

/* A bred child that landed below its parent: the sampler exploring, said so. */
.lin-explore { color: var(--silk-dim); font-style: italic; border-bottom: 1px dotted var(--silk-mute); cursor: help; }
.pf-offer-what, .pf-ghost-what { color: var(--phos-b); font-style: normal; font-weight: 600; }
/* How far an aimed offer went the way its control was turned: the model's
   measurement, so amber; a miss says so in words, not in another colour. */
.pf-offer-aim, .pf-ghost-aim { color: var(--phos-b-dim); font-style: normal; }

/* ---------- PERFORM: the XY pad, a mode of the well ---------- */
/* Its axis pickers along the well's top edge (clear of the corner's two
   buttons), the field filling the rest, the end words at its edges. */
.pf-xy { position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; gap: var(--s2); padding: var(--s3); }
.pf-xy.hidden { display: none; }
.pf-xy-head { display: flex; align-items: center; gap: var(--s2); min-height: 30px; padding-right: calc(2 * var(--s7) + var(--s5)); color: var(--silk-dim); font-size: var(--t-label); }
.pf-xy-field {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background:
    linear-gradient(90deg, transparent calc(50% - 0.5px), var(--phos-a-12) 50%, transparent calc(50% + 0.5px)),
    linear-gradient(0deg, transparent calc(50% - 0.5px), var(--phos-a-12) 50%, transparent calc(50% + 0.5px)),
    radial-gradient(circle at 50% 50%, var(--phos-a-06), transparent 70%),
    var(--black-50);
  cursor: crosshair;
  touch-action: none;
  overflow: hidden;
}
.pf-xy-field:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
.pf-xy-dot {
  position: absolute;
  width: 18px; height: 18px;
  margin: -9px 0 0 -9px; /* token-exempt: half the dot's 18px, which centers it on the point: geometry, not space */
  border-radius: 50%;
  background: var(--phos-a);
  box-shadow: 0 0 14px var(--phos-a-70);
  pointer-events: none;
  transition: left var(--d-press) linear, top var(--d-press) linear;
}
.pf-xy-l, .pf-xy-r, .pf-xy-t, .pf-xy-b {
  position: absolute;
  font-size: var(--t-label);
  color: var(--silk-dim);
  pointer-events: none;
}
.pf-xy-l { left: 8px; top: 50%; transform: translateY(-50%); }
.pf-xy-r { right: 8px; top: 50%; transform: translateY(-50%); }
.pf-xy-t { top: 6px; left: 50%; transform: translateX(-50%); }
.pf-xy-b { bottom: 6px; left: 50%; transform: translateX(-50%); }
/* An axis the patch can't move says so in its end words, struck through as
   the guide promises: dimmed alone, they read as merely unselected. */
.pf-xy-field.dead-x .pf-xy-l, .pf-xy-field.dead-x .pf-xy-r,
.pf-xy-field.dead-y .pf-xy-t, .pf-xy-field.dead-y .pf-xy-b {
  color: var(--phos-b-dim); opacity: 0.55;
  text-decoration: line-through; text-decoration-thickness: 1px;
}
.pf-xy-note {
  position: absolute; left: 0; right: 0; bottom: 26px;
  text-align: center; font-size: var(--t-label); color: var(--phos-b);
  pointer-events: none;
}
/* An axis still being listened to: dim, not struck through, not amber. */
.pf-xy-field.pending-x .pf-xy-l, .pf-xy-field.pending-x .pf-xy-r,
.pf-xy-field.pending-y .pf-xy-t, .pf-xy-field.pending-y .pf-xy-b { opacity: 0.55; }
.pf-xy-note.pending { color: var(--silk-dim); }
@media (prefers-reduced-motion: reduce) { .pf-xy-dot { transition: none; } }

/* ---------- booth attract mode ----------
   The band lies over the top of PERFORM's well (`.pf-marquee`) rather than
   over the stage: attract is the product's pitch, and the pitch is the
   knobs, pads and hood moving — so the band covers none of them, and
   nothing moves when a visitor takes over. The logotype follows the brand: silk Jost 500 on the
   0.145em ramp, no glow. The phosphor is the caption's, because the caption
   is the live reading — what the invisible hand is doing right now. */
.booth-attract {
  justify-self: center;
  max-width: 100%;
  display: inline-flex;
  align-items: baseline;
  gap: var(--s4);
  padding: var(--s2) var(--s5) var(--s2);
  border: 1px solid var(--phos-a-deep);
  border-radius: 999px;
  background: var(--scrim-90);
  box-shadow: var(--lift);
  pointer-events: none;
  white-space: nowrap;
}
.booth-attract.hidden { display: none; }
.ba-title {
  font-family: var(--font-silk);
  font-weight: 500;
  font-size: var(--t-title);
  letter-spacing: 0.145em;
  color: var(--silk);
}
/* Under the wordmark's title size: the line at the body size, its mono
   caption at the value size. */
.ba-sub { font-family: var(--font-silk); font-size: var(--t-body); color: var(--silk); animation: ba-pulse 2.4s ease-in-out infinite; } /* token-exempt: a loop's period, not a transition */
.ba-cap { font-size: var(--t-value); color: var(--phos-b); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ba-cap:empty { display: none; }
@keyframes ba-pulse { 50% { opacity: 0.45; } }
@media (prefers-reduced-motion: reduce) { .ba-sub { animation: none; } }

/* PERFORM's live value on a rack knob, beside the kept pointer. */
.knob-ghost {
  stroke: var(--phos-b);
  stroke-width: 2.4;
  stroke-linecap: round;
  filter: drop-shadow(0 0 3px var(--phos-b-85));
  pointer-events: stroke;
}
g.performed .knob-body { stroke: var(--phos-b-dim); }
g.performed .knob-value { fill: var(--phos-b); }
/* Under 0.8× — where the knob label tier drops below 8px (`syncSilkFloor`) —
   the amber readout is too small to carry "performed", or is not drawn at
   all, and the ghost pointer is a 2px stub on a knob 20px across. So the
   body's ring lights amber too, at a weight that survives the zoom: the knob
   PERFORM is playing can be found across the whole rack at a glance. */
#rack-svg[data-illegible~="label"] g.performed .knob-body { stroke: var(--phos-b); stroke-width: 2.5; }

/* ---------- the guide pill (guide.js) ----------
   The first-visit steps, one at a time, bottom left of the stage (the
   specimen's `.guide`): pips, the step, ×. PERFORM's steps today, so it
   shows on PERFORM, and PERFORM keeps it a row of room under the well. */
.guide { position: absolute; left: var(--s5); bottom: var(--s4); z-index: 6; max-width: calc((100% - var(--rail-w)) / 2 - var(--s5)); }
.guide.hidden, body:not([data-level="perform"]):not([data-level="patch"]) .guide { display: none; }
/* Where a level has no other room for a toast (PERFORM on a small phone, or
   with the XY pad or HOW IT WORKS filling the well), the pill gives way
   while the toast stands over it, its place kept (main.js `laneOverGuide`):
   unseen and out of the pointer's way, its × out of reach, and its step
   still a status a screen reader hears as it comes up (hidden whole, it
   left the accessibility tree, and a step that came up then went unsaid). */
.guide.under-toast { opacity: 0; pointer-events: none; }
.guide.under-toast button { visibility: hidden; }
/* PATCH leaves the pill a row under its well, as PERFORM does. */
/* Each room for the pill is asked of the stage, whose child the pill is,
   and of its children only: asked of body, it was asked again across the
   whole page at every element added or taken away anywhere in it, so each
   of ⌘K's letters restyled more of the page, and slower (#513). :where()
   keeps each rule's specificity. */
body[data-level="patch"] :where(.stage):has(> #guide:not(.hidden)) > #view-patch { padding-bottom: calc(var(--s7) + var(--s2)); }
body :where(.stage):has(> #guide:not(.hidden)) .view.perform .pf-left { padding-bottom: var(--s7); }
.guide .next {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
  max-width: 100%;
  height: 36px;
  padding: 0 var(--s2) 0 var(--s4);
  border: 1px solid var(--plate-edge);
  border-radius: 999px;
  background: var(--panel-hi);
  box-shadow: var(--cast);
  font-family: var(--font-silk);
  font-size: var(--t-body);
  color: var(--silk);
}
.guide .pips { display: flex; gap: var(--s1); }
.guide .pips i { width: 5px; height: 5px; border-radius: 50%; background: var(--plate-edge); }
.guide .pips i.done { background: var(--phos-a); }
.guide .pf-step { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.guide .pf-step.all { color: var(--phos-a); }
.guide .x { flex: none; width: 24px; height: 24px; display: grid; place-items: center; border: 0; border-radius: 50%; background: none; color: var(--silk-dim); cursor: pointer; }
.guide .x:hover { color: var(--silk); background: var(--plate-edge); }
.guide .x:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 1px; }
/* Under a finger, the pill and its × are a finger's size. */
@media (pointer: coarse) {
  .guide .next { height: 44px; }
  .guide .x { width: 40px; height: 40px; }
}
@media (max-width: 1180px) {
  .guide { left: var(--s3); bottom: var(--s3); max-width: calc((100% - var(--rail-w)) / 2 - var(--s3)); }
  .guide .next { font-size: var(--t-value); gap: var(--s2); padding-left: var(--s3); }
}
/* A preset row whose load is on its way: said at once, where the eye is. A
   bank row whose bench open is on its way says the same, until the rack is
   the patch it names (a bench open is a render, and on a busy engine a row
   used to sit outlined for seconds over the old rack with nothing said). */
.preset-item.loading,
.bank-item.opening { box-shadow: inset 0 0 0 1px var(--phos-a-deep); }
.preset-item.loading .bi-name::after,
.bank-item.opening .bi-name::after {
  content: "opening…";
  margin-left: var(--s2);
  font-family: var(--font-mono);
  font-size: var(--t-value);
  color: var(--phos-a-dim);
  animation: pf-breathe 1.2s ease-in-out infinite; /* token-exempt: a loop's period, not a transition */
}
@keyframes pf-breathe { 50% { opacity: 0.45; } }
/* PERFORM while the patch the player picked is on its way: the title still
   names what the keys play (it is true), dimmed, and the status line names
   what is coming. */
.pf-name.pending { opacity: 0.55; }

/* ---------- PERFORM on a shorter screen ----------
   Everything PERFORM needs mid-phrase has to be on screen without scrolling:
   on a screen under ~840 px tall the right column tightens (smaller dials,
   lower pads, less air). */
@media (max-height: 840px) {
  .pf-right { gap: var(--s2); }
  .pf-deck { row-gap: var(--s1); }
  .pf-knob svg { max-width: 56px; }
  .pf-pad { min-height: 44px; }
  .pf-hood { min-height: 3.6em; max-height: 6.5em; }
}

/* ---------- Explain anything (explain.js, Plan-005 task 10) ----------
   The ? chip beside a control, a touch's hold ring, the answer (a figure, a
   sentence) and the lesson on filters. The chip and the answer are fixed
   overlays: nothing in a row moves for them. Among the popovers (40–45),
   the answer above the palette; the lesson is a dialog over the views, and
   while it is open the toasts and the alarm are lifted over it, as stage
   mode lifts them. */
.xp-chip {
  position: fixed;
  z-index: 46;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  line-height: 1;
  color: var(--silk);
  background: var(--panel-hi);
  border: 1px solid var(--plate-edge);
  box-shadow: var(--cast);
  opacity: 0;
  transform: scale(0.8);
  pointer-events: none;
  cursor: pointer;
  transition: opacity var(--d-state), transform var(--d-state) var(--e-settle), border-color var(--d-press);
}
.xp-chip.on { opacity: 1; transform: none; pointer-events: auto; }
.xp-chip:hover { border-color: var(--phos-a-deep); color: var(--phos-a); }
/* A finger held on a control: a ring fills for as long as the hold takes
   (PERFORM's long press), then the answer opens. */
@property --xp-k { syntax: "<number>"; inherits: false; initial-value: 0; }
.xp-ring {
  position: fixed;
  z-index: 95;
  width: 64px;
  height: 64px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  background: conic-gradient(var(--phos-a) calc(var(--xp-k) * 1turn), var(--phos-a-12) 0);
  -webkit-mask: radial-gradient(circle, transparent 27px, var(--black) 28px);
  mask: radial-gradient(circle, transparent 27px, var(--black) 28px);
}
.xp-ring.on { opacity: 1; --xp-k: 1; transition: --xp-k calc(var(--d-move) + var(--d-press)) linear; }
.xp-ring.done { --xp-k: 1; animation: xp-ring-done var(--d-state) var(--e-settle) forwards; }
@keyframes xp-ring-done { 0% { opacity: 1; } 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.35); } }

.xp {
  position: fixed;
  z-index: 45;
  width: min(360px, calc(100vw - 2 * var(--s3)));
  box-sizing: border-box;
  padding: var(--s3) var(--s3) var(--s3);
  background: var(--surface-pop);
  border: 1px solid var(--hairline);
  border-radius: var(--r3);
  box-shadow: var(--shadow-pop);
  display: none;
  outline: none;
}
.xp.on { display: grid; gap: var(--s2); }
/* The caret, on the edge facing what was asked, at its center. */
.xp[data-side]::before {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  background: var(--pop-hi);
  border: 1px solid var(--hairline);
  transform: rotate(45deg);
}
.xp[data-side="below"]::before { top: -6px; left: calc(var(--cx) - 5px); border-right: 0; border-bottom: 0; }
.xp[data-side="above"]::before { bottom: -6px; left: calc(var(--cx) - 5px); border-left: 0; border-top: 0; background: var(--pop-lo); }
.xp[data-side="right"]::before { left: -6px; top: calc(var(--cy) - 5px); border-top: 0; border-right: 0; }
.xp[data-side="left"]::before { right: -6px; top: calc(var(--cy) - 5px); border-bottom: 0; border-left: 0; }
.xp-head { display: flex; justify-content: space-between; align-items: center; gap: var(--s2); }
.xp-title { font-family: var(--font-silk); font-size: var(--t-label); font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--silk); }
.xp-x {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
  color: var(--silk-mute);
  font-size: var(--t-body);
  cursor: pointer;
}
.xp-x:hover { color: var(--silk); background: var(--plate-hi); }
.xp-x:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 1px; }
.xp-fig {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r2);
  background: var(--bezel);
  border: 1px solid var(--hairline);
  cursor: pointer;
}
/* The last measurement, kept while the next one renders. */
.xp-fig.old { opacity: 0.45; }
/* Every control on the panel, a tap apart; the pressed one is the answer's.
   A fixed height and a border on every button, so pressing moves nothing. */
.xp-sw { display: flex; flex-wrap: wrap; gap: var(--s1); }
.xp-sw button {
  /* Whole names, never cut: eight wrap onto a second row. */
  flex: 1 0 auto;
  height: 28px;
  padding: 0 var(--s1);
  border-radius: var(--r1);
  border: 1px solid var(--hairline);
  background: none;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--silk-dim);
  cursor: pointer;
  white-space: nowrap;
}
.xp-sw button:hover { color: var(--silk); border-color: var(--silk-dim); }
.xp-sw button[aria-pressed="true"] { color: var(--phos-a); border-color: var(--phos-a-deep); background: var(--phos-a-06); }
.xp-sw button:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 1px; }
.xp-say { margin: 0; font-family: var(--font-silk); font-size: var(--t-body); line-height: 1.45; color: var(--silk); }
.xp-acts { display: flex; flex-wrap: wrap; gap: var(--s2); }
.xp-learn { display: inline-flex; align-items: center; gap: var(--s2); }
.xp-sub { font-family: var(--font-mono); font-size: var(--t-label); letter-spacing: 0; text-transform: none; color: var(--silk-dim); }
@media (pointer: coarse) { .xp-sw button { height: 40px; } }
@media (max-width: 700px) {
  .xp { left: 0 !important; right: 0; top: auto !important; bottom: 0; width: 100%; border-radius: var(--r3) var(--r3) 0 0; }
  .xp[data-side]::before { display: none; }
}
@media (max-height: 500px) {
  .xp { max-height: calc(100vh - 16px); overflow-y: auto; }
}

/* The lesson: a dialog, the shape beside the words. */
.xl {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: none;
  place-items: center;
  background: var(--scrim-82);
  backdrop-filter: blur(3px);
  padding: var(--s4);
}
.xl.on { display: grid; }
html.xl-on .toasts { z-index: 61; }
html.xl-on .alarm { z-index: 62; }
.xl-panel {
  width: min(760px, 100%);
  box-sizing: border-box;
  max-height: calc(100vh - 2 * var(--s4));
  overflow-y: auto;
  background: var(--surface-pop);
  border: 1px solid var(--hairline);
  border-radius: var(--r3);
  box-shadow: var(--shadow-pop);
  padding: var(--s4) var(--s5);
  display: grid;
  gap: var(--s3);
}
.xl-top { display: flex; justify-content: space-between; align-items: center; }
.xl-cap { font-family: var(--font-silk); font-size: var(--t-label); font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--silk); }
.xl-main { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: var(--s5); align-items: center; min-height: 320px; }
.xl-shape { width: 200px; height: 300px; border-radius: var(--r2); background: var(--bezel); border: 1px solid var(--hairline); }
.xl-body { display: grid; gap: var(--s3); align-content: center; min-width: 0; }
.xl-body p { margin: 0; font-family: var(--font-silk); font-size: var(--t-body); line-height: 1.45; color: var(--silk-dim); max-width: 46ch; }
.xl-body .xl-try { color: var(--silk); }
.xl-body .xl-trouble { color: var(--silk); }
.xl-body .xl-trouble.fail { color: var(--led-red); }
.xl-body .xl-trouble:empty { display: none; }
.xl-panel:focus { outline: none; }
.xl-h { margin: 0; font-family: var(--font-silk); font-size: var(--t-title); font-weight: 500; line-height: 1.15; color: var(--silk); text-wrap: balance; }
.xl-filter {
  width: 320px;
  max-width: 100%;
  height: auto;
  aspect-ratio: 320 / 150;
  border-radius: var(--r2);
  background: var(--bezel);
  border: 1px solid var(--hairline);
  cursor: ew-resize;
  touch-action: none;
}
.xl-filter:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
.xl-list { margin: 0; padding-left: var(--s4); display: grid; gap: var(--s2); font-family: var(--font-silk); font-size: var(--t-body); line-height: 1.45; color: var(--silk); }
.xl-body .util-btn { justify-self: start; }
.xl-nav { display: flex; align-items: center; gap: var(--s3); border-top: 1px solid var(--hairline); padding-top: var(--s3); }
.xl-pips { display: flex; gap: var(--s1); }
.xl-pips i { width: 7px; height: 7px; border-radius: 50%; background: var(--plate-edge); }
.xl-pips i.on { background: var(--phos-a); }
.xl-count { margin-right: auto; font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-mute); }
.xl-next { display: inline-flex; align-items: center; gap: var(--s2); }
.xl-next.primary { color: var(--phos-a); border-color: var(--phos-a-deep); }
.xl-next kbd {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  color: var(--silk-dim);
  border: 1px solid var(--hairline);
  border-radius: 3px;
  padding: 0 var(--s1);
}
@media (pointer: coarse) { .xp kbd, .xl kbd, .xp-chip { display: none; } }
@media (max-width: 700px) {
  .xl { padding: 0; place-items: end stretch; }
  .xl-panel { border-radius: var(--r3) var(--r3) 0 0; max-height: 92vh; padding: var(--s4); }
  .xl-main { grid-template-columns: 1fr; justify-items: center; gap: var(--s4); min-height: 0; }
  .xl-shape { width: 120px; height: 180px; }
  .xl-panel[data-step="1"] .xl-main { grid-template-columns: 88px minmax(0, 1fr); justify-items: stretch; align-items: start; gap: var(--s3); }
  .xl-panel[data-step="1"] .xl-shape { width: 88px; height: 132px; position: sticky; top: 0; }
  .xl-panel[data-step="1"] .xl-body > p.xl-p { display: none; }
  .xl-panel[data-step="1"] .xl-filter { grid-column: 1 / -1; }
}
@media (max-height: 500px) {
  .xl { padding: var(--s2); }
  .xl-main { grid-template-columns: 96px minmax(0, 1fr); min-height: 0; gap: var(--s3); align-items: start; }
  .xl-shape { width: 96px; height: 144px; }
  .xl-body > p.xl-p { display: none; }
  .xl-filter { max-height: 120px; width: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .xp-ring.done { animation: none; opacity: 0; }
}

/* =====================================================================
   PATCH, Plan-005 task 7 (patch.js): the model's guess, each cable's
   measured level, a patch from nothing, and the module sheet on touch.
   ===================================================================== */

/* ---------- the guess ----------
   A guess looks like a guess (voice.md, ADR-012): dashed amber, and the
   model's italic for its reason. The line sits over the rack's top left, as
   the prototype's readout does; the module is drawn at its socket. */
.guess-read {
  position: absolute;
  z-index: 5;
  left: var(--s4);
  top: var(--s3);
  right: var(--s7);
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  pointer-events: none;
}
.guess-read[hidden] { display: none; }
/* The words, the live region: laid out as the line was before the ✕ moved
   out of it. */
.guess-read .gr-line {
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: var(--s3);
}
.gr-chip {
  flex: none;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--phos-b);
}
.gr-why {
  min-width: 0;
  font-family: var(--font-voice);
  font-style: italic;
  font-size: var(--t-voice);
  color: var(--silk);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.guess-read .gr-why:only-child { color: var(--silk-dim); }
/* The way back from a place asked about to the output's guess. */
.gr-back {
  flex: none;
  pointer-events: auto;
  align-self: center;
  border: 0;
  background: none;
  padding: 0 var(--s1);
  font-size: var(--t-label);
  line-height: 1;
  color: var(--silk-dim);
  cursor: pointer;
}
.gr-back:hover, .gr-back:focus-visible { color: var(--silk); }
@media (pointer: coarse) { .gr-back { min-width: 40px; min-height: 40px; } }
/* "What goes here?": where the guess asked for a place goes (after the
   drive, on the filter's cutoff, in the empty socket). */
.gr-at {
  flex: none;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--silk-dim);
}

.rack-guess .guess-lead { fill: none; stroke: var(--phos-b-55); stroke-width: 1.6; stroke-dasharray: 5 5; }
.rack-guess .guess-at { fill: var(--phos-b); }
.guess-plate { cursor: pointer; outline: none; }
.guess-plate .gp-body {
  fill: var(--phos-b-06);
  stroke: var(--phos-b-dim);
  stroke-width: 1.5;
  stroke-dasharray: 6 5;
  transition: stroke var(--d-state) ease, fill var(--d-state) ease;
}
/* Over an empty socket it covers the socket's own "drop a source" outright. */
.guess-plate.over .gp-body { fill: var(--rack); }
.guess-plate.over:hover .gp-body, .guess-plate.over:focus-visible .gp-body { fill: var(--rack); }
.guess-plate:hover .gp-body,
.guess-plate:focus-visible .gp-body { stroke: var(--phos-b); fill: var(--phos-b-10); }
.guess-plate .gp-name {
  font-family: var(--font-silk);
  font-size: var(--t-rack-title);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  fill: var(--phos-b);
}
.guess-plate .gp-word {
  font-family: var(--font-silk);
  font-size: var(--t-rack-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  fill: var(--phos-b-dim);
}
.guess-plate .gp-p { font-family: var(--font-mono); font-size: var(--t-rack-value); fill: var(--phos-b-dim); }
/* Under the silkscreen floor the guess reads as a module does (#519): its
   name larger, its small print left off (the well's top line says it all at
   full size). */
#rack-svg.silk-floor[data-illegible~="name"] .guess-plate .gp-name { font-size: calc(var(--t-rack-title) * 1.3); }
#rack-svg.silk-floor[data-illegible~="label"] .guess-plate .gp-word,
#rack-svg.silk-floor[data-illegible~="label"] .guess-runner .gr-name,
#rack-svg.silk-floor[data-illegible~="value"] .guess-plate .gp-p,
#rack-svg.silk-floor[data-illegible~="value"] .guess-runner .gr-lcb,
#rack-svg.silk-floor[data-illegible~="value"] .rack-guess .gp-face-word { visibility: hidden; }
/* Under the model view, the ranking's next two: fainter, smaller, dashed,
   each with its lower bound; not controls. */
.guess-runner { pointer-events: auto; }
.rack-guess .runner-lead { fill: none; stroke: var(--phos-b-30); stroke-width: 1.2; stroke-dasharray: 3 4; pointer-events: none; }
.guess-runner .gr-body { fill: var(--phos-b-06); stroke: var(--phos-b-40); stroke-width: 1.2; stroke-dasharray: 4 4; }
.guess-runner .gr-name {
  font-family: var(--font-silk);
  font-size: var(--t-rack-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  fill: var(--phos-b-70);
  pointer-events: none;
}
.guess-runner .gr-lcb { font-family: var(--font-mono); font-size: var(--t-rack-value); fill: var(--phos-b-55); pointer-events: none; }
/* Beside the guess plate: the patch's face, and its face with the guess, as
   rendered (Plan-005 task 3). The words under them are the rack's value tier. */
.rack-guess .gp-face { pointer-events: none; }
.rack-guess .gp-face-word { font-family: var(--font-mono); font-size: var(--t-rack-value); fill: var(--silk-dim); text-anchor: middle; pointer-events: none; }
.guess-plate text { pointer-events: none; }
.gp-skip { cursor: pointer; outline: none; }
.gp-skip .gp-skip-hit { fill: transparent; stroke: transparent; }
.gp-skip path { stroke: var(--phos-b-dim); stroke-width: 1.6; stroke-linecap: round; }
.gp-skip:hover path, .gp-skip:focus-visible path { stroke: var(--phos-b); }
.gp-skip:focus-visible .gp-skip-hit { stroke: var(--phos-b-55); }

/* The module rail's row for the guessed kind: an amber mark in the row's own
   left padding, so its name never moves. */
.nb-item { position: relative; }
.nb-item.guessed::before {
  content: "";
  position: absolute;
  left: 1px;
  top: 50%;
  width: 5px;
  height: 5px;
  margin-top: -2.5px; /* token-exempt: half the mark, to centre it */
  border-radius: 50%;
  background: var(--phos-b);
  box-shadow: 0 0 6px var(--phos-b-60);
}

/* ---------- each cable's measured level ----------
   The light a cable carries at rest is its level as the engine measured it
   (`edit_cable_levels`): stroke opacity, set per cable by `paintWireLevel`,
   eased so the light arriving reads as the measurement landing. The mark on
   its middle lights a bar per third of the meter's range, hollow while the
   patch has changed since it was measured. */
/* At rest only: while notes sound the live meter drives it, and a transition
   would make the meter lag. */
#rack-svg:not(.flowing) .wire.audio { transition: stroke-opacity var(--d-state) ease; }
.cable-marks { pointer-events: none; }
.cable-mark { pointer-events: all; }
.cable-mark .lm-box { fill: var(--rack-96); stroke: var(--phos-a-45); stroke-width: 1; }
.cable-mark .lm-bar { fill: none; stroke: var(--phos-a-45); stroke-width: 0.8; }
.cable-mark .lm-bar.on { fill: var(--phos-a); stroke: var(--phos-a); }
.cable-mark.unknown .lm-box { stroke: var(--silk-mute); stroke-dasharray: 2 2; }
.cable-mark.unknown .lm-bar { stroke: var(--silk-mute); }

/* ---------- the module sheet (touch) ----------
   The prototype's sheet: up from the bottom on a narrow screen, a panel at
   the bottom right on a wide one. Every setting is a wide slider with − and
   + steps, or its choices as buttons. */
.msheet {
  position: fixed;
  z-index: 58;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  max-height: 60vh;
  padding: var(--s1) var(--s3) var(--s3);
  background: var(--panel);
  border: 1px solid var(--hairline);
  border-bottom: 0;
  border-radius: var(--r3) var(--r3) 0 0;
  box-shadow: 0 -24px 48px -24px var(--black-90);
  transform: translateY(calc(100% + 24px));
  visibility: hidden;
  transition: transform var(--d-move) var(--e-settle), visibility 0s linear var(--d-move);
}
.msheet.on { transform: none; visibility: visible; transition: transform var(--d-move) var(--e-settle), visibility 0s; }
.msheet.dragging { transition: none; }
@media (min-width: 701px) and (min-height: 501px) {
  .msheet {
    left: auto;
    right: var(--s5);
    bottom: var(--s5);
    width: 420px;
    border-radius: var(--r3);
    border-bottom: 1px solid var(--hairline);
  }
}
.ms-grab { flex: none; width: 40px; height: 4px; border-radius: 2px; background: var(--hairline); margin: var(--s2) auto; touch-action: none; }
.ms-head { flex: none; display: flex; align-items: center; justify-content: space-between; gap: var(--s3); touch-action: none; }
.ms-title { display: flex; align-items: baseline; gap: var(--s3); min-width: 0; }
.ms-name { font-family: var(--font-silk); font-size: var(--t-title); font-weight: 500; color: var(--silk); }
.ms-sub { font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ms-x {
  flex: none; width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center;
  background: none; border: 0; color: var(--silk-dim); font-size: var(--t-body); cursor: pointer;
}
.ms-body { overflow-y: auto; min-height: 0; overscroll-behavior: contain; padding-bottom: var(--s1); }
.ms-says { margin: var(--s1) 0 var(--s2); font-size: var(--t-body); line-height: 1.35; color: var(--silk-dim); }
.ms-rows { display: grid; gap: var(--s2); }
.ms-row {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) 44px;
  grid-template-areas: "lab lab lab" "minus track plus";
  gap: 2px var(--s2);
  align-items: center;
  border-radius: var(--r2);
}
.ms-row > .ms-lab { grid-area: lab; }
.ms-row > .ms-step:first-of-type { grid-area: minus; }
.ms-row > .ms-track { grid-area: track; }
.ms-row > .ms-step:last-of-type { grid-area: plus; }
.ms-row.hl { background: var(--phos-a-06); box-shadow: 0 0 0 1px var(--phos-a-deep); }
.ms-lab { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s2); }
.ms-n { font-family: var(--font-silk); font-size: var(--t-label); font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--silk); }
.ms-val { font-family: var(--font-mono); font-size: var(--t-value); color: var(--silk-dim); font-variant-numeric: tabular-nums; }
.ms-step {
  width: 44px; height: 44px; border-radius: var(--r2);
  border: 1px solid var(--hairline); background: var(--panel-hi); color: var(--silk);
  font-family: var(--font-mono); font-size: var(--t-title); line-height: 1;
  display: grid; place-items: center; touch-action: manipulation; user-select: none; -webkit-user-select: none;
  cursor: pointer;
}
.ms-step:active { background: var(--plate-lo); }
.ms-track { position: relative; height: 44px; touch-action: none; cursor: pointer; border-radius: var(--r2); outline: none; }
.ms-track:focus-visible { box-shadow: 0 0 0 2px var(--phos-a-deep); }
.ms-rail, .ms-fill { position: absolute; top: 50%; height: 4px; margin-top: -2px; border-radius: 2px; }
.ms-rail { left: 14px; right: 14px; background: var(--hairline); }
.ms-fill { left: 14px; background: var(--phos-a); box-shadow: 0 0 8px var(--phos-a-50); }
.ms-thumb {
  position: absolute; top: 50%; width: 26px; height: 26px; margin: -13px 0 0 -13px; /* token-exempt: half the thumb, to centre it */
  border-radius: 50%; background: radial-gradient(circle at 40% 35%, var(--knob-hi), var(--knob-mid));
  border: 2px solid var(--phos-a); box-shadow: 0 2px 6px var(--black-60);
}
.ms-row-seg { grid-template-columns: 1fr; grid-template-areas: none; }
.ms-row-seg > .ms-lab { grid-area: auto; }
.ms-seg { display: flex; flex-wrap: wrap; gap: var(--s1); }
.ms-seg button {
  flex: 1; min-width: 64px; height: 40px; border-radius: var(--r2);
  border: 1px solid var(--hairline); background: none; cursor: pointer;
  font-family: var(--font-silk); font-size: var(--t-label); font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--silk-dim);
}
.ms-seg button[aria-checked="true"] { color: var(--phos-a); border-color: var(--phos-a-deep); background: var(--phos-a-06); }
.ms-foot { flex: none; display: flex; justify-content: flex-end; padding-top: var(--s3); margin-top: var(--s2); border-top: 1px solid var(--hairline); }
.ms-rm { height: 40px; }
.ms-rm:hover, .ms-rm:active { color: var(--led-red); }

@media (prefers-reduced-motion: reduce) {
  #rack-svg:not(.flowing) .wire.audio, .guess-plate .gp-body { transition: none; }
}

/* =====================================================================
   PATCH, Plan-008 C2a: the mock's canvas (`z-desk-patch.png`). The head
   (cap, name, subtitle; the acts on the right), the well (the rack on its
   rounded canvas, the face at OUT, the camera's corner, the catalog on
   demand), and the readout at the well's foot.
   ===================================================================== */
#view-patch { padding: var(--s5) var(--s5) var(--s3); gap: var(--s3); }
.pt-head {
  flex: none;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "cap next" "title acts" "sub edit";
  align-items: center;
  column-gap: var(--s4);
  row-gap: var(--s1);
  min-width: 0;
}
.pt-cap-row { grid-area: cap; display: flex; align-items: baseline; gap: var(--s2); min-height: 24px; }
.pt-head .nextstep { grid-area: next; justify-self: end; font-size: var(--t-value); padding: 2px var(--s3); }
.pt-head .nextstep:empty { visibility: hidden; }
.pt-cap {
  white-space: nowrap;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk-dim);
}
.pt-family { color: var(--silk-mute); }
/* The name at the level's display size, as PERFORM's (one line, the whole
   name in its title when a long one is cut). */
#view-patch .pt-title {
  grid-area: title;
  margin: 0;
  min-width: 0;
  max-width: none;
  font-family: var(--font-silk);
  font-size: clamp(var(--t-title), 3.5vw, var(--t-display));
  font-weight: 400;
  line-height: 1.08;
  letter-spacing: -0.01em;
  color: var(--silk);
}
#view-patch .pt-title.hearing { color: var(--phos-b); }
.pt-sub {
  grid-area: sub;
  margin: 0;
  min-height: 32px;
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-width: 0;
}
#view-patch .pt-sub .subject-meta {
  min-width: 0;
  font-family: var(--font-silk);
  font-size: var(--t-body);
  line-height: 1.45;
  color: var(--silk-dim);
}
.pt-sub .budget:empty { display: none; }
.pt-headacts { grid-area: acts; display: flex; align-items: center; justify-content: flex-end; gap: var(--s2); }
.pt-editbar { grid-area: edit; justify-self: end; }
/* The head's acts: silk caps, a glyph, quiet until approached. */
.pt-act {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  height: 32px;
  padding: 0 var(--s3);
  border: 1px solid transparent;
  border-radius: var(--r2);
  background: none;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--silk-dim);
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--d-press) ease, border-color var(--d-press) ease, background var(--d-press) ease;
}
.pt-act:hover:not(:disabled) { color: var(--silk); border-color: var(--hairline); background: var(--panel-hi); }
.pt-act:disabled { color: var(--silk-mute); cursor: default; }
.pt-act[aria-expanded="true"] { color: var(--phos-a); }
.pt-act:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 1px; }
.pt-ic { width: 14px; height: 14px; flex: none; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.pt-act kbd {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: 0;
  text-transform: none;
  color: var(--silk-mute);
  border: 1px solid var(--hairline);
  border-radius: var(--r1);
  padding: 0 var(--s1);
}
@media (pointer: coarse) { .pt-act kbd { display: none; } .pt-act { height: 40px; min-width: 40px; justify-content: center; } }
.seg-new { gap: var(--s1); }
.seg-new:empty { display: none; }
.seg-new > * { display: inline-flex; }
#patch-clear:hover:not(:disabled) { color: var(--led-red); }
/* Narrower, the acts keep their glyphs and give up their words (each is
   named by its aria-label and its title); ⚡ keeps its words. */
@media (max-width: 1399px) {
  .pt-act .pt-act-l, .pt-act kbd { display: none; }
  .pt-act { padding: 0 var(--s2); }
}
/* ⚡ EVOLVE FROM THIS and its ▾: one control, amber, the model's act. */
.pt-evolve { position: relative; display: inline-flex; align-items: stretch; }
.pt-evolve #rack-evolve { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.pt-evolve-more {
  width: 28px;
  border: 1px solid var(--evolve-edge);
  border-left-color: var(--evolve-cast);
  border-radius: 0 var(--r1) var(--r1) 0;
  background: linear-gradient(180deg, var(--evolve-hi), var(--phos-b) 62%, var(--evolve-lo));
  color: var(--evolve-ink);
  font-size: var(--t-label);
  cursor: pointer;
}
.pt-evolve-more:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 1px; }
@media (pointer: coarse) { .pt-evolve-more { width: 40px; } .pt-evolve #rack-evolve { min-height: 40px; } }
/* A menu over the head: ⚡'s locks, the camera's layout. */
.pt-menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--s1));
  z-index: 30;
  display: grid;
  min-width: 180px;
  padding: var(--s1);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: var(--surface-pop);
  box-shadow: var(--shadow-pop);
}
.pt-menu.hidden { display: none; }
.pt-menu .tt { display: grid; }
.pt-mi {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: 32px;
  padding: 0 var(--s3);
  border: 0;
  border-radius: var(--r1);
  background: none;
  text-align: left;
  font-family: var(--font-silk);
  font-size: var(--t-body);
  color: var(--silk);
  cursor: pointer;
}
.pt-mi:hover:not(:disabled), .pt-mi:focus-visible { background: var(--panel-hi); outline: none; }
.pt-mi:disabled { color: var(--silk-mute); cursor: default; }
.pt-mi.hidden { display: none; }
.pt-mi[aria-checked="true"]::before { content: "✓"; color: var(--phos-a); }
.pt-mi[aria-checked="false"]::before { content: ""; width: 0.8em; }
@media (pointer: coarse) { .pt-mi { min-height: 44px; } }
/* The edit bar: once something was edited, "N changes · KEEP AS NEW · pick
   the edit ☐ · undo to as opened" (the specimen's `#pt-editbar`). */
.pt-editbar {
  display: flex;
  align-items: center;
  gap: var(--s1);
  height: 32px;
  padding: 0 var(--s1) 0 var(--s3);
  border: 1px solid var(--phos-a-deep);
  border-radius: 999px;
  background: var(--panel-hi);
}
.pt-editbar[hidden] { display: none; }
.pt-ed-n { font-size: var(--t-value); color: var(--phos-a); margin-right: var(--s1); white-space: nowrap; }
.pt-ed-sep { color: var(--silk-mute); }
.pt-ed-btn {
  border: 0;
  background: none;
  padding: var(--s1) var(--s2);
  border-radius: 999px;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--silk);
  cursor: pointer;
  white-space: nowrap;
}
.pt-ed-btn:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--plate-hi); }
.pt-ed-btn:disabled,
.pt-ed-btn[aria-disabled="true"] { color: var(--silk-mute); cursor: default; }
.pt-ed-btn:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 1px; }
.pt-editbar .toggle { gap: var(--s1); padding: 0 var(--s1); white-space: nowrap; }
@media (pointer: coarse) { .pt-editbar { height: 40px; } .pt-ed-btn { min-height: 40px; } }
/* Narrow, "undo to as opened" keeps its glyph (named by its aria-label). */
.pt-ed-g { display: none; font-size: var(--t-body); }
@media (max-width: 1399px) {
  .pt-ed-l { display: none; }
  .pt-ed-g { display: inline; }
}

/* HOW TO READ THIS, over the well's foot. */
.pt-legend {
  position: absolute;
  left: var(--s4);
  right: var(--s4);
  bottom: var(--s4);
  z-index: 12;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s5);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: var(--panel-94);
}
.pt-legend[hidden] { display: none; }
.pt-leg-cap {
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--silk-dim);
}
.pt-leg { display: inline-flex; align-items: center; gap: var(--s2); font-family: var(--font-silk); font-size: var(--t-body); color: var(--silk-dim); }
.pt-leg svg { width: 44px; height: 22px; flex: none; fill: none; stroke-linecap: round; }
.pt-leg .lg-plate { fill: var(--plate-lo); stroke: var(--steel); stroke-width: 1.2; }
.pt-leg .lg-audio { stroke: var(--phos-a); stroke-width: 2; }
.pt-leg .lg-mod { stroke: var(--phos-a-80); stroke-width: 2; stroke-dasharray: 4 4; }
.pt-leg .lg-mark { fill: var(--well); stroke: var(--phos-a-90); stroke-width: 1; }
.pt-leg .lg-bars { stroke: var(--phos-a-90); stroke-width: 2; }
.pt-leg .lg-light { fill: var(--phos-a); stroke: none; }
.pt-leg .lg-guess { stroke: var(--phos-b); stroke-width: 1.4; stroke-dasharray: 4 3; }
.pt-leg .lg-lock { stroke: var(--phos-b); stroke-width: 1.6; }
.pt-leg .lg-lean { stroke: var(--led-red); stroke-width: 2; stroke-opacity: 0.7; }
.pt-leg .lg-without { stroke: var(--silk); stroke-width: 1.2; stroke-dasharray: 2 2; }
.pt-leg .lg-tick { stroke: var(--silk); stroke-width: 1.6; stroke-linejoin: round; }
.pt-leg .lg-jack { fill: var(--jack-bore); stroke: var(--phos-a); stroke-width: 1.6; }
.pt-leg .lg-face { stroke: var(--phos-a); stroke-width: 1.4; fill: var(--phos-a-15); }

/* The camera's corner, bottom left of the well (C2a). */
.pt-corner {
  position: absolute;
  left: var(--s3);
  bottom: var(--s3);
  z-index: 8;
  display: flex;
  align-items: center;
  gap: var(--s1);
}
.pt-play-slot { position: absolute; right: var(--s3); top: var(--s3); z-index: 8; }
/* Under 1600 px HOW TO READ THIS keeps its glyph only, and ADD MODULE its
   word without the printed key, so the name keeps the head's room. */
@media (max-width: 1599px) {
  #pt-how .pt-act-l, #pt-add kbd { display: none; }
}

/* ---- the well: the rack on its rounded canvas (C2a) ----
   The frame is the specimen's `.well`: a rounded, recessed canvas with a
   faint glow toward its middle. The viewport inside it loses its own edge. */
#view-patch .rack-frame {
  border: 1px solid var(--hairline);
  border-radius: var(--r3);
  overflow: hidden;
  background: radial-gradient(120% 90% at 50% 60%, var(--panel-lo), var(--bezel) 62%);
  box-shadow: inset 0 0 0 1px var(--black-60), inset 0 30px 60px -30px var(--black-75);
}
#view-patch .rack-scroll {
  border: 0;
  border-radius: inherit;
  background: none;
  box-shadow: none;
}
#view-patch .rack-dots { opacity: 0.6; }
/* Plates, to the specimen's `.pl`: a flat plate with a hairline edge, its
   name in silk caps; the panel's bevel, screws and pocket are not drawn. */
#rack-svg .mod-plate { fill: url(#plateGrad); stroke: var(--steel); stroke-width: 1; }
#rack-svg .mod-plate.modside { fill: var(--panel-lo); stroke: var(--steel); stroke-dasharray: 5 4; }
#rack-svg .plate-screw,
#rack-svg .plate-roll,
#rack-svg .plate-lit,
#rack-svg .plate-shade,
#rack-svg .plate-edge,
#rack-svg .plate-rule { display: none; }
/* ⋯ and the lock in their pocket on the plate's top edge: shown with them
   (on hover, selection or focus; always to a finger). */
#rack-svg .ctrl-well { fill: var(--panel-lo); stroke: var(--steel); stroke-width: 1; opacity: 0; transition: opacity var(--d-state) ease; }
#rack-svg .plate-hot .ctrl-well,
#rack-svg .rack-plates g.selected .ctrl-well,
#rack-svg .ctrl-well.locked { opacity: 1; }
@media (pointer: coarse) { #rack-svg .ctrl-well { opacity: 1; } }
#rack-svg .mod-title {
  font-size: var(--t-rack-value);
  font-weight: 600;
  letter-spacing: 0.18em;
  fill: var(--silk);
}
/* A setting, printed as its value where a knob would be ("-1" over "oct"),
   with a dotted rule under it that says it changes on a click; the one that
   names the module's kind sits in the head, right of the name. */
#rack-svg .enum-body { fill: transparent; stroke: none; }
#rack-svg .enum-body:hover { fill: var(--white-04); }
#rack-svg .enum-text { fill: var(--silk); }
#rack-svg .enum-text.head { text-anchor: end; fill: var(--silk-dim); }
#rack-svg .enum-line { stroke: var(--silk-mute); stroke-width: 1; stroke-dasharray: 1.5 2; pointer-events: none; }
#rack-svg .knob-value { fill: var(--silk); }
/* A modulation socket nothing is plugged into is there, quietly, until the
   plate is pointed at or a cable is out looking for one. */
#rack-svg .mod-tab:not(.filled),
#rack-svg .mod-tab-name:not(.filled) { opacity: 0.45; }
#rack-svg .plate-hot .mod-tab,
#rack-svg .plate-hot .mod-tab-name { opacity: 1; }
/* Compact: the name and the setting larger, to read at the zoom that chose
   compact (the knobs are three to read, not to grab). */
#rack-svg.lod-compact .mod-title { font-size: calc(var(--t-rack-title) * 1.3); }
#rack-svg.lod-compact .enum-text.head { font-size: calc(var(--t-rack-value) * 1.3); }
#rack-svg.lod-compact.silk-floor .mod-title,
#rack-svg.lod-compact.silk-floor .enum-text.head { visibility: visible; }
/* …and the same at full detail: a name that would print under the type
   floor (`--t-label`, the `name` tier) prints larger, so it never reads
   smaller than the rest of the app's text (#519; the fit stops where this
   size is at the floor, main.js `fitRead`, whose NAME_LARGER is this 1.3);
   and where the readouts are under the floor (the knobs are still controls
   there, their values and labels left off), the setting does too: each
   module still says what it is and how it is set. */
#rack-svg.silk-floor[data-illegible~="name"] .mod-title { font-size: calc(var(--t-rack-title) * 1.3); visibility: visible; }
#rack-svg.silk-floor[data-illegible~="value"] .enum-text.head { font-size: calc(var(--t-rack-value) * 1.3); visibility: visible; }
/* The amp's envelope, drawn from its four knobs as set. */
#rack-svg .env-fig { fill: var(--phos-a-12); stroke: var(--phos-a-60); stroke-width: 1.2; stroke-linejoin: round; pointer-events: none; }
#rack-svg .mod-title.modside { fill: var(--silk-dim); }
#rack-svg .mod-title.empty { fill: var(--silk-mute); }
/* A knob, as the specimen draws one: its travel and its value as arcs, and a
   pointer; no body, no tick ring. */
#rack-svg .knob-ticks { display: none; }
#rack-svg .knob-body { fill: var(--panel-lo); stroke: none; }
#rack-svg .knob-track { stroke: var(--hairline); stroke-width: 2.4; }
#rack-svg .knob-ind { stroke: var(--silk); stroke-width: 1.6; }
#rack-svg .knob-ind.modside { stroke: var(--silk); }
#rack-svg .knob-label { font-family: var(--font-mono); letter-spacing: 0; text-transform: none; fill: var(--silk-mute); }
/* OUT: the amp's lead to the jack, its name, and the face past it. */
.out-lead { fill: none; stroke: var(--phos-a-34); stroke-width: 2; pointer-events: none; }
.out-jack { fill: var(--bezel); stroke: var(--phos-a); stroke-width: 2; pointer-events: none; }
.out-dot { fill: var(--phos-a); pointer-events: none; }
.out-name {
  font-family: var(--font-silk);
  font-size: var(--t-rack-label);
  font-weight: 600;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  fill: var(--silk-dim);
  pointer-events: none;
}
.out-slot { position: absolute; z-index: 5; display: block; }
.out-slot.hidden { display: none; }
.out-play {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  border-radius: var(--r2);
  background: none;
  cursor: pointer;
}
.out-play:disabled { cursor: default; opacity: 0.55; }
/* The face of the patch without the selected module, measured: a dashed
   outline over the face at OUT, the same size and floor. Not a control. */
.out-without { position: absolute; inset: 0; pointer-events: none; }
.out-without[hidden] { display: none; }
.out-without .face { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.out-play > .face-slot { position: absolute; inset: 0; }
.face-slot.face-out .face { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.out-play:hover:not(:disabled) > .face-slot { filter: brightness(1.15); }
.out-play:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
/* Sounding: the face lit, as the menu bar's ▶ is. Waiting for an edit: ▶'s
   dotted amber ring. */
.out-play.playing { box-shadow: 0 0 0 1px var(--phos-a-45), 0 0 24px var(--phos-a-18); }
.out-play.pending { outline: 1px dotted var(--phos-b); outline-offset: 2px; }
/* A modulation cable's depth and rate, as set (`modCableText`). */
.mod-cable-label {
  font-family: var(--font-mono);
  font-size: var(--t-rack-value);
  fill: var(--silk-dim);
  pointer-events: none;
}
#rack-svg.silk-floor[data-illegible~="value"] .mod-cable-label { visibility: hidden; }
/* A plate in the hand moves its cables every frame; the words beside them
   are placed at the build, so they step aside until the plate is put down. */
#rack-scroll.moving-plate .mod-cable-label { visibility: hidden; }
/* The well's edges: modules past the view, and a press to go to them. */
.pt-edge {
  position: absolute;
  top: var(--s7);
  bottom: var(--s7);
  z-index: 6;
  width: 44px;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 0 var(--s2);
  border: 0;
  color: var(--silk-dim);
  font-family: var(--font-mono);
  font-size: var(--t-value);
  cursor: pointer;
}
.pt-edge[hidden] { display: none; }
.pt-edge.l { left: 0; justify-content: flex-start; background: linear-gradient(90deg, var(--bezel-72), transparent); }
.pt-edge.r { right: 0; justify-content: flex-end; background: linear-gradient(270deg, var(--bezel-72), transparent); }
.pt-edge b { font-weight: 500; color: var(--phos-a-dim); }
.pt-edge:hover, .pt-edge:focus-visible { color: var(--silk); }
.pt-edge:focus-visible { outline: 2px solid var(--phos-a); outline-offset: -2px; }

/* ---- the well's foot: the camera's corner, then the readout (C2a) ---- */
.pt-foot {
  position: absolute;
  left: var(--s3);
  right: var(--s3);
  bottom: var(--s3);
  z-index: 9;
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: 32px;
  pointer-events: none;
}
.pt-foot > * { pointer-events: auto; }
.pt-foot .pt-corner { position: static; flex: none; }
/* The readout: one line, the module's name in silk caps and what it does. */
.pt-read {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  min-height: 28px;
}
.pt-read.rest { pointer-events: none; }
.pr-line { display: flex; align-items: baseline; gap: var(--s3); min-width: 0; white-space: nowrap; overflow: hidden; }
.pr-name {
  flex: none;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--phos-a);
}
.pr-sub { flex: none; font-size: var(--t-value); color: var(--silk-mute); }
.pr-says {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--font-silk);
  font-size: var(--t-body);
  color: var(--silk);
}
.pr-lock { flex: none; font-size: var(--t-value); color: var(--phos-b); }
/* A measurement about the selected module: what the patch is without it,
   when there is no face to draw (silent, or failing the vet). */
.pr-without { flex: none; font-size: var(--t-value); color: var(--silk-dim); }
/* Which of PERFORM's controls turn it, as measured: the controls' names are
   silk labels, so caps; kept whole, the description gives way. */
.pr-wired { flex: none; font-family: var(--font-silk); font-size: var(--t-label); letter-spacing: 0.06em; color: var(--silk); }
/* Pointed at in the catalog: the longer description opens over the well's
   bottom edge, above the line, and never resizes the well. */
.pr-more { display: none; }
.pt-read.open .pr-more {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% + var(--s2));
  display: grid;
  grid-template-columns: minmax(150px, 0.8fr) minmax(0, 1.2fr) minmax(0, 1.2fr);
  gap: var(--s4);
  align-items: start;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: var(--panel-94);
  box-shadow: var(--shadow-pop);
}
/* Under the model view, what the model makes of the selected module (its
   family's part of the utility, the lean): over the well's bottom edge,
   above the line, as the catalog's description is. */
.pr-model {
  position: absolute;
  left: 0;
  bottom: calc(100% + var(--s2));
  max-width: min(100%, 560px);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: var(--panel-94);
  box-shadow: var(--shadow-pop);
  font-size: var(--t-value);
  line-height: 1.5;
  color: var(--silk-dim);
}
.pr-model b.up { color: var(--phos-b); font-weight: 400; }
.pr-model b.down { color: var(--silk-dim); font-weight: 400; }
/* Which way your taste leans on a module is the model view's (⌥, or MODEL):
   at rest the longer description and the keyboard's card leave it out, and
   the description takes two columns. */
body:not(.model-view) .sd-model,
body:not(.model-view) .sp-model { display: none; }
body:not(.model-view) .pt-read.open .pr-more { grid-template-columns: minmax(150px, 0.8fr) minmax(0, 2fr); }
/* Where the line is narrow (a narrow window, or beside the open catalog) the
   three columns would be a word wide each: they stack, and the card scrolls
   rather than rising past the well's top. */
.pt-read { container-type: inline-size; }
@container (max-width: 560px) {
  .pt-read.open .pr-more,
  body:not(.model-view) .pt-read.open .pr-more {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s2);
    max-height: 300px;
    overflow-y: auto;
  }
}
/* Something in hand, or a socket waiting for a module: the armed line. */
.pt-read.armed {
  padding: var(--s1) var(--s3);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: var(--panel-94);
}
.pt-read.armed .pv-scope { height: 30px; }
/* The scope stands clear of the foot. */
#view-patch .scope-shell.corner-bl,
#view-patch .scope-shell.corner-br { bottom: calc(var(--s3) + 40px); }

/* ---- the selection (C2a) ----
   The selected plate shows its ⋯ (the structure menu) and its lock; any
   other plate shows them only while the pointer is on it. A locked plate's
   edge is solid amber. */
#rack-svg .mod-menu-btn, #rack-svg .mod-lock { opacity: 0; transition: opacity var(--d-state) ease; }
#rack-svg .plate-hot .mod-menu-btn,
#rack-svg .plate-hot .mod-lock,
#rack-svg g.selected .mod-menu-btn,
#rack-svg g.selected .mod-lock,
#rack-svg .mod-lock.on,
#rack-svg .mod-group:focus-within .mod-menu-btn,
#rack-svg .mod-group:focus-within .mod-lock { opacity: 1; }
#rack-svg .mod-plate.locked { stroke: var(--phos-b); stroke-width: 1.6; stroke-dasharray: none; }
#rack-svg .rack-plates g.selected .mod-plate { stroke: var(--phos-a-dim); stroke-width: 1.4; }
#rack-svg .rack-plates g.selected .mod-plate.locked { stroke: var(--phos-b); }
#rack-svg .rack-plates g.selected .mod-title { fill: var(--phos-a); }
/* A finger has no hover: ⋯ and the lock show on every plate. */
@media (pointer: coarse) { #rack-svg .mod-menu-btn, #rack-svg .mod-lock { opacity: 1; } }
/* The well's top line while something is in hand: over the guess's line,
   which steps aside for it. */
.pick-chip { gap: var(--s2); padding: var(--s1) var(--s1) var(--s1) var(--s3); }
.pick-chip.hidden { display: none; }
.pick-armed { display: inline-flex; align-items: center; gap: var(--s2); min-width: 0; }
.pick-armed:empty { display: none; }
.pick-armed b { font-family: var(--font-silk); font-size: var(--t-body); color: var(--phos-a); letter-spacing: 0.02em; text-transform: none; }
.pick-chip .pick-chip-text { color: var(--silk-dim); }
.pick-chip .pick-chip-text:empty { display: none; }
.guess-read.held { visibility: hidden; }
@media (pointer: coarse) { .pick-chip-x { width: 40px; height: 40px; } }
/* With the catalog open, the well's top line starts past it. */
.rack-frame.cat-open .pick-chip,
.rack-frame.cat-open .guess-read { left: calc(248px + var(--s3) + var(--s3)); }
.pick-armed { overflow: hidden; flex-wrap: wrap; row-gap: 2px; }
.pick-armed > * { white-space: nowrap; }
/* The line stands over the well's top: a pointer goes through it to the
   plates and sockets under it, except on what it can press (▶, ✕) or read
   on hover (the price). */
#pick-chip { pointer-events: none; }
#pick-chip button, #pick-chip .sd-price, #pick-chip .pv-strip { pointer-events: auto; }
.pick-armed .sd-hint { margin-left: 0; }
/* Beside the open catalog the line has less room than the well: its width
   is what is left, and the price and ▶ go to a second line rather than past
   the well's edge. */
/* Beside the open catalog the line has less room than the well: its width
   is what is left, and it stays one line (C2b). The module, IN HAND, the
   price's figure and ▶ stay whole; what gives way, cut with an ellipsis, is
   the reasons: what happens at the socket, how many are lit, the preview's
   label, and a price that is a reason rather than a figure ("no price yet:
   it needs a few picks"). */
.rack-frame.cat-open .pick-chip { max-width: calc(100% - 248px - var(--s3) - var(--s3) - var(--s4)); }
.pick-chip, .rack-frame.cat-open .pick-chip { white-space: nowrap; flex-wrap: nowrap; }
/* Its parts are the line's own items, so each can give way by itself. */
.pick-chip .pick-armed { display: contents; }
.pick-chip .pick-armed > *, .pick-chip .pv > * { flex: none; }
/* The preview's ▶ comes first in it and never goes: what gives is its trace
   and label, after it. */
.pick-chip .pick-armed .pv { flex: 0 1 auto; min-width: 22px; overflow: hidden; }
.pick-chip .pick-armed .pv-scope { width: 96px; height: 26px; }
.pick-chip .pick-armed .pv-label,
.pick-chip .pick-armed .sd-hint,
.pick-chip .pick-armed .sd-price:has(.pr-mute),
.pick-chip .pick-chip-text {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pick-chip .pick-chip-text { flex-shrink: 2; }
.sd-price .pr-short { display: none; }
.rack-frame.cat-open .pick-chip .sd-price .pr-long { display: none; }
.rack-frame.cat-open .pick-chip .sd-price .pr-short { display: inline; }
/* Narrower still (the catalog open in a 1000 px window): the preview's trace
   and the count of lit sockets (the canvas lights them) step out, so where it
   goes and the price keep their words. ▶ stays. */
@media (max-width: 1180px) {
  .rack-frame.cat-open .pick-chip { gap: var(--s1); }
  .rack-frame.cat-open .pick-chip .pv-scope,
  .rack-frame.cat-open .pick-chip .pick-armed .sd-hint,
  .rack-frame.cat-open .pick-chip .pick-armed .sd-verb { display: none; }
}

/* ---- the camera's corner (C2a): fit · − · + · map · layout ▾ ---- */
.pt-corner { gap: 2px; padding: 2px; border-radius: var(--r2); background: var(--bezel-72); }
.pt-cam {
  min-width: 28px;
  height: 28px;
  padding: 0 var(--s2);
  border: 1px solid transparent;
  border-radius: var(--r1);
  background: none;
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--silk-dim);
  cursor: pointer;
  white-space: nowrap;
}
.pt-cam:hover, .pt-cam:focus-visible { color: var(--silk); border-color: var(--hairline); background: var(--panel-hi); }
.pt-cam:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 1px; }
.pt-cam[aria-pressed="true"], .pt-cam[aria-expanded="true"] { color: var(--phos-a); }
#pt-zoom-out, #pt-zoom-in { font-family: var(--font-mono); font-size: var(--t-value); letter-spacing: 0; }
@media (pointer: coarse) { .pt-cam { min-width: 40px; height: 40px; } }
.pt-layout { position: relative; display: inline-flex; }
/* The corner's menu opens upward, from its left. */
.pt-laymenu { top: auto; bottom: calc(100% + var(--s1)); left: 0; right: auto; min-width: 240px; }
.pt-laymenu .tt { display: grid; }
.pt-laymenu .tt.held { visibility: visible; }
.pt-msep { display: block; height: 1px; margin: var(--s1) var(--s2); background: var(--hairline); }
/* The minimap stands over the corner. */
#view-patch .rack-map { bottom: calc(var(--s3) + 40px); left: var(--s3); }
.cknob, .cknob * { pointer-events: none; }
/* The fit hint is a note in the well's corner, over the camera. */
#view-patch .fit-hint { left: var(--s3); top: auto; bottom: calc(var(--s3) + 44px); transform: none; }

/* ---- set aside (C2a): the chip at the foot, the shelf over it ---- */
.pt-aside b { font-weight: 600; color: var(--phos-a); margin-left: var(--s1); }
.pt-aside[hidden] { display: none; }
.pt-shelf {
  position: absolute;
  left: var(--s3);
  right: var(--s3);
  bottom: calc(var(--s3) + 40px);
  z-index: 12;
  min-height: 40px;
  background: var(--panel-94);
  box-shadow: var(--shadow-pop);
}
.nb-aside-list { display: grid; gap: var(--s1); padding: 0 var(--s2) var(--s2); }
.nb-aside-list .tray-item { max-width: none; }
/* A finger needs more than the jack's 13 px to start a cable. */
@media (pointer: coarse) {
  .tray-item .t-jack { position: relative; }
  .tray-item .t-jack::before { content: ""; position: absolute; inset: -14px; }
  .tray-item .t-x { min-width: 40px; min-height: 40px; }
}

/* ---- TEACH (C2a): a chip at the foot, the strip over it ---- */
.pt-teach { margin-left: auto; color: var(--phos-b); }
.pt-teach[hidden] { display: none; }
#view-patch .play-duel {
  position: absolute;
  left: var(--s3);
  right: var(--s3);
  bottom: calc(var(--s3) + 40px);
  z-index: 12;
  margin: 0;
}
.pd-fold { margin-left: var(--s1); }
@media (pointer: coarse) { .pd-fold { min-width: 40px; min-height: 40px; } }
/* The touch sheet's figure (C2a): the bench's face beside its settings. */
.msheet .ms-body { display: grid; grid-template-columns: minmax(0, 1fr) 72px; gap: var(--s3); align-items: start; }
.ms-fig { margin: 0; position: sticky; top: 0; display: grid; justify-items: center; gap: var(--s1); }
.ms-face { position: relative; display: block; width: 72px; height: 120px; }
.ms-face .face { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.ms-cap { font-family: var(--font-mono); font-size: var(--t-label); color: var(--silk-mute); }
/* One input feeding several AUDIO IN modules (`ain-fan`). */
.ain-fan, .ain-fan * { pointer-events: none; }
.ain-fan-line { fill: none; stroke: var(--phos-a-60); stroke-width: 2; stroke-dasharray: 1.5 4; stroke-linecap: round; }
.ain-fan-src { fill: var(--bezel); stroke: var(--phos-a); stroke-width: 2; }
.ain-fan-dot { fill: var(--phos-a); }
.ain-fan-name { font-family: var(--font-mono); font-size: var(--t-rack-value); fill: var(--phos-a-dim); }
#rack-svg.silk-floor[data-illegible~="value"] .ain-fan-name { visibility: hidden; }
/* The sheet's lane buttons: the input line, MONITOR, NEW CLIP, ALLOW INPUT;
   RECORD. A finger's size. */
.ms-lane { display: flex; flex-wrap: wrap; gap: var(--s1); }
.ms-lane:empty { display: none; }
.ms-lane-btn {
  flex: 1 1 auto;
  min-width: 96px;
  min-height: 44px;
  padding: 0 var(--s3);
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: var(--panel-hi);
  font-family: var(--font-silk);
  font-size: var(--t-label);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--silk-dim);
}
.ms-lane-btn.on { color: var(--phos-a); border-color: var(--phos-a-deep); background: var(--phos-a-06); }
/* Where a knob was as the sound was opened, once it has been turned: a pale
   pointer across its rim (`paintWas`). */
#rack-svg .knob-was { stroke: var(--silk-mute); stroke-width: 1.4; stroke-linecap: round; pointer-events: none; }
/* A bred sound as it was bred (`paintLineage`): the same pale pointer at the
   seed's value on each knob the generation changed, and a silk tick on the
   top edge of each module it changed. */
#rack-svg .knob-seed { stroke: var(--silk-mute); stroke-width: 1.4; stroke-linecap: round; pointer-events: none; }
#rack-svg .lineage-tick { fill: none; stroke: var(--silk); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
#view-patch .pt-sub .pt-from { color: var(--silk); }
.pt-leg .lg-arc { stroke: var(--phos-a); stroke-width: 2; }
.pt-leg .lg-was { stroke: var(--silk-mute); stroke-width: 1.6; }
/* Focused from the keyboard while it waits: the focus ring stays where it is,
   and the dotted amber cue moves onto the face's own edge, as ▶'s did. */
.out-play.pending:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; border: 1px dotted var(--phos-b); }

/* =====================================================================
   A narrow window, a tablet and a phone (#134; Plan-008 §7, Plan-005 task
   8), to the prototype's phone (`docs/notes/vision-2026-09/prototype/
   style.css`, "v2: touch, designed rather than squeezed" and the shell
   audit's four parts). Three shapes, chosen by the stylesheet alone, with
   shell.js (`sheets`) only opening and closing them (`body.bank-open`,
   `body.keys-open`):
   - 999 px and under: the bank leaves its column and is a sheet from the
     left, opened by BANK at the start of the menu bar;
   - a phone held upright (700 px and under, taller than 500): the bank is
     a sheet from the bottom, the cross is the bar along the bottom (the
     levels in to out, EVOLVE beside them, then KEYS), and the keys are a
     drawer over that bar;
   - a short screen (a phone on its side, 500 px and under): the cross
     stays at the right edge with KEYS under it, and the keys are a drawer.
   Above 999 px nothing here applies, but for these four hidden things. */
.bank-btn, .sheet-grab, .rail-keys, .rail .rl, .sheet-scrim { display: none; }

.bank-btn {
  position: relative;
  flex: none;
  width: 40px;
  height: 40px;
  place-items: center;
  padding: 0;
  margin-left: calc(-1 * var(--s2));
  border: 1px solid var(--hairline);
  border-radius: var(--r2);
  background: none;
  color: var(--silk-dim);
  cursor: pointer;
}
.bank-btn svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.bank-btn:hover, .bank-btn[aria-expanded="true"] { color: var(--silk); border-color: var(--plate-edge); }
.bank-btn:focus-visible { outline: 2px solid var(--phos-a); outline-offset: 2px; }
.sheet-scrim { position: fixed; inset: 0; z-index: 41; background: var(--scrim-82); }

@media (max-width: 999px) {
  .app {
    grid-template-areas: "bar" "stage" "keys";
    grid-template-columns: minmax(0, 1fr);
    /* The height a phone's browser shows, not the one under its toolbar
       (100vh there runs past the screen, and the bar along the bottom with
       it); and clear of a notch on its side. */
    height: 100dvh;
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
  }
  .bank-btn { display: grid; }
  /* The bank, a sheet from the left over the stage: above a level's lifted
     controls (35), under the dialogs (55+), as the ladder at `.toasts`
     places popovers. Hidden while away, so its rows take no tab stop. */
  .bank {
    position: fixed;
    top: var(--menubar-h);
    bottom: 0;
    left: 0;
    z-index: 42;
    width: min(320px, 86vw);
    flex: none;
    border-right: 1px solid var(--plate-edge);
    box-shadow: var(--shadow-pop);
    transform: translateX(-100%);
    visibility: hidden;
    transition: transform var(--d-move) var(--e-settle), visibility var(--d-move);
    /* Clear of a notch on a phone on its side. */
    padding-left: env(safe-area-inset-left, 0px);
  }
  /* Opening, it is visible at once (only the slide is a transition), so the
     focus BANK sends into it from the keyboard lands; closing, it stays
     visible until it has slid away. */
  body.bank-open .bank { transform: none; visibility: visible; transition: transform var(--d-move) var(--e-settle); }
  body.bank-open .sheet-scrim { display: block; }
  /* A word about your sounds, said while the sheet is up, stands over it. */
  body.bank-open .toasts { z-index: 43; }
}
/* On its side a phone has width to spare, and a row's swiped actions have
   need of it: with 320 px, four buttons of a finger's size left a name 25 px
   (#534). */
@media (max-height: 500px) and (max-width: 999px) {
  .bank { width: min(420px, 86vw); }
}

/* ---- a phone held upright ---- */
@media (max-width: 700px) and (min-height: 501px) {
  :root {
    /* The bar along the bottom, and a phone's home indicator under it. */
    --nav-h: calc(58px + env(safe-area-inset-bottom, 0px));
    --menubar-h: 52px;
  }
  .app {
    grid-template-areas: "bar" "stage" "keys" "nav";
    grid-template-rows: var(--menubar-h) minmax(0, 1fr) auto var(--nav-h);
  }
  .stage { --rail-w: 0px; }
  .menubar { gap: var(--s2); padding: 0 var(--s3); }
  .bank-btn { margin-left: 0; }
  .where { display: none; }
  .cmdk-btn { padding: 0 var(--s2); }

  /* The bank: a sheet from the bottom, its handle at the top. */
  .bank {
    top: auto;
    right: 0;
    width: auto;
    height: 78vh;
    border: 1px solid var(--plate-edge);
    border-bottom: 0;
    border-radius: var(--r3) var(--r3) 0 0;
    transform: translateY(100%);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .rail { padding-left: env(safe-area-inset-left, 0px); padding-right: env(safe-area-inset-right, 0px); }
  .sheet-grab {
    display: block;
    position: relative;
    flex: none;
    width: 100%;
    height: 44px;
    padding: 0;
    border: 0;
    background: none;
    cursor: grab;
    touch-action: none;
  }
  .sheet-grab::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 14px;
    width: 40px;
    height: 4px;
    transform: translateX(-50%);
    border-radius: 2px;
    background: var(--plate-edge);
  }
  .bank-head { padding-top: 0; }

  /* The cross is the bar along the bottom: PERFORM (the sound under your
     hands) first, then PATCH, TASTE, LEARNING, then EVOLVE
     beside them, then KEYS. The level you're at is lit, with a line
     along its top edge. */
  .rail {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    top: auto;
    transform: none;
    z-index: 38;
    height: var(--nav-h);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr)) minmax(0, 1.1fr) minmax(0, 0.9fr);
    justify-items: stretch;
    align-items: stretch;
    background: var(--panel);
    border-top: 1px solid var(--hairline);
  }
  .rail-line, .rail-branch, .rail-hint, .rail-puck { display: none; }
  .rail-mid { display: contents; }
  .rail-stop, .rail-stop.aside, .rail-keys {
    width: auto;
    height: auto;
    min-height: 44px;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--s1);
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    color: var(--silk-dim);
    cursor: pointer;
  }
  .rail-stop svg, .rail-stop.aside svg, .rail-keys svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
  .rail .rl {
    display: block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    font-family: var(--font-silk);
    font-size: var(--t-label);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
  }
  .rail-stop[aria-current="location"],
  .rail.traveling .rail-stop[aria-current="location"] {
    color: var(--phos-a);
    background: none;
    box-shadow: inset 0 2px 0 var(--phos-a);
  }
  .rail-stop:focus-visible, .rail-keys:focus-visible { outline: 2px solid var(--phos-a); outline-offset: -4px; }
  .rail-stop[data-level="perform"] { order: 1; }
  .rail-stop[data-level="patch"] { order: 2; }
  .rail-stop[data-level="taste"] { order: 3; }
  .rail-stop[data-level="learning"] { order: 4; }
  .rail-stop[data-level="evolve"] { order: 5; border-left: 1px solid var(--hairline); }
  .rail-keys { order: 6; border-left: 1px solid var(--hairline); }
  .rail-keys[aria-expanded="true"] { color: var(--phos-a); }

  /* The keys: a drawer that opens over the bar, lifting the stage rather
     than covering it. The octave and the cluster on one line, the keybed
     the width of the screen under them. */
  .keybar { display: none; }
  body.keys-open .keybar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2) var(--s3);
    padding: var(--s2) var(--s3);
  }
  .keys-side { width: auto; flex-direction: row; align-items: center; }
  .keys-side .panel-label, .oct-word { display: none; }
  .keys-cluster { margin-left: auto; gap: var(--s2); }
  .keys-cluster .vol .pc-cap { display: none; }
  #vol { width: 64px; }
  .piano { order: 3; flex: 1 0 100%; height: 120px; }
  .app.bigkeys .piano { height: clamp(160px, 32vh, 260px); }
  .keys-pop { right: 0; max-width: calc(100vw - 2 * var(--s3)); white-space: normal; }
  .kp-row { flex-wrap: wrap; }

  .guide { left: var(--s3); right: var(--s3); bottom: var(--s3); max-width: none; }
  .model-tag { max-width: calc(100vw - 2 * var(--s4)); }
}

/* ---- a short screen: a phone on its side ---- */
@media (max-height: 500px) and (max-width: 999px) {
  .app { grid-template-rows: var(--menubar-h) minmax(0, 1fr) auto; }
  .where { display: none; }
  .rail-keys {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    margin-top: var(--s2);
    padding: 0;
    border: 1px solid var(--hairline);
    border-radius: 50%;
    background: var(--panel);
    color: var(--silk-mute);
    cursor: pointer;
  }
  .rail-keys svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
  .rail-keys[aria-expanded="true"] { color: var(--phos-a); border-color: var(--phos-a-deep); }
  .rail-hint { display: none; }
  .rail-line { height: 10px; }
  .keybar { display: none; }
  body.keys-open .keybar { display: flex; }
}

/* A finger: what is pressed is the size of one, and a field is read at a
   size iOS does not zoom into. */
@media (pointer: coarse) {
  .bank-btn, .cmdk-btn { min-width: 44px; height: 44px; }
  .bank-find input, #cmdk-input { font-size: 16px; } /* token-exempt: under 16 px iOS Safari zooms the page into a focused field */
  .menubar kbd { display: none; }
}

/* ---- each level on a phone held upright (#134) ----
   One column, read from the top, and the level scrolls where it runs past
   the screen; nothing is cut and nothing scrolls sideways. */
@media (max-width: 700px) and (min-height: 501px) {
  /* PERFORM: the sound (its name and its well), then what you turn, the
     hood and the pads, clear of the guide pill at the bottom. */
  .view.perform { padding: var(--s3) var(--s3) calc(var(--s7) + var(--s5)); }
  .pf-grid { flex: none; grid-template-columns: minmax(0, 1fr); gap: var(--s5); }
  .pf-left { grid-template-rows: auto auto; }
  .pf-well { height: min(46vh, 380px); min-height: 240px; }
  .pf-right { justify-content: flex-start; }

  /* EVOLVE: the words, then the small map the width of the screen, then
     the pair side by side, each card's PLAY and PICK under its name. */
  #view-evolve { overflow-y: auto; padding: var(--s3) var(--s3) var(--s5); gap: var(--s3); }
  .ev-head { flex-direction: column; align-items: stretch; gap: var(--s3); }
  .ev-map { width: 100%; height: 96px; }
  .duel-row { flex: none; gap: var(--s2); }
  .duel-card { grid-template-rows: auto auto; gap: var(--s2); }
  .duel-well { height: min(38vh, 320px); }
  .duel-well > .face-slot { inset: var(--s6) var(--s2) var(--s2); }
  .duel-corner { top: var(--s2); right: var(--s2); left: var(--s2); justify-content: flex-end; gap: var(--s1); }
  .duel-controls { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--s1); }
  .duel-controls .hw-btn { min-width: 0; padding-left: var(--s1); padding-right: var(--s1); }
  .ev-foot { flex-wrap: wrap; }
  .ev-map { height: clamp(64px, 11vh, 96px); }

  /* PATCH: the head stacked (the cap, the name, what it is made of, then
     the acts), and the well the rest of the screen: a tapped module opens
     its sheet (patch.js), and two fingers zoom the rack. */
  #view-patch { padding: var(--s3) var(--s3) var(--s2); gap: var(--s2); overflow-y: auto; }
  /* The well keeps room for the rack and its foot; past that the level
     scrolls (a shorter screen, larger text) rather than cutting the foot. */
  #view-patch .play-body { min-height: 200px; }
  body.keys-open #view-patch .play-body { min-height: 120px; }
  .pt-head {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "cap next" "title title" "sub sub" "acts acts" "edit edit";
  }
  #view-patch .pt-title { font-size: var(--t-title); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .pt-sub { min-height: 0; }
  .pt-headacts { justify-content: flex-start; flex-wrap: wrap; }
  .pt-editbar { justify-self: start; }
  /* The well's foot: the readout on a line of its own over the camera, and
     TEACH after the camera, on the next line if it must. */
  .pt-foot { flex-wrap: wrap; row-gap: var(--s1); }
  .pt-foot .pt-read { order: -1; flex: 1 0 100%; }
  .pt-foot .pt-read:empty { display: none; }

  /* The catalog: the well's width. With a module in hand it folds to its
     head and its status line at the well's foot, so the lit sockets above
     can be tapped (a socket chosen first keeps the list: a module is still
     to be chosen). */
  .nodebank { left: var(--s2); right: var(--s2); top: var(--s2); bottom: var(--s2); width: auto; }
  .nodebank:has(.nb-item.armed) { top: auto; }
  .nodebank:has(.nb-item.armed) :is(.nb-search, .nb-aside, .nb-inpatch, #nb-groups, .nb-legend, .nb-empty, .nb-none) { display: none; }


  /* TASTE and LEARNING: the headline at a title's size, so the map and the
     room start on the first screen. */
  #view-taste .lv-title, #view-learning .lv-title { font-size: var(--t-title); }
}

/* ---- each level on a short screen (a phone on its side, #134) ----
   Every level scrolls where it runs past the screen, its headline at a
   title's size, and EVOLVE's pair keeps PLAY and PICK in reach. */
@media (max-height: 500px) and (max-width: 999px) {
  :root { --menubar-h: 46px; }
  .view.perform, #view-patch, #view-evolve { padding: var(--s3); }
  #view-evolve, #view-taste { overflow-y: auto; }
  #view-evolve .ev-title, #view-patch .pt-title, #view-taste .lv-title, #view-learning .lv-title { font-size: var(--t-title); }
  .ev-map { height: 96px; }
  .duel-row { flex: none; }
  .duel-card { grid-template-rows: auto auto; }
  .duel-well { height: min(62vh, 240px); }
  .md { grid-template-rows: auto auto; }
}

/* The narrowest phones: the wordmark gives the sound in hand its room, and
   the bar's names their width. */
@media (max-width: 400px) {
  .brand .word { display: none; }
}
@media (max-width: 380px) and (min-height: 501px) {
  /* Each column at least its name's width, the rest shared: LEARNING is the
     longest, and in six equal columns at 312 px it was cut. */
  .rail { grid-template-columns: repeat(6, minmax(min-content, 1fr)); }
  .rail .rl { letter-spacing: 0; }
}

/* A phone has no keys to print: the hints go, and the keybed keeps only its
   C's octave. A touch tablet keeps them, since it may have a keyboard. */
@media (hover: none) and (pointer: coarse) and (max-width: 700px), (hover: none) and (pointer: coarse) and (max-height: 500px) {
  kbd { display: none !important; }
  /* The octave's keys, named Z and X for a keyboard there is none of: an
     arrow each instead, their names their aria-labels. */
  .kcap#oct-down, .kcap#oct-up { font-size: 0; }
  .kcap#oct-down::before { content: "‹"; font-size: var(--t-body); }
  .kcap#oct-up::before { content: "›"; font-size: var(--t-body); }

  /* …but for a key named in a sentence (the walkthroughs': "esc to put it
     down"), which would read with a hole in it. */
  .tour-body kbd { display: inline !important; }
  .pkey .hint, .bkey .hint { color: transparent; }
  .pkey .hint b { color: var(--pkey-hint); }
  .pkey.down .hint b { color: var(--pkey-down-hint); }
}

/* Touch gestures (#134): a long press on a card, a bank row or
   TASTE's map is the model view held (shell.js), so it neither selects text
   nor opens the page's callout; a card swiped up to pick (main.js) rides
   above its neighbour while it follows the finger. */
@media (pointer: coarse) {
  #view-evolve section[aria-label^="Sound "], .bank-item, .preset-item, #taste-crt {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }
  .bank-item input { -webkit-user-select: text; user-select: text; }
  .duel-card.swiping { z-index: 2; will-change: transform; }
  /* A finger moving up on a card is the card's from the start, so the swipe
     wins even where the column scrolls; down and sideways stay the page's.
     `pan-up` names the scroll a finger moving down makes (content scrolls
     up): measured in Chromium at 360 x 640, `pan-down` let an upward finger
     scroll the column and stopped a downward one. */
  #view-evolve section[aria-label^="Sound "] { touch-action: pan-x pan-up; }
}

/* ---- a phone and a tablet, polished (#134) ----
   Each level is one screen, as an app's view is: what it is for (the
   knobs, the pair and PICK, the rack, the map) fits without the page
   scrolling, and where there is more than fits it scrolls inside its own
   panel, never the level. And on a touch screen each thing pressed is a
   finger's size, 44 px (a bank row's acts and stars are as tall, and share
   the row's width, below), and a label stays on one line. */
@media (pointer: coarse) {
  .inhand-play, .model-btn, .util-btn, .hw-btn, .inspect-btn, .pf-disclose, .md-disclose,
  .ev-lineage-btn, .ts-tog, .ts-tplay, .btab, .bank-tour-btn, .midi-ind,
  .pt-act, .pt-cam, .pt-evolve-more, .pt-evolve #rack-evolve, .guide .x,
  .nb-collapse, .nb-q, .nb-chip, .nb-fold, .nb-item, .perf-sel, .pf-how-tab, .wi-play, .brand {
    min-height: 44px;
  }
  .inhand-play, .model-btn, .util-btn, .ts-tplay, .bank-tour-btn, .pt-act, .pt-cam, .pt-evolve-more, .guide .x,
  .nb-collapse, .nb-chip, .pf-how-tab, .wi-play, .brand { min-width: 44px; }
  .nb-chip, .pf-how-tab { display: inline-flex; align-items: center; justify-content: center; }
  .nb-head .panel-label { white-space: nowrap; }
  .inspect-btn, .hw-btn, .pt-cam, .pf-disclose { white-space: nowrap; }
  .bank-find input, #cmdk-input { min-height: 44px; }
  #vol { min-height: 44px; }
  /* The octave's two keys: a finger wide, and with no keyboard to name, an
     arrow each (their names are their aria-labels). */
  .kcap#oct-down, .kcap#oct-up { min-width: 44px; min-height: 44px; display: inline-grid; place-items: center; }
  /* A sound's row in the bank: its four acts a finger's size tall, and as
     wide where the row has the width. A tablet's strip is the row's width
     shared among them, down to 39 px each for four and 31 px for a bred
     sound's five in the narrowest bank, and a phone's stars are 32 px wide
     (#534). */
  .bi-hear, .bi-star, .bi-save, .bi-kill { min-width: 44px; min-height: 44px; }
}

/* One screen a level, a phone held upright. */
@media (max-width: 700px) and (min-height: 501px) {
  /* PERFORM: the name, the well a third of the screen, then the controls,
     which scroll in their own column when there are more than fit. */
  /* Each level fits; where it cannot (a shorter phone, larger text) it
     scrolls rather than cutting what is under the fold. */
  .view.perform { overflow-y: auto; padding: var(--s2) var(--s3) 0; }
  .pf-grid { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; gap: var(--s3); }
  .pf-left { flex: none; display: flex; flex-direction: column; gap: var(--s2); }
  .pf-right { flex: 1 1 0; }
  /* The guide pill stands over the controls' column here, which keeps
     room for it at its foot; the well needs none. */
  body :where(.stage):has(> #guide:not(.hidden)) .view.perform .pf-left { padding-bottom: 0; }
  .pf-head { gap: 0; }
  .pf-blurb:empty { display: none; }
  .pf-knob svg { max-width: 60px; }
  .pf-cap-row { min-height: 0; }
  /* The row keeps its height (the moved bar's, shown or not), so the
     controls under it never move under a finger when the sound leaves
     home. */
  .pf-name { font-size: var(--t-title); }
  .pf-well { height: clamp(120px, 22dvh, 300px); min-height: 0; }
  .pf-right { overflow-y: auto; overscroll-behavior: contain; padding-bottom: calc(var(--s7) + var(--s5)); gap: var(--s3); }
  .pf-k-ends, .pf-k-sub { letter-spacing: 0; }

  /* EVOLVE: the pair fills what the words and the map leave, PLAY and PICK
     always on the screen. */
  #view-evolve { overflow-y: auto; padding: var(--s2) var(--s3) var(--s2); gap: var(--s2); }
  .ev-head { gap: var(--s2); }
  .duel-row { flex: 1 1 0; min-height: 0; }
  .duel-card { grid-template-rows: minmax(0, 1fr) auto; min-height: 0; }
  .duel-well { height: auto; min-height: 96px; }
  .duel-corner .inspect-btn { padding-left: var(--s1); padding-right: var(--s1); }
  .ev-map { height: clamp(56px, 9dvh, 96px); }
  /* The foot on one line: another pair and its rule, then EVOLVE POOL and
     what each generation did. */
  .ev-foot { flex: none; flex-wrap: nowrap; gap: var(--s2); }
  .ev-foot .duel-deal { flex: 1 1 0; }
  .ev-evolve { flex: none; }

  /* PATCH: the acts on one line, the words of what it is made of read
     whole (on two lines, if it takes two). */
  .pt-headacts { flex-wrap: nowrap; gap: var(--s1); }
  .pt-evolve { min-width: 0; flex: 0 1 auto; }
  .pt-evolve #rack-evolve { padding-left: var(--s2); padding-right: var(--s2); letter-spacing: 0.06em; }
  .pt-sub { flex-wrap: wrap; row-gap: 0; }
  #view-patch .pt-sub .subject-meta { white-space: normal; overflow: visible; text-overflow: clip; }
  /* With the keys' drawer up the well is short: the words go, and so does
     its foot (the camera and TEACH), which stood over the rack; two
     fingers still zoom it, and the foot is back when the drawer closes. */
  body.keys-open .pt-sub, body.keys-open #view-patch .pt-foot { display: none; }

  /* UNDER THE HOOD's rows, in the controls' own scroll here. */
  .pf-hood-row { min-height: 44px; }

  /* LEARNING: the map shorter, so what it weighs starts on the screen. */
  .md-map { height: clamp(120px, 22dvh, 200px); }
}

/* One screen a level, a phone on its side. */
@media (max-height: 500px) and (max-width: 999px) {
  .view.perform { overflow-y: auto; padding-bottom: 0; }
  .pf-grid { flex: 1 1 0; min-height: 0; }
  .pf-left { grid-template-rows: auto minmax(0, 1fr); }
  .pf-cap-row { min-height: 0; }
  /* The row keeps its height (the moved bar's, shown or not), so the
     controls under it never move under a finger when the sound leaves
     home. */
  .pf-name { font-size: var(--t-title); }
  .pf-well { min-height: 0; }
  .pf-right { overflow-y: auto; overscroll-behavior: contain; justify-content: flex-start; padding-bottom: calc(var(--s7) + var(--s5)); }

  #view-evolve { overflow-y: auto; padding-bottom: var(--s2); gap: var(--s2); }
  .ev-map { height: 56px; }
  .duel-row { flex: 1 1 0; min-height: 0; }
  .duel-card { grid-template-rows: minmax(0, 1fr) auto; min-height: 0; }
  .duel-well { height: auto; min-height: 80px; }
  .duel-foot { flex-direction: row; align-items: center; }

  /* The cross's stops are a finger's size (below): the level leaves them a
     desktop's room, EVOLVE's beside the line clear of the controls. */
  .stage { --rail-w: 96px; }
  .pf-hood-row { min-height: 44px; }
  /* The catalog the screen's height, not the short well's. */
  .nodebank { position: fixed; top: calc(var(--menubar-h) + var(--s2)); bottom: var(--s2); left: calc(var(--s2) + env(safe-area-inset-left, 0px)); width: 300px; z-index: 36; }

  /* LEARNING: the map beside what it weighs and its forecasts. */
  .md-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-auto-rows: auto; align-items: start; }
  .md-mapp { grid-column: 1; grid-row: 1 / span 3; }
  .md-weighs, .md-fcp, .md-foot { grid-column: 2; min-height: auto; }
  .md-map { height: clamp(120px, 40dvh, 240px); }
}

/* A touch tablet, and a phone on its side: the cross's stops a finger's
   size, and the levels give it the room a desktop's has. */
@media (pointer: coarse) and (min-width: 701px), (pointer: coarse) and (max-height: 500px) {
  .rail-stop, .rail-stop.aside, .rail-keys { width: 44px; height: 44px; }
  .rail-line { height: 12px; }
}

/* Narrower than a 360 px phone (or larger text): PATCH's acts take a second
   line rather than one of them being squeezed out of sight. */
@media (max-width: 359px) and (min-height: 501px) {
  .pt-headacts { flex-wrap: wrap; row-gap: var(--s1); }
}

/* The sound in hand's pill holds its ▶ at a finger's size, and on the
   narrowest phones the menu bar packs closer so the pill keeps its face
   and the count beside it is not squeezed (#134). */
@media (pointer: coarse) {
  .inhand { height: auto; min-height: 46px; padding-top: 0; padding-bottom: 0; }
}
@media (max-width: 340px) {
  .menubar { gap: var(--s1); padding-left: var(--s2); padding-right: var(--s2); }
  .inhand { flex: none; gap: var(--s1); padding-left: 2px; padding-right: var(--s1); }
}

/* A control's two ends, its line and EVOLVE's rule for the pair: on one
   line where they fit, else two lines of a length, never a word or two
   stood alone (#134). */
.pf-k-ends, .pf-k-sub, .duel-rule { text-wrap: balance; }
/* On a phone, a line of status or guidance that takes two lines takes two
   of a length too. */
@media (max-width: 999px) {
  .pf-status, .md-fcnote, #teach-copy, .md-none, .lv-sub, .lv-title { text-wrap: balance; }
}
@media (max-width: 340px) {
  .pf-ctlhead { flex-wrap: wrap; row-gap: 0; }
}

/* UNDER THE HOOD on a touch screen: every row a finger's size, the hood
   scrolling on its own to the rest, three rows in sight (#134). */
@media (pointer: coarse) {
  .pf-hood-row { min-height: 44px; }
  .pf-hood { max-height: calc(2em + 3 * 46px); }
}
/* …and never squeezed under three of them (a short screen's 3.6em floor,
   above, left one in sight at 1024 x 768): the hood keeps its height and the
   column scrolls to the pads if it must. Not on a phone held upright, whose
   controls' column scrolls already. */
@media (pointer: coarse) and (min-width: 701px), (pointer: coarse) and (max-height: 500px) {
  .pf-hood { flex: none; min-height: calc(2em + 3 * 46px); }
  .pf-right { overflow-y: auto; overscroll-behavior: contain; }
}

/* The face at OUT is drawn at the rack's zoom, 14 px across on a phone:
   on a touch screen its ▶ answers a finger's 44 px around its middle,
   without the face growing (#134). */
/* While sockets are lit the ring goes, so it never stands over the socket
   beside OUT (the patch's own input). */
#view-patch:has(#rack-svg .jack.legal) .out-play::before { display: none; }
@media (pointer: coarse) {
  .out-play::before {
    content: "";
    position: absolute;
    top: min(0px, calc((100% - 44px) / 2));
    bottom: min(0px, calc((100% - 44px) / 2));
    left: min(0px, calc((100% - 44px) / 2));
    right: min(0px, calc((100% - 44px) / 2));
  }
}

/* ---- a phone, stationary (#134, the second pass) ----
   As a native app's screens are: the page never moves under a finger. No
   level scrolls (each lays out to its screen, with the keys' drawer open or
   closed, and folds what does not fit behind a pane's tab: PERFORM's and
   LEARNING's `.lv-panes`); a list in a sheet scrolls as every list does
   (the bank's, the catalog's, the hood's rows). Nothing a finger holds
   selects text or raises the page's callout and loupe: on iOS a selection
   is UIKit's own gesture, not a DOM event, so only the stylesheet stops it,
   and stopping it on the whole page is what lets a long press reach its
   hold (a selection begun cancels the pointer). */
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  html, body { overscroll-behavior: none; }
  /* The stage takes no pan of the browser's own: what scrolls says so. */
  .stage { touch-action: manipulation; }
  .view.on { overscroll-behavior: none; }
  /* An EVOLVE card takes every move of a finger on it: nothing under it
     scrolls, so an upward swipe is never the browser's (main.js picks on
     it; down and sideways do nothing). */
  #view-evolve section[aria-label^="Sound "] { touch-action: none; }
}
@media (pointer: coarse) and (max-width: 700px) and (min-height: 501px), (pointer: coarse) and (max-height: 500px) and (max-width: 999px) {
  body, body * {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }
  body :is(input, textarea, [contenteditable="true"]),
  body :is(input, textarea, [contenteditable="true"]) * {
    -webkit-user-select: text;
    user-select: text;
  }
  /* No grey flash over what a finger presses: each control lights itself. */
  body * { -webkit-tap-highlight-color: transparent; }
}

/* A word on a phone held upright is a banner across the screen, in the
   free band of the level nearest its foot (main.js `positionToastLane` sets
   its top, and over a picture its sides): as wide as its words up to the
   screen's, on one or two lines with the action beside them (or under them
   when they need the room). The lane shows one at a time (toasts.js), so no
   stack grows over the level. A swipe up puts it away (main.js), on a phone
   on its side too. */
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  .toasts { left: calc(var(--s2) + env(safe-area-inset-left, 0px)); right: calc(var(--s2) + env(safe-area-inset-right, 0px)); align-items: flex-end; z-index: 39; }
  .toast {
    max-width: 100%;
    flex-wrap: wrap;
    gap: var(--s1) var(--s2);
    padding: var(--s1) var(--s3);
    border-radius: var(--r2);
    font-size: var(--t-value);
    line-height: 1.3;
    touch-action: none;
  }
  .toast-msg {
    flex: 1 1 14em;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    text-wrap: pretty;
  }
  .toast-undo { flex: none; margin-left: auto; min-height: 36px; white-space: nowrap; }
  .toast-stack { flex: none; }
  /* The counter keeps to the words' row where it has room (the words keep
     9em beside the action): on a row of its own it made the banner a row
     taller for two characters. */
  .toast:has(> .toast-stack:not(.hidden)) .toast-msg { flex-basis: 9em; }
  /* Where no band of the level holds the banner at its own shape, it is one
     row (main.js `positionToastLane`'s rungs): its words, two lines at most
     (`.toast-msg`'s clamp above), beside its action rather than over it. */
  .toasts.tight .toast { flex-wrap: nowrap; }
  .toasts.tight .toast-msg { flex: 1 1 0; }
  .toast.swiped { transform: translateY(-120%); opacity: 0; transition: transform var(--d-move) var(--e-settle), opacity var(--d-move) var(--e-settle); }
  .model-tag { max-width: calc(100vw - 2 * var(--s3)); }
}
/* On a phone on its side the toast is not a banner: it stands where it is
   free, as on a desk (main.js `positionToastLane`), as wide as its words up
   to a desk's toast. */
@media (max-height: 500px) and (max-width: 999px) {
  .toast { max-width: min(360px, 62vw); }
}

/* The keys' drawer, a phone held upright: one row of its controls (the
   octave, the volume, MIDI, REC, KEYS ⋯) over a keybed the screen's width,
   so the level above it keeps most of the screen. */
@media (max-width: 700px) and (min-height: 501px) {
  body.keys-open .keybar { flex-wrap: wrap; gap: var(--s1) var(--s2); padding: var(--s1) var(--s2) var(--s2); }
  .keys-side .oct { gap: 0; }
  #oct-label { min-width: 2em; }
  .keys-cluster { gap: var(--s1); flex: 1 1 auto; min-width: 0; justify-content: flex-end; }
  .keys-cluster .vol .pc-val { display: none; }
  #vol { width: clamp(48px, 18vw, 96px); }
  .piano { height: clamp(88px, 13dvh, 120px); }
}

/* Stationary from a 360 x 600 phone held upright, and one on its side 380 px
   tall, up: a smaller screen, or larger text (a 390 px phone at 125% lays
   the page out 312 px wide), keeps the level's own scroll rather than
   cutting off what does not fit. */
@media (max-width: 700px) and (min-width: 340px) and (min-height: 600px), (max-height: 500px) and (min-height: 380px) and (max-width: 999px) {
  .view.on, .view.perform .pf-right { overflow: hidden !important; }
}

/* A level's panes' tabs (shell.js): drawn only on a phone, where one pane
   of the level is on the screen at a time. A segmented control: the tabs
   share one rounded track, the one shown lit. */
.lv-panes { display: none; }
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  .lv-panes {
    display: flex;
    flex: none;
    gap: 2px;
    padding: 2px;
    border: 1px solid var(--hairline);
    border-radius: var(--r2);
    background: var(--well);
  }
  .lv-panes [role="tab"] {
    flex: 1 1 0;
    min-width: 0;
    min-height: 44px;
    padding: 0 var(--s1);
    border: 0;
    border-radius: calc(var(--r2) - 2px);
    background: none;
    color: var(--silk-dim);
    font-family: var(--font-silk);
    font-size: var(--t-value);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    cursor: pointer;
  }
  .lv-panes [role="tab"][aria-selected="true"] { background: var(--panel); color: var(--phos-a); box-shadow: inset 0 0 0 1px var(--plate-edge); }
  .lv-panes [role="tab"]:focus-visible { outline: 2px solid var(--phos-a); outline-offset: -2px; }

  /* PERFORM: the controls and the pads, or what is under the hood. */
  .pf-ctlcap { display: none; }
  .pf-ctlhead { gap: var(--s2); }
  .pf-ctlhead .lv-panes { flex: 1 1 auto; }
  .pf-actions { gap: var(--s1); }
  #view-perform[data-pane="controls"] .pf-hood { display: none; }
  #view-perform[data-pane="hood"] :is(.pf-deck, .pf-pads, .pf-status) { display: none; }
  #view-perform[data-pane="hood"] .pf-hood { flex: 1 1 0; min-height: 0; max-height: none; overflow-y: auto; overscroll-behavior: contain; }
  .view.perform .pf-right { padding-bottom: 0; }

  /* LEARNING: one of its three panels, the tabs under its headline. */
  #view-learning .md { display: flex; flex-direction: column; height: 100%; overflow: hidden; gap: var(--s2); }
  #view-learning .md-head { order: -2; }
  #view-learning .lv-panes { order: -1; }
  #view-learning .md-body { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
  #view-learning .md-right { display: contents; }
  #view-learning .md-panel { flex: 1 1 0; min-height: 0; }
  #view-learning:not([data-pane="weighs"]) .md-weighs,
  #view-learning:not([data-pane="map"]) .md-mapp,
  #view-learning:not([data-pane="forecasts"]) .md-fcp { display: none; }
  #view-learning .md-mapp { display: flex; flex-direction: column; }
  #view-learning:not([data-pane="map"]) .md-mapp { display: none; }
  #view-learning .md-map { flex: 1 1 0; min-height: 0; height: auto; }
  #view-learning .md-weighs { overflow-y: auto; overscroll-behavior: contain; }
}

/* Each level on one screen, a phone held upright, the keys' drawer open or
   not (#134). What says again what the bar along the bottom says (a
   level's cap) goes; with the drawer up, what is for looking at (a well, a
   map, a headline) gives its room to what is for the hands. */
@media (max-width: 700px) and (min-height: 501px) {
  /* PERFORM: the name, the well, the tabs, then the controls and pads. */
  .view.perform { padding: var(--s2) var(--s3) var(--s2); }
  .pf-grid { gap: var(--s2); }
  .pf-cap { display: none; }
  /* The moved bar (KEEP, BACK) rides at the name's line's end, so its coming
     and going moves nothing under a finger; the name gives it the room. */
  .pf-head { position: relative; display: block; }
  .pf-cap-row { position: absolute; top: 0; right: 0; min-height: 44px; pointer-events: none; }
  .pf-cap-row .pf-moved.on { pointer-events: auto; }
  .pf-head:has(.pf-moved.on) .pf-title { padding-right: 172px; } /* token-exempt: the moved bar's width (MOVED, KEEP, BACK), not spacing */
  .pf-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  .pf-k-line, .pf-k-sub { min-height: 0; }
  /* Whole, never cut: two lines of a length where one will not hold it. */
  .pf-k-sub { white-space: normal; overflow: visible; text-overflow: clip; text-wrap: balance; letter-spacing: 0; }
  .pf-pads .pf-pad, .pf-pads > button { min-height: 44px; }
  .pf-title { min-height: 0; }
  .pf-well { flex: 1 1 0; height: auto; min-height: 0; }
  .pf-left { flex: 1 1 0; min-height: 64px; }
  .pf-right { flex: none; gap: var(--s2); }
  .pf-deck { row-gap: var(--s1); }
  .pf-knob svg { max-width: 52px; }
  body.keys-open .view.perform .pf-right { flex: 1 1 0; min-height: 0; }
  /* With the keys up the controls take the column's height: their rows
     share it, the dials as large as it allows, with no band left empty. */
  body.keys-open .view.perform .pf-deck { flex: 1 1 auto; grid-auto-rows: 1fr; align-items: center; }
  body.keys-open .pf-status { display: none; }
  body.keys-open .pf-k-ends { display: none; }

  /* EVOLVE: the line of what to do (its pips), the small map, the pair,
     the foot. Its cap and headline go: the bar names the level, and the
     line says the question. */
  #view-evolve .ev-cap, #view-evolve .ev-title { display: none; }
  #view-evolve .duel-mid, #view-evolve .teach { margin: 0; }
  .duel-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--t-body); }
  .duel-fam { display: none; }
  .duel-foot { gap: var(--s1); }
  .duel-guess { left: var(--s1); right: var(--s1); max-width: calc(100% - 2 * var(--s1)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  body.keys-open #view-evolve .ev-map, body.keys-open #view-evolve .ev-foot { display: none; }
  body.keys-open .duel-well { min-height: 64px; }
  .ev-foot .duel-rule { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

  /* TASTE and LEARNING: the headline's cap goes with the drawer up. */
  body.keys-open :is(#view-taste, #view-learning) :is(.lv-eyebrow, .lv-sub) { display: none; }
}

/* A bank row on a phone (#134), as a phone's own lists' rows: its name and
   its ▶ at rest, the ▶ at the row's end; swiped left, its other actions
   (★, save, cut) slide out beside the ▶ at the row's end, on the row's
   ground (main.js `bankSwipedId`). A row's stars folded out stay out. The
   name gives them the room they take: its box ends where they begin, and
   what does not fit ends in an ellipsis, never under them (#534). The
   buttons are a finger's size (`--hit`), the stars as tall and a thumb's
   width. */
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  .bank-item {
    --hit: 24px; /* token-exempt: the ▶'s column and the strip's buttons, a mouse's size */
    --star: 24px; /* token-exempt: a star's width in the strip */
    touch-action: pan-y;
    min-height: 40px;
    justify-content: center;
    gap: 2px;
    padding-right: calc(var(--hit) + var(--s2) + var(--s1)); /* the ▶'s column: its button, the strip's padding and its offset from the row's end */
  }
  .bank-item .bi-acts {
    position: absolute;
    right: var(--s1);
    top: 50%;
    bottom: auto;
    transform: translateY(-50%);
    padding-left: var(--s2);
    gap: 0;
    background: var(--panel-hi);
    border-radius: var(--r1);
  }
  @media (pointer: coarse) {
    .bank-item {
      --hit: 44px; /* token-exempt: a finger's size */
      --star: 32px; /* token-exempt: a thumb's width for a star */
    }
  }
  .bank-item .bi-acts button { flex: 0 0 var(--hit); width: var(--hit); } /* the width too: Firefox sizes a strip that is placed over the row by it, not by the basis */
  .bank-item .bi-acts .stars { flex: none; }
  .bank-item .bi-acts .star { flex: 0 0 var(--star); width: var(--star); }
  /* The name's box ends where the strip that shows begins: the row's end
     already holds the ▶'s column, so the name gives up what the strip takes
     beyond it. Three more buttons (four for a bred sound's compare), or the
     five stars less the ▶'s. */
  .bank-item:not(.preset-item):is(.acts-open, :focus-within) .bi-top { padding-right: calc(3 * var(--hit)); }
  .bank-item.bred:is(.acts-open, :focus-within) .bi-top { padding-right: calc(4 * var(--hit)); }
  .bank-item.rating:not(.preset-item) .bi-top { padding-right: calc(5 * var(--star) - var(--hit)); }
  /* Away, but in the accessibility tree and the tab order: a screen
     reader, a keyboard or a mouse reaches them, and focus inside the row
     slides them out as a swipe does. */
  .bank-item:not(.acts-open):not(.rating):not(:focus-within) .bi-acts > :not(.bi-hear) { position: absolute; width: 1px; height: 1px; min-width: 0; min-height: 0; padding: 0; overflow: hidden; clip-path: inset(50%); }
  .bank-item:not(.acts-open):not(.rating):not(:focus-within) .bi-acts { background: none; }
  .bank-item.acts-open .bi-acts { animation: acts-out var(--d-state) var(--e-settle); box-shadow: -8px 0 12px var(--panel-hi); }
  .bank-item .bi-flag, .bank-item .bi-pct { right: calc(var(--hit) + var(--s2)); }
  /* A preset's IN POOL stands left of the ▶'s column, level with the name,
     in every state the ▶ has (the ▶ is at the row's end, shown at rest under
     a finger), and the name stops short of it on every preset row, so none
     moves when a preset joins the pool. The selectors are the ones that
     place it on a mouse's screen and under a coarse pointer, repeated: an
     :is() list takes the weight of its heaviest member, and this has to win
     over each of them. */
  .preset-item { padding-right: calc(var(--hit) + var(--s2) + 2 * var(--s1) + 52px); } /* token-exempt: the ▶'s column, then the width IN POOL takes */
  .preset-item .pb-in,
  .preset-item:is(:hover, .under, :focus-within, .kbd, :has(.bi-hear.playing)) .pb-in,
  body.model-view .preset-item:not(:hover, .under) .pb-in {
    right: calc(var(--hit) + var(--s2) + var(--s1));
    top: 50%;
    bottom: auto;
    transform: translateY(-50%);
    background: none;
  }
}
@keyframes acts-out { from { transform: translate(24px, -50%); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .bank-item.acts-open .bi-acts { animation: none; } }

/* …and the second look (#134): sizes that make each of these one screen. */
@media (max-width: 700px) and (min-height: 501px) {
  /* A level's cap names it again under the bar that names it. */
  :is(#view-taste, #view-learning) .lv-eyebrow, #view-patch .pt-cap:not(.pt-family) { display: none; }
  /* PERFORM's controls: the dial a thumb's size, its words close under it. */
  .pf-knob { padding: 0; }
  .pf-knob svg { width: 48px; height: 48px; max-width: none; }
  .pf-k-name { margin-top: 0; }
  .pf-deck { gap: var(--s2) var(--s1); }
  .pf-ctlhead { flex-wrap: nowrap; }
  .pf-ctlhead .lv-panes [role="tab"] { font-size: var(--t-label); }
  .pf-actions .pf-disclose { letter-spacing: 0.06em; padding-left: var(--s1); padding-right: var(--s1); }
  .pf-status { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  body.keys-open .pf-knob svg { width: 44px; height: 44px; }
  /* The keys' drawer on one row: the octave, the volume, MIDI, REC and
     KEYS ⋯ close together. */
  .keys-side { flex: none; }
  .keys-cluster { gap: 2px; }
  #vol { width: 52px; }
  .keys-cluster .util-btn, .midi-ind { padding-left: var(--s2); padding-right: var(--s2); letter-spacing: 0.04em; }
  /* LEARNING's math and its JSON, opened, take part of the panel's room. */
  #view-learning :is(.md-math, .md-json) { max-height: 28dvh; overflow: auto; }
}

/* A phone on its side (#134): the same, in a short screen. */
@media (max-height: 500px) and (max-width: 999px) {
  #view-evolve .ev-cap, #view-evolve .ev-title, :is(#view-taste, #view-learning) .lv-eyebrow { display: none; }
  body.keys-open #view-evolve :is(.ev-map, .ev-foot) { display: none; }
  body.keys-open #view-evolve .ev-head { min-height: 0; }
  body.keys-open :is(#view-taste, #view-learning) :is(.lv-sub) { display: none; }
}

/* PERFORM, third look (#134): each control's own line says what it is
   listening to, so the column's line of it goes; with the keys up the
   offer pad's hint does too. On a phone on its side the controls are one
   row of six, without their two ends' words, and with the keys up the
   well gives the room to them. */
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  /* The column's line (how many controls reach the patch, what it is
     listening to, turn an amber one to ask) on one or two lines of a length
     with the keys down; with them up each control's own line says what it
     is listening to, and an amber control is its ask. */
  .view.perform .pf-status { white-space: normal; overflow: visible; text-overflow: clip; text-wrap: balance; }
  body.keys-open .view.perform .pf-status { display: none; }
  body.keys-open .pf-pad[data-sub]::after { display: none; }
}
@media (max-height: 500px) and (max-width: 999px) {
  .pf-deck { --pf-cols: 6 !important; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--s1); }
  /* Six across, a control's line keeps only what fits whole: its one-way
     arrow; the knobs it turns are under the hood's tab and in its title. */
  .pf-k-sub:not([data-short]) { visibility: hidden; }
  .pf-k-ends { display: none; }
  .pf-knob svg { width: 44px; height: 44px; }
  .pf-k-line, .pf-k-sub { min-height: 0; }
  /* Whole, never cut: two lines of a length where one will not hold it. */
  .pf-k-sub { white-space: normal; overflow: visible; text-overflow: clip; text-wrap: balance; letter-spacing: 0; }
  .view.perform .pf-cap { display: none; }
  body.keys-open .view.perform .pf-left { display: none; }
  body.keys-open .view.perform .pf-grid { grid-template-columns: minmax(0, 1fr); }
}
/* WANDER at the head of the pad row, its dial and its name, so the row is
   one line; its state is its ring's (lit while frozen), and its words are
   its aria-valuetext. */
@media (max-width: 700px) and (min-height: 501px) {
  .pf-pads { grid-template-columns: auto repeat(4, minmax(0, 1fr)); column-gap: var(--s1); }
  .pf-pads .pf-wander { grid-column: auto; display: flex; flex-direction: column; align-items: center; gap: 0; padding: 0; }
  .pf-pads .pf-wander svg { width: 40px; height: 40px; max-width: none; }
  .pf-pads .pf-wander :is(.pf-k-line, .pf-k-ends) { display: none; }
  .pf-pads .pf-wander .pf-k-name { font-size: var(--t-label); letter-spacing: 0.06em; }
  .pf-pad { padding-left: var(--s1); padding-right: var(--s1); letter-spacing: 0.06em; }
}

/* The keys' drawer on a phone held upright, one row on every phone
   (312 to 430 px wide; #134): the octave's arrows drawn small inside their
   44 px, the volume as short as a thumb's throw, REC its dot (its words
   are its name), KEYS ⋯; MIDI is a row of KEYS ⋯ here (main.js). */
@media (max-width: 700px) and (min-height: 501px) {
  body.keys-open .keybar { flex-wrap: wrap; row-gap: var(--s1); column-gap: var(--s1); }
  .keybar .keys-side, .keybar .keys-cluster { flex-wrap: nowrap; }
  .keys-side .oct { gap: 0; }
  .kcap#oct-down, .kcap#oct-up { width: 44px; border-color: transparent; background: none; box-shadow: none; }
  .keys-cluster { flex: 1 1 0; min-width: 0; gap: var(--s1); }
  .keys-cluster .vol { flex: 1 1 40px; min-width: 36px; }
  #vol { width: 100%; min-width: 36px; }
  #rec-btn { position: relative; width: 44px; min-width: 44px; padding: 0; justify-content: center; }
  #rec-btn .rec-w { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  #keys-btn { flex: none; }
  .keys-pop .midi-wrap { display: flex; align-items: center; }
  .piano { order: 3; flex: 1 0 100%; }
}
/* Under the hood, the pane takes the column's room for its rows (#134). */
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  #view-perform[data-pane="hood"] .pf-right { flex: 1 1 0; min-height: 0; }
  #view-perform[data-pane="hood"] .pf-hood { min-height: calc(3 * 44px); }
}
@media (max-height: 500px) and (max-width: 999px) {
  .duel-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
@media (max-width: 339px) {
  .pf-ctlhead { flex-wrap: wrap; }
}

/* ---- a phone, the pixel pass (#134) ---- */
@media (pointer: coarse) {
  /* The volume fader is a finger tall: its track stays a 4 px line through
     its middle, lit exactly to the thumb (main.js sets --v), not a pill the
     fader's whole height with a fixed gradient across it. */
  #vol {
    background:
      linear-gradient(90deg, var(--phos-a-deep), var(--phos-a-deep)) left center / calc(6px + (100% - 12px) * var(--v, 0.8)) 4px no-repeat,
      linear-gradient(90deg, var(--hairline), var(--hairline)) center / 100% 4px no-repeat;
    border-radius: 0;
  }
}
@media (pointer: coarse) {
  /* A glyph or a word in a fixed square sits in its middle: PERFORM's XY
     and stage buttons stood their glyph at the left. */
  .pf-corner .util-btn { justify-content: center; padding-left: 0; padding-right: 0; min-width: 44px; }
  /* The octave's arrows, in the middle of their 44 px and a thumb's size
     to read. */
  .kcap#oct-down::before, .kcap#oct-up::before { font-size: var(--t-title); line-height: 1; display: block; transform: translateY(3px); }
}
/* A finger leaves no hover behind it (iOS keeps :hover on what was last
   tapped): a button lights only for a real state (lit, pressed, open). */
@media (hover: none) {
  .util-btn:hover:not(.lit):not([aria-pressed="true"]):not([aria-expanded="true"]) { border-color: var(--black); }
  .inspect-btn:hover:not(:disabled):not([aria-pressed="true"]):not([aria-expanded="true"]) { color: var(--silk-dim); border-color: var(--hairline); }
}
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  /* EVOLVE's card: its letter clear of the corner's buttons. */
  .duel-corner { left: calc(var(--s3) + 1.5em); }
  /* The bank's sheet: the row in hand drawn whole, its ring inside the list. */
  .bank-list { padding-right: var(--s1); }
}
@media (max-width: 700px) and (min-height: 501px) {
  /* EVOLVE's card corner: CIRCUIT and PATCH share the corner's width, each
     its words whole inside its box. */
  .duel-corner { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .duel-corner .inspect-btn { min-width: 0; padding-left: var(--s1); padding-right: var(--s1); font-size: var(--t-label); letter-spacing: 0; justify-content: center; text-align: center; }
  /* PLAY and PICK the card's width. */
  .duel-foot { flex-direction: column; align-items: stretch; }
  .duel-controls { width: 100%; }
  .duel-controls .hw-btn { justify-content: center; }
  /* The foot: two columns, each read from its own edge. ANOTHER PAIR over
     how the pair was dealt; EVOLVE POOL over what each generation did. */
  .ev-foot { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; gap: var(--s2); }
  .ev-foot .duel-deal { flex-direction: column; align-items: flex-start; gap: 2px; }
  .ev-foot .duel-rule { max-width: 100%; text-align: left; }
  .ev-evolve { justify-items: end; }
  .ev-lineage-btn { letter-spacing: 0.06em; text-align: right; }
}
@media (max-width: 700px) and (min-height: 501px) {
  /* PATCH's head: no empty line where its cap was. */
  #view-patch .pt-cap-row { min-height: 0; }
}
@media (max-width: 700px) and (min-height: 501px) {
  /* The card's letter at the well's foot, so CIRCUIT and PATCH have the
     corner's whole width and their words fit their boxes. */
  .duel-side { top: auto; bottom: var(--s2); }
  .duel-corner { left: var(--s2); right: var(--s2); top: var(--s2); }
  /* Words in the foot wrap as two lines of a length, never one alone. */
  .ev-foot .duel-rule { white-space: normal; overflow: visible; text-overflow: clip; text-wrap: balance; }
  .ev-lineage-btn { text-wrap: balance; }
}
@media (pointer: coarse) {
  /* Every other fader a finger tall keeps its 4 px track through its
     middle, not a pill its whole height. */
  input[type="range"]:not(#vol) { background: linear-gradient(90deg, var(--phos-a-deep), var(--hairline)) center / 100% 4px no-repeat; border-radius: 0; }
}
@media (max-width: 700px) and (min-height: 501px) {
  /* MIDI's panel, opened from its row in KEYS ⋯, stands over the screen
     under the menu bar, whole, rather than off its top edge. */
  .keys-pop .midi-panel { position: fixed; top: calc(var(--menubar-h) + var(--s2)); left: var(--s2); right: var(--s2); bottom: auto; width: auto; max-height: 60dvh; overflow-y: auto; }
}

/* The menu bar on a phone (#134): every control 44 px tall on one centre
   line, the same gap between each, the count whole beside MODEL, and the
   sound in hand's pill taking what is left. */
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  .menubar { align-items: center; gap: var(--s2); }
  .menubar > * { margin-top: 0; margin-bottom: 0; align-self: center; }
  .bank-btn, .cmdk-btn, .model-btn { height: 44px; min-height: 44px; min-width: 44px; margin: 0; }
  .model-btn { width: 44px; padding: 0; justify-content: center; border-radius: var(--r2); }
  .inhand { height: 44px; min-height: 44px; }
  .menubar .taught { flex: none; min-width: max-content; margin: 0; position: relative; z-index: 1; }
}

/* With the keys up a phone too short for the well gives its room to the
   controls; a taller one keeps the name and the well over them. */
@media (max-width: 700px) and (min-height: 501px) and (max-height: 799px) {
  body.keys-open .view.perform .pf-left { display: none; }
}
@media (max-width: 700px) and (min-height: 800px) {
  body.keys-open .view.perform .pf-left { flex: 1 1 0; min-height: 0; }
}
@media (max-width: 700px) and (min-height: 501px) {
  body.keys-open .pf-knob svg { width: clamp(44px, 8dvh, 64px); height: clamp(44px, 8dvh, 64px); }
  /* EVOLVE: CIRCUIT and PATCH on a row of their own at the well's top, the
     face under them, never under the buttons. */
  .duel-well > .face-slot { inset: calc(44px + 2 * var(--s2)) var(--s2) var(--s2); }
  /* The foot, each line whole: ANOTHER PAIR and EVOLVE POOL side by side,
     then how the pair was dealt, then what each generation did. */
  .ev-foot { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "deal evolve" "rule rule" "lineage lineage"; row-gap: 2px; }
  .ev-foot .duel-deal { display: contents; }
  .ev-foot #skip-duel { grid-area: deal; justify-self: start; }
  .ev-foot .duel-rule { grid-area: rule; white-space: nowrap; text-wrap: nowrap; }
  .ev-foot .duel-pred { grid-area: lineage; justify-self: start; align-self: center; }
  .ev-evolve { display: contents; }
  .ev-evolve .evolve-wrap { grid-area: evolve; justify-self: end; }
  .ev-lineage-btn { grid-area: lineage; justify-self: end; white-space: nowrap; text-wrap: nowrap; }
}
@media (max-width: 700px) and (min-height: 501px) {
  /* While the guide pill is up it stands at the foot of PERFORM's column,
     in room kept for it there, over none of the pads. */
  body :where(.stage):has(> #guide:not(.hidden)) .view.perform .pf-right { padding-bottom: calc(var(--s7) + var(--s2)); }
}
@media (max-width: 700px) and (min-height: 800px) {
  /* Tall enough to keep the well with the keys up: the controls take their
     own height (and the guide pill's room), the well the rest. */
  body.keys-open .view.perform .pf-right { flex: none; }
}
@media (max-width: 340px) {
  .menubar { gap: var(--s1); }
}

/* Nothing on a phone's levels is cut with an ellipsis (#134): a control's
   line, a name and a tab say their words whole, on two lines of a length
   where one will not hold them. */
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  .pf-k-sub, .pf-k-ends { letter-spacing: 0; }
  .duel-name, .pf-name, .lv-panes [role="tab"] { text-overflow: clip; }
}
@media (max-width: 700px) and (min-height: 501px) {
  /* EVOLVE's foot, one block: ANOTHER PAIR and EVOLVE POOL the same height
     on one row; under them how the pair was dealt beside what each
     generation did, each in its half, with no band between. */
  .ev-foot { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: "deal evolve" "rule lineage" "pred pred"; row-gap: var(--s1); column-gap: var(--s2); align-items: center; }
  .ev-foot #skip-duel, .ev-foot .evolve-wrap, .ev-foot #evolve-btn { min-height: 44px; height: 44px; }
  .ev-foot #skip-duel { justify-self: stretch; justify-content: center; }
  .ev-foot .evolve-wrap { justify-self: stretch; display: flex; }
  .ev-foot #evolve-btn { flex: 1 1 auto; justify-content: center; }
  .ev-foot .duel-rule { grid-area: rule; white-space: normal; text-wrap: balance; align-self: center; }
  .ev-foot .duel-pred { grid-area: pred; justify-self: start; }
  .ev-foot .duel-pred:empty { display: none; }
  .ev-lineage-btn { grid-area: lineage; justify-self: end; white-space: normal; text-wrap: balance; letter-spacing: 0.04em; min-height: 44px; }
}

/* A half-closed control's line on a phone: the arrow and its end's word
   (perform.js `data-short`) is drawn; the sentence stays its text, for a
   screen reader and the control's title. */
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  .pf-k-sub[data-short] { font-size: 0; }
  .pf-k-sub[data-short]::after { content: attr(data-short); font-size: var(--t-label); }
}
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  /* The menu bar's square buttons are one box (BANK's), their glyphs in
     their middles, and the bar's two sides the same margin. */
  .menubar { padding-left: calc(var(--s3) + env(safe-area-inset-left, 0px)); padding-right: calc(var(--s3) + env(safe-area-inset-right, 0px)); }
  .cmdk-btn { width: 44px; padding: 0; gap: 0; justify-content: center; background: none; border-color: var(--hairline); }
  .cmdk-btn > :not(svg) { display: none; }
  .bank-btn, .cmdk-btn, .model-btn { display: grid; place-items: center; }
}
@media (max-width: 340px) {
  .menubar { padding-left: var(--s2); padding-right: var(--s2); }
}
@media (max-height: 500px) and (max-width: 999px) {
  /* On its side the column has no room for the line over the controls:
     each control's own line says what it is listening to. */
  .view.perform .pf-status { display: none; }
}
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  /* PATCH's line of what it is made of, whole. */
  #view-patch .pt-sub .subject-meta, #rack-meta { white-space: normal; overflow: visible; text-overflow: clip; }
}
@media (max-width: 700px) and (min-height: 501px), (max-height: 500px) and (max-width: 999px) {
  /* The sound's name in PATCH's head, whole: on two lines if it must. */
  #view-patch .pt-title, #rack-subject { white-space: normal; overflow: visible; text-overflow: clip; text-wrap: balance; }
}
@media (max-height: 500px) and (max-width: 640px) {
  /* The smallest phones on their side: PATCH's head stacked, so the
     sound's name keeps a line of its own. */
  .pt-head { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "cap next" "title title" "sub sub" "acts acts" "edit edit"; }
}
