/* Beatomat 2026 — mockup stylesheet.
   One object under two lighting conditions: data-room="on" is the appliance in
   a lit room, data-room="off" is the same appliance at 11pm with the lights
   down. Nothing moves between them; only the light does.

   The type is one family used at both ends of its width axis. The machine
   speaks WIDE (headings, set names, the wordmark); its own labels and its data
   speak NARROW, uppercase and letterspaced, the way a mixer's faceplate is
   silkscreened. That is the whole typographic idea and it costs no second
   webfont. */

:root[data-room="on"] {
  --paper: #f3f4f1;
  --card: #fdfdfc;
  --sunk: #eaece7;
  --ink: #161816;
  --ink-2: #788077;
  --hair: #e1e5df;
  --hair-strong: #d2d7cf;
  --plate: #161816;
  --plate-2: #202320;
  --plate-hair: rgba(255, 255, 255, .10);
  --plate-ink: #f2f4f0;
  --plate-ink-2: #8b938a;
  --accent: #0b5c50;
  --accent-ink: #ffffff;
  --live: #14b892;
}

:root[data-room="off"] {
  --paper: #0e100e;
  --card: #171a17;
  --sunk: #0a0b0a;
  --ink: #eef1ec;
  --ink-2: #8b938a;
  --hair: #242923;
  --hair-strong: #333a32;
  --plate: #050605;
  --plate-2: #0f120f;
  --plate-hair: rgba(255, 255, 255, .09);
  --plate-ink: #f2f4f0;
  --plate-ink-2: #8b938a;
  --accent: #0f7d6c;
  --accent-ink: #ffffff;
  --live: #14b892;
}

* { box-sizing: border-box; }

/* Every applier in the shell hides things with the `hidden` attribute, and any
   component rule that sets `display` silently overrides it — which is how two
   idle decks both claimed to be ON AIR. This makes `hidden` mean hidden. */
[hidden] { display: none !important; }

/* Spacing is set by the components, never inherited from the browser's
   defaults — a stray 1em margin on a <p> is what makes a dense instrument
   panel go loose. */
h1, h2, h3, p, ul, ol { margin: 0; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: Archivo, system-ui, sans-serif;
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* The machine's voice: wide. Used for headings, set names, the wordmark and
   nothing else. */
.wide {
  font-variation-settings: 'wdth' 116;
  letter-spacing: -.005em;
}

/* The machine's labels: narrow, uppercase, letterspaced. Silkscreen. */
.silk {
  font-variation-settings: 'wdth' 68;
  text-transform: uppercase;
  letter-spacing: .15em;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--ink-2);
}

.num {
  font-variation-settings: 'wdth' 74;
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
}

/* Read by a screen reader, never drawn. */
.sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; border: 0; background: none; cursor: pointer; }

/* ─────────────────────────── shell ─────────────────────────── */

.app {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) 88px;
  /* dvh, not vh. On mobile Chrome 100vh is the viewport as it would be with the
     address bar hidden — taller than what you can actually see — so the last
     row of this grid, which is the bottom navigation, sat below the fold until
     you scrolled the whole page. dvh tracks the visible area instead. */
  height: 100vh;
  height: 100dvh;
}

/* The slots are wrappers the shell writes into; the grid must see through them
   to the rail and the bar, or neither gets its row. */
#railSlot, #barSlot { display: contents; }

.rail {
  grid-row: 1 / 3;
  border-right: 1px solid var(--hair);
  padding: 20px 14px 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow: auto;
}

.mark {
  font-variation-settings: 'wdth' 118;
  font-weight: 700;
  font-size: 21px;
  letter-spacing: -.01em;
  display: flex;
  align-items: center;
  gap: 1px;
  padding: 2px 8px 22px;
}

/* The O is the brand first and the lamp second: it keeps the accent green in
   every state, and the state lives in the fill. */
.mark .ring {
  width: 15px;
  height: 15px;
  border: 2.5px solid var(--accent);
  border-radius: 50%;
  margin: 0 1px;
  transition: background .2s, box-shadow .2s;
}
.mark.onair .ring {
  background: var(--live);
  border-color: var(--live);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--live) 22%, transparent);
}

.rail nav { display: flex; flex-direction: column; gap: 2px; }

.rail a.item, .rail button.item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: 10px;
  color: var(--ink-2);
  font-size: 14px;
  width: 100%;
  text-align: left;
}
.rail .item:hover { background: var(--sunk); color: var(--ink); }
.rail .item.on { background: var(--sunk); color: var(--ink); font-weight: 600; }
.rail .item.soon { opacity: .45; cursor: default; }
.rail .item.soon:hover { background: none; color: var(--ink-2); }
.rail .item svg { width: 17px; height: 17px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.7; }

.rail hr { border: 0; border-top: 1px solid var(--hair); margin: 14px 10px; }
.rail .silk { padding: 0 10px 8px; }
.rail .grow { flex: 1; }

/* The account zone. The allowance lives here and nowhere else — never in the
   mixer, which is instruments. */
.acct {
  border: 1px solid var(--hair);
  border-radius: 12px;
  padding: 11px 12px;
  display: grid;
  gap: 7px;
}
.acct .row { display: flex; align-items: center; gap: 6px; }
.acct .plan {
  font-variation-settings: 'wdth' 70;
  text-transform: uppercase;
  letter-spacing: .13em;
  font-size: 10px;
  font-weight: 700;
  color: var(--accent);
}
.acct .left { font-size: 13px; }
.acct .info {
  width: 15px; height: 15px; border-radius: 50%;
  border: 1px solid var(--hair-strong);
  color: var(--ink-2);
  font-size: 10px; line-height: 13px; text-align: center;
  margin-left: auto;
}
.acct .info:hover { border-color: var(--ink-2); color: var(--ink); }
.gauge { height: 3px; border-radius: 2px; background: var(--sunk); overflow: hidden; }
.gauge i { display: block; height: 100%; background: var(--accent); }
.acct .who { font-size: 12.5px; color: var(--ink-2); }

/* ─────────────────────────── centre ─────────────────────────── */

.centre { overflow: auto; padding: 0 0 40px; }
.pad { padding: 0 32px; }

.find {
  position: sticky;
  top: 0;
  z-index: 4;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(12px);
  padding: 16px 32px;
  display: flex;
  gap: 12px;
  align-items: center;
}
.find input {
  flex: 1;
  max-width: 460px;
  height: 38px;
  border-radius: 10px;
  border: 1px solid var(--hair);
  background: var(--card);
  color: var(--ink);
  padding: 0 14px;
  font: inherit;
  font-size: 14px;
}
.find .newset {
  flex: none; display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  height: 38px; padding: 0 15px; border-radius: 10px; border: 0;
  background: var(--accent); color: var(--accent-ink); font: inherit; font-size: 14px;
}
.find .newset svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.find .newset:hover { filter: brightness(1.12); }
/* On a narrow screen the word goes and the plus stays: it is the one control
   here that is understood without it. */
@media (max-width: 560px) { .find .newset span { display: none; } .find .newset { padding: 0 12px; } }
.find input::placeholder { color: var(--ink-2); }
.find input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

h1.wide { font-size: 27px; font-weight: 700; margin: 0; }
h2.wide { font-size: 19px; font-weight: 600; margin: 0; }

/* ───────────────────── the faceplate ─────────────────────
   The signature. Every functional display in the app is one continuous dark
   inset — not three cards with gaps between them. That is what makes it read
   as one machine instead of a web page with boxes on it. */

.plate {
  background: var(--plate);
  border-radius: 16px;
  color: var(--plate-ink);
  overflow: hidden;
  position: relative;
  /* With the room lights off the page is nearly as dark as the faceplate, so
     the inset would dissolve into it. One hairline keeps it an object. */
  box-shadow: inset 0 0 0 1px var(--plate-hair);
}
.plate .silk { color: var(--plate-ink-2); }
.plate-div { width: 1px; background: var(--plate-hair); }

/* Home's top block: the same faceplate, once, holding what to start now. */
.starter { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 24px; padding: 22px 24px; align-items: center; }
.starter h1 { font-size: 26px; margin: 7px 0 9px; }
.starter p { color: var(--plate-ink-2); font-size: 14px; max-width: 56ch; }
.starter .go {
  display: inline-flex; align-items: center; gap: 9px;
  margin-top: 16px; padding: 11px 20px 11px 15px;
  border-radius: 999px; background: var(--live); color: #06231d;
  font-weight: 600; font-size: 14.5px;
}
.starter .go:hover { filter: brightness(1.07); }
.starter .go svg { width: 15px; height: 15px; fill: currentColor; }
.starter .covers { display: grid; grid-template-columns: repeat(4, 74px); gap: 7px; }
.starter .covers img { width: 74px; height: 74px; border-radius: 8px; object-fit: cover; background: var(--plate-2); }

/* ───────────────────────── shelves ───────────────────────── */

.shelf-head { display: flex; align-items: baseline; gap: 14px; padding: 30px 32px 13px; }
.shelf-head .all { margin-left: auto; font-size: 12.5px; color: var(--ink-2); }
.shelf-head .all:hover { color: var(--ink); text-decoration: underline; }

.shelf { display: flex; gap: 16px; overflow-x: auto; padding: 2px 32px 6px; scrollbar-width: none; }
.shelf::-webkit-scrollbar { display: none; }

.card { width: 182px; flex: none; }
/* One cover, not a mosaic of four. A mix's record sleeve is the loudest thing
   on the page and four of them fighting inside 182px is noise — and it is four
   times the network for the same information. */
.card .cover {
  display: block;
  width: 182px; height: 182px;
  border-radius: 12px;
  border: 1px solid var(--hair);
  overflow: hidden;
  background: var(--sunk);
  background-size: cover; background-position: center;
  position: relative;
}
/* A mix whose tracks carry no artwork used to get a plain sunk square, which on
   a shelf of twelve reads as a site with holes in it. It now carries a cover
   drawn from its own mood — see shell/cover.js — painted as the background,
   with any real artwork as an <img> on top. The image is the only part that can
   fail, and when it does the cover is already behind it. */
.card .cover img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  position: absolute; inset: 0;
}
.card .play {
  position: absolute; right: 10px; bottom: 10px;
  width: 40px; height: 40px; border-radius: 50%;
  background: var(--live); color: #06231d;
  display: grid; place-items: center;
  opacity: 0; transform: translateY(6px);
  transition: opacity .16s, transform .16s;
}
.card .play svg { width: 14px; height: 14px; fill: currentColor; margin-left: 2px; }
.card:hover .play, .card:focus-within .play { opacity: 1; transform: none; }
.card .t { display: block; margin: 11px 0 3px; font-size: 14.5px; font-weight: 600; }
.card .m { display: block; font-size: 12.5px; color: var(--ink-2); }

/* A filtered view is a grid rather than a rail: you asked for one thing, so it
   wraps down the page instead of scrolling sideways past the edge. */
.grid {
  display: grid; gap: 20px 16px; margin: 2px 32px 0;
  /* Elastic columns, not a fixed maximum: with a hard 182px cap, two cards
     plus their gap do not fit a 350px phone, so the grid gave up and drew one
     card per row at full width. */
  grid-template-columns: repeat(auto-fill, minmax(146px, 1fr));
}
.grid .card, .grid .card .cover { width: 100%; }
.grid .card .cover { height: auto; aspect-ratio: 1; }

/* Genre tiles carry no artwork at all — a dark plate with the name set wide.
   No stock imagery anywhere in this product. */
.tile {
  flex: none; width: 182px; height: 96px;
  border-radius: 12px; background: var(--plate); color: var(--plate-ink);
  display: flex; align-items: flex-end; padding: 13px 14px;
  font-variation-settings: 'wdth' 116; font-weight: 600; font-size: 16px;
}
.tile:hover { outline: 2px solid var(--accent); outline-offset: 2px; }

.soon-slot {
  flex: none; width: 182px; height: 182px;
  border-radius: 12px; border: 1px dashed var(--hair-strong);
  display: grid; place-content: center; gap: 6px; text-align: center;
  color: var(--ink-2);
}

/* ───────────────────── player bar ───────────────────── */

.bar {
  grid-column: 2;
  border-top: 1px solid var(--hair);
  background: var(--paper);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 20px;
  padding: 0 24px;
}
.bar .np { display: flex; align-items: center; gap: 12px; min-width: 0; }
/* A flex item will not shrink below its content unless it is told it may, and
   without this the title pushes straight through the transport buttons. */
.bar .np > div { min-width: 0; }
/* The drawn cover is painted as this element's background and the artist's own
   picture loads on top of it. An <img> with no src still paints its background,
   which is what makes a rotted catalogue URL degrade to a cover rather than to
   the broken-image glyph. */
.bar .np #barArt {
  display: block; flex: none;
  width: 46px; height: 46px; border-radius: 8px;
  background: var(--sunk) center / cover no-repeat;
}
.bar .np .t { font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar .np .a { font-size: 12.5px; color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar .ctrl { display: flex; align-items: center; gap: 10px; }
.bar .rnd {
  width: 40px; height: 40px; border-radius: 50%;
  border: 1px solid var(--hair-strong);
  display: grid; place-items: center;
}
.bar .rnd:hover { border-color: var(--ink); }
.bar .rnd svg { width: 15px; height: 15px; fill: currentColor; }
.bar .auto {
  height: 40px; padding: 0 18px; border-radius: 999px;
  border: 1px solid var(--hair-strong);
  font-variation-settings: 'wdth' 70; text-transform: uppercase;
  letter-spacing: .14em; font-size: 11px; font-weight: 700;
}
/* A lamp, not a button: the mode is changed on the deck, in one place. */
.bar .auto.lamp { display: inline-flex; align-items: center; cursor: default; }
.bar .auto.on { background: var(--live); border-color: var(--live); color: #06231d; }
.bar .right { justify-self: end; display: flex; align-items: center; gap: 14px; }
.bar .clock { font-size: 12.5px; color: var(--ink-2); }

/* The bar splits while a blend runs: two titles, the real fader between them,
   and the reason underneath. It closes again when the blend ends. */
.bar.mixing { grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); }
.seambar { display: grid; grid-template-columns: minmax(0, 1fr) 120px minmax(0, 1fr); align-items: center; gap: 14px; width: 100%; }
.seambar .side { min-width: 0; }
.seambar .side.to { text-align: right; }
.seambar .fade { height: 3px; border-radius: 2px; background: var(--hair-strong); position: relative; }
.seambar .fade i { position: absolute; inset: 0 auto 0 0; width: 62%; background: var(--live); border-radius: 2px; }
.seambar .fade b {
  position: absolute; top: 50%; left: 62%; transform: translate(-50%, -50%);
  width: 11px; height: 11px; border-radius: 50%; background: var(--live);
}
.seambar .side p { line-height: 1.3; }
.under { text-align: center; font-size: 12.5px; color: var(--ink-2); padding-top: 8px; }
.under em { font-style: normal; color: var(--ink); }

/* ───────────────────── deck screen ───────────────────── */

/* The answer to a request, once, where the request was made. */
.asknote {
  flex-basis: 100%; margin: 6px 0 0; font-size: 12.5px; color: var(--live);
}

/* Two ways to want a record, side by side. */
.ask { display: inline-flex; gap: 4px; }
.pick.soon { font-size: 11px; letter-spacing: .04em; }

/* The column widens only while the strips are in it. */
.console.handson { grid-template-columns: minmax(0, 1fr) 210px minmax(0, 1fr) 258px; }

/* ── the manual surface ───────────────────────────────────────────────────────
   Everything here appears with the hand that would use it and is absent otherwise:
   a dimmed control is still a control asking to be pressed. */
.strips { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; width: 100%; }
/* Five knobs a side now that the filter is one of them, so they tighten rather than
   making the mixer column taller than the decks beside it. */
.strip .kn svg { width: 30px; height: 30px; }
.strip .kn .silk { font-size: 9px; }
.strip { display: grid; gap: 6px; justify-items: center; }
.kn { display: grid; justify-items: center; gap: 2px; cursor: ns-resize; touch-action: none; }
.kn svg { width: 34px; height: 34px; }
.kn circle { fill: var(--plate-2); stroke: var(--plate-hair); }
.kn line { stroke: var(--live); stroke-width: 2.6; stroke-linecap: round; }
.kn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }
/* Dead, not dim: a YouTube record cannot be filtered at all, and that is a fact about
   the record rather than a mode you can leave. */
.kn.dead { cursor: not-allowed; }
.kn.dead line { stroke: var(--plate-ink-2); opacity: .35; }
.kn.dead circle { opacity: .5; }

/* ── the deck's own controls ──────────────────────────────────────────────────
   Drawn in manual-mode.html from the start and built on 2026-08-16. Everything here
   appears with the hand that would use it, and anything that cannot act on THIS record
   is present, disabled and struck through rather than absent — three different reasons
   turn up on this panel and only a visible control can carry which one it is. */
.deck .src { margin: 0 0 6px; font-size: 10px; letter-spacing: .08em; }
.deckctl { display: grid; gap: 10px; margin-top: 10px; }
.platter { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 12px; align-items: center; }
/* The platter is drawn and deliberately does not turn: scratching is sample-level
   scrubbing and a new deck core, and a disc that span without moving the record would
   be the prettiest lie on the page. */
.platter .disc {
  width: 54px; height: 54px; border-radius: 50%; flex: none;
  background: radial-gradient(circle at 50% 50%, var(--plate-2) 0 22%, var(--plate) 23% 100%);
  border: 1px solid var(--plate-hair); position: relative;
}
.platter .disc::after {
  content: ''; position: absolute; left: 50%; top: 6px; width: 2px; height: 21px;
  background: var(--plate-ink-2); transform: translateX(-50%); border-radius: 1px;
}
.transport { display: grid; gap: 5px; }
.trow { display: flex; gap: 5px; }
.transport button, .loops button {
  flex: 1 1 0; min-width: 0; appearance: none; cursor: pointer; font: inherit;
  background: var(--plate-2); border: 1px solid var(--plate-hair); color: var(--plate-ink-2);
  border-radius: 7px; padding: 7px 4px; font-size: 11px;
}
.transport button:hover:not(:disabled), .loops button:hover:not(:disabled) { color: var(--plate-ink); }
.transport button[aria-pressed="true"], .loops button[aria-pressed="true"] {
  border-color: var(--live); color: var(--plate-ink);
}
/* Dead, not dim — and struck, so the eye can tell "not here" from "not yet". */
.transport button.dead, .loops button.dead, .kn.dead, .pitchtrack.dead { cursor: not-allowed; }
.transport button.dead, .loops button.dead { opacity: .4; }
.struck { position: relative; opacity: .4; cursor: not-allowed; }
.struck::after {
  content: ''; position: absolute; left: 8%; right: 8%; top: 50%; height: 1px;
  background: currentColor;
}

/* The pitch fader. Vertical and up is faster, the way it is on every turntable, and it
   rests in the MIDDLE — the one centred handle on this panel that is a truth rather
   than a default, because a record at its own speed is the middle of the travel. */
.pitch { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 10px; align-items: center; }
.pitchtrack {
  position: relative; width: 22px; height: 84px; border-radius: 11px;
  background: var(--plate-2); border: 1px solid var(--plate-hair);
  touch-action: none; cursor: ns-resize; display: block;
}
.pitchtrack::before {
  content: ''; position: absolute; left: 50%; top: 8px; bottom: 8px; width: 1px;
  background: var(--plate-hair); transform: translateX(-50%);
}
.pitchtrack b {
  position: absolute; left: 50%; width: 18px; height: 10px; border-radius: 3px;
  background: var(--plate-ink); transform: translate(-50%, -50%);
}
.pitchtrack.dead b { background: var(--plate-ink-2); opacity: .4; }
.pitchtrack:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.loops { display: flex; gap: 4px; }
.loops button { padding: 6px 2px; font-size: 10.5px; }

/* Mix now, and the two the design draws struck because they are BLOCKED rather than
   unbuilt: a browser has one output, and the licence enumerates no derivative-work
   right. Showing them says which is which; hiding them says nothing. */
.move { display: flex; gap: 5px; width: 100%; }
.move button {
  flex: 1 1 0; appearance: none; cursor: pointer; font: inherit; font-size: 11px;
  background: var(--plate-2); border: 1px solid var(--plate-hair); color: var(--plate-ink);
  border-radius: 7px; padding: 7px 4px;
}
.move #mixNowDeck { border-color: var(--live); }
.kn.wide svg { width: 30px; height: 30px; }

.pads { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-top: 10px; }
.pads button {
  appearance: none; background: var(--plate-2); border: 1px solid var(--plate-hair);
  color: var(--plate-ink-2); border-radius: 7px; padding: 7px 0; font: inherit;
  font-size: 11px; cursor: pointer;
}
.pads button:hover { color: var(--plate-ink); }
.pads button[aria-pressed="true"] { border-color: var(--live); color: var(--plate-ink); }

/* The fader is a readout under Beatomat and a control under a hand — the same element
   either way, so there is one truth about where the mix is. */
.fader.live .knob { cursor: ew-resize; }
.fader.live .track { cursor: ew-resize; }
/* A finger drag reaches the pointer handlers only if the browser is told not to treat
   it as a scroll first. The EQ knobs got this when they were built; the fader did not,
   so on a phone dragging it scrolled the page and the mix never moved. Only while it
   is a control — under Beatomat it is a readout and the page must scroll over it. */
.fader.live { touch-action: none; }
.fader .knob:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* The one switch, in the deck's header. Two positions, one word each, and the lit one
   is filled rather than merely outlined — state lives in the fill, the way the
   wordmark's O does. */
.modesw {
  display: inline-flex; gap: 2px; margin-left: auto;
  background: var(--sunk); border: 1px solid var(--hair-strong); border-radius: 999px; padding: 3px;
}
.modesw button {
  appearance: none; border: 0; background: none; cursor: pointer; font: inherit;
  font-size: 11.5px; letter-spacing: .09em; text-transform: uppercase; font-weight: 600;
  color: var(--ink-2); padding: 6px 13px; border-radius: 999px; white-space: nowrap;
}
.modesw button:hover { color: var(--ink); }
.modesw button[aria-pressed="true"] { background: var(--accent); color: var(--accent-ink); }

/* What the position means, in words, because a switch with two nouns on it still does
   not say what it changes. */
.modeline { padding: 0 32px 12px; margin: -8px 0 0; font-size: 13px; color: var(--ink-2); }

.setline { display: flex; align-items: center; gap: 14px; padding: 4px 32px 14px; flex-wrap: wrap; }
.setline h1 { min-width: 0; }
.credits {
  margin-left: auto; font-size: 12.5px; color: var(--ink-2);
  border: 1px solid var(--hair); border-radius: 999px; padding: 6px 13px;
}
.credits:hover { border-color: var(--ink-2); color: var(--ink); }

/* The recipe drawn: blocks on two lanes, overlapping exactly where the blends
   are, with the playhead walking across. */
.timeline { position: relative; height: 62px; margin: 0 32px 18px; border-radius: 12px; background: var(--sunk); overflow: hidden; }
.timeline .blk {
  position: absolute; height: 24px; border-radius: 6px;
  /* 22% read fine on paper and almost vanished with the lights off, where the
     card it mixes into is already nearly black. */
  background: color-mix(in srgb, var(--accent) 34%, var(--card));
  display: flex; align-items: center; padding: 0 8px;
  font-size: 10.5px; color: var(--ink-2); overflow: hidden; white-space: nowrap;
}
.timeline .blk.a { top: 6px; }
.timeline .blk.b { top: 32px; }
.timeline .blk.live { background: var(--accent); color: #fff; }
/* Behind the blocks: how much of the evening has already gone. */
.timeline .played {
  position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  background: color-mix(in srgb, var(--ink) 6%, transparent);
}
.timeline .head { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--live); }
.timeline .lanes { position: absolute; inset: 0; }

.console { display: grid; grid-template-columns: minmax(0, 1fr) 132px minmax(0, 1fr) 258px; margin: 0 32px; }

.deck { padding: 16px; display: grid; gap: 0; align-content: start; }
.deck .screen {
  aspect-ratio: 16 / 10; border-radius: 10px; background: var(--plate-2);
  position: relative; overflow: hidden; display: grid; place-items: center;
}
/* The sleeve is an opaque layer over the whole screen, not a picture sitting in
   a grid cell. Two reasons, and the second is the one that bit: a YouTube
   player lives in this same box, and if the artwork collapses — a cover whose
   URL has rotted is 20 pixels tall — an empty red YouTube player shows through
   on a track that has nothing to do with YouTube. The layer stays opaque
   whether or not the image ever arrives, and carries the mark when it does not. */
.deck .screen .sleeve {
  position: absolute; inset: 0; z-index: 1;
  background: var(--plate-2);
  background-size: cover; background-position: center;
  display: grid; place-items: center;
}
/* The mark belongs to a record that brought no picture — it is what we show
   INSTEAD of a sleeve, not a badge over somebody's artwork. The artwork is a
   background layer and a pseudo-element paints above a background, so without
   this the ring sits on top of every cover in the catalogue.
   The deck's drawn cover is asked for without its own ring (see deck-view.js),
   so the O on screen is always the live one below — the element that beats. */
.deck .screen .sleeve[data-cover="own"]::after { display: none; }
/* The artwork is a background layer on the sleeve now, not an element. There is
   nothing to lay out, nothing to take out of the layout, and no placeholder
   glyph that can ever be painted. */

/* The O, on its own. It is the mark before it is anything else, so a record
   with no sleeve still looks like it belongs to this machine rather than like
   a failure. */
.sleeve::after {
  content: '';
  /* Big, because this is the only thing on a record with no picture and it is
     across a room from whoever is listening. It was 46px and read as a small
     mark in a large empty panel. */
  width: 78px; height: 78px;
  border: 4px solid var(--live);
  border-radius: 50%;
  /* The fill IS the beat, and it is the loud half of the gesture — growing by a
     third is a nudge, flooding with green and glowing is something you see from
     the sofa. It is also the mark's own language: the wordmark's O says on-air
     by filling and never by changing colour. Here it says it at the tempo of
     the record. */
  background: color-mix(in srgb, var(--live) calc(var(--pulse-fill, 0) * 100%), transparent);
  box-shadow: 0 0 calc(var(--pulse-fill, 0) * 34px)
    color-mix(in srgb, var(--live) calc(var(--pulse-fill, 0) * 62%), transparent);
  opacity: .92;
  /* --pulse is written by the frame loop from the playhead and the tempo, so
     the ring swells on the beat and harder on the one. No transition and no
     animation: both would add a clock of their own between the music and the
     mark, and the whole point is that there is only one clock. It rests at 1
     whenever the deck is silent, has no tempo, or the visitor asked for less
     motion. */
  transform: scale(var(--pulse, 1));
  will-change: transform, background-color, box-shadow;
}
/* The player fills the screen when it is the thing being watched. */
.deck .screen > iframe, .deck .screen > div {
  position: absolute; inset: 0; width: 100%; height: 100%; border: 0;
}
.deck .onair, .deck .tag { z-index: 2; }
.deck .onair {
  position: absolute; left: 10px; top: 10px;
  display: flex; align-items: center; gap: 6px;
  background: var(--live); color: #06231d;
  font-variation-settings: 'wdth' 70; text-transform: uppercase;
  letter-spacing: .13em; font-size: 9.5px; font-weight: 700;
  padding: 4px 9px; border-radius: 999px;
}
.deck .tag { position: absolute; right: 10px; top: 10px; }
.deck .wave { height: 46px; margin-top: 12px; display: block; width: 100%; }
.deck .t { margin-top: 12px; font-size: 15px; font-weight: 600; }
.deck .a { font-size: 13px; color: var(--plate-ink-2); }
.deck .meta { margin-top: 8px; display: flex; gap: 14px; font-size: 11px; color: var(--plate-ink-2); }
.deck .prog { height: 2px; background: var(--plate-hair); margin-top: 12px; border-radius: 2px; }
.deck .prog i { display: block; height: 100%; background: var(--plate-ink-2); border-radius: 2px; }
.deck .times { display: flex; justify-content: space-between; margin-top: 7px; font-size: 11px; color: var(--plate-ink-2); }

/* The mixer reads as instruments, so it is the only column with no artwork in
   it: two meters, the fader, the countdown, and the six band lamps. */
.mixer { padding: 16px 10px; display: grid; align-content: start; justify-items: center; gap: 14px; border-left: 1px solid var(--plate-hair); border-right: 1px solid var(--plate-hair); }
.meters { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; width: 100%; }
.vu { height: 104px; border-radius: 4px; background: var(--plate-2); position: relative; overflow: hidden; }
.vu i { position: absolute; left: 0; right: 0; bottom: 0; height: 0; background: var(--live); }

.fader { position: relative; width: 100%; }
.fader .track { height: 4px; border-radius: 2px; background: var(--plate-2); position: relative; }
.fader .fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; background: var(--live); border-radius: 2px; }
.fader .knob {
  position: absolute; top: -6px; left: 50%;
  width: 14px; height: 16px; border-radius: 4px;
  background: var(--plate-ink); transform: translateX(-50%);
}
/* Nothing playing is not a crossfade at rest — the knob says so by going
   quiet rather than sitting mid-travel as if a mix were under way. */
.fader .knob.idle { background: var(--plate-ink-2); }
.fader .ends { display: flex; justify-content: space-between; margin-top: 8px; color: var(--plate-ink-2); }
.count { text-align: center; }
.count b { display: block; font-variation-settings: 'wdth' 74; font-size: 25px; font-weight: 700; line-height: 1.1; }
.lamps { display: grid; grid-template-columns: 1fr 1fr; gap: 5px 10px; width: 100%; }
.lamp {
  font-variation-settings: 'wdth' 68; text-transform: uppercase; letter-spacing: .1em;
  font-size: 9px; font-weight: 700; text-align: center;
  padding: 4px 0; border-radius: 5px;
  background: var(--plate-2); color: #565e55;
}
.lamp.on { background: color-mix(in srgb, var(--live) 20%, transparent); color: var(--live); }

/* What it's doing. The ring is the panel's pulse — it moves even when the
   engine has nothing to say — and a line is written only when a decision was
   actually taken. */
.doing { padding: 16px; border-left: 1px solid var(--plate-hair); display: grid; align-content: start; gap: 14px; }
.doing .hd { display: flex; gap: 13px; align-items: center; }
.doing .hd .now { font-size: 13.5px; margin-top: 3px; }
.feed { list-style: none; margin: 0; padding: 0; display: grid; gap: 11px; }
/* transform and opacity only: this runs continuously while audio plays, and
   anything that triggers layout is audible. */
@keyframes feed-in {
  from { transform: translateY(-6px); opacity: 0; }
  to { transform: none; opacity: 1; }
}
.feed li {
  display: grid; grid-template-columns: 1fr auto; gap: 10px;
  font-size: 12.5px; line-height: 1.4;
  animation: feed-in .45s cubic-bezier(.2, .8, .3, 1) both;
}
@media (prefers-reduced-motion: reduce) { .feed li { animation: none; } }
.feed li .b { font-variation-settings: 'wdth' 74; font-size: 10.5px; color: #4e564d; }
.feed li:nth-child(1) { color: var(--plate-ink); }
.feed li:nth-child(1) .k { color: var(--live); }
.feed li:nth-child(2) { color: #aab2a8; }
.feed li:nth-child(3) { color: #798077; }
.feed li:nth-child(4) { color: #565e55; }
.k { font-variation-settings: 'wdth' 68; text-transform: uppercase; letter-spacing: .12em; font-size: 9px; font-weight: 700; display: block; color: #565e55; margin-bottom: 2px; }

.whyline { margin: 16px 32px 0; font-size: 14px; text-align: center; color: var(--ink-2); }
.whyline em { font-style: normal; color: var(--ink); }
.whyline a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }

/* The bar leads to the deck, so it has to look like it does. Only when it is
   actually a control: applyBar drops the role when nothing is loaded. */
#barRest[role="button"], #barSeam[role="button"] { cursor: pointer; }
#barRest[role="button"]:focus-visible, #barSeam[role="button"]:focus-visible {
  outline: 2px solid var(--live); outline-offset: 3px; border-radius: 8px;
}

/* ------------------------- the queue, on the deck ------------------------ */

/* Two columns of the same width, because neither is subordinate: one is what
   you asked for and the other is what is offered. On a phone they stack, and
   Up next is first — your own list is not something you should have to scroll
   past suggestions to reach. */
.queuewrap { margin: 26px 32px 0; }
.queue { display: grid; grid-template-columns: 1fr; gap: 16px; }
@media (min-width: 900px) { .queue { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
/* The cold deck is one panel, full width. Two columns there would be the way
   in squeezed beside a box apologising for being empty. */
@media (min-width: 900px) { .queue.only { grid-template-columns: minmax(0, 1fr); } }

.queue .col { background: var(--card); border: 1px solid var(--hair); border-radius: 14px; overflow: hidden; }
.queue .col > header { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; padding: 14px 16px 10px; }
.queue .col h3 { margin: 0; font-size: 16px; font-weight: 700; }
.queue .col .sub { font-size: 12px; color: var(--ink-2); }
.queue .ai {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--live); border: 1px solid var(--live); border-radius: 999px; padding: 2px 8px;
}
.queue .yours { margin: 0; padding: 0 16px 12px; font-size: 12px; color: var(--ink-2); border-bottom: 1px solid var(--hair); }

/* The chips are over the suggestions only. Up next is your list: nothing
   filters it, so it has a sentence there instead of controls. */
.queue .dials { padding: 0 16px 12px; display: flex; flex-direction: column; gap: 7px; border-bottom: 1px solid var(--hair); }
/* One scrolling row per dial, not a wrapping block: the label on the left has
   to stay level with the choices it names, and a wrapped second line leaves it
   pointing at nothing. (The older .chips rule further down wraps, and is still
   used by the pages outside the shell.) */
.queue .chips { display: flex; flex-wrap: nowrap; align-items: center; gap: 7px; overflow-x: auto; scrollbar-width: none; }
.queue .chips::-webkit-scrollbar { display: none; }
.queue .chips .lbl { flex: none; width: 52px; font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2); }
.queue .chip {
  flex: none; font: inherit; font-size: 13px; white-space: nowrap; cursor: pointer;
  padding: 6px 13px; border-radius: 999px;
  background: var(--sunk); border: 1px solid var(--hair); color: var(--ink-2);
}
.queue .chip:hover { color: var(--ink); border-color: var(--hair-strong); }
.queue .chip[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); }
/* Autoplay is steering, so the chips cannot be pressed — and a control that is
   unpressable has to LOOK unpressable.
   Dimming alone was shipped first and was invisible: in the light theme an
   unpressed chip is already pale on pale, and 55% of that is not a state, it is
   the same chip. So the PILL goes rather than the colour — no fill, no border,
   nothing raised. A control that stops looking like a control is the signal,
   and it reads in both themes because it is a change of shape, not of value.
   The one in force keeps its pill, because once the row stops being buttons
   that is the only thing it is still for: reading what the machine chose. */
.queue .chip:disabled {
  cursor: default; background: transparent; border-color: transparent; color: var(--ink-2);
}
.queue .chip:disabled:hover { color: var(--ink-2); border-color: transparent; }
.queue .chip:disabled[aria-pressed="true"] {
  background: var(--ink); border-color: var(--ink); color: var(--paper);
}
.queue .chips.locked .lbl { opacity: .55; }
.queue .chip.toggle[aria-pressed="true"] { background: transparent; border-color: var(--live); color: var(--live); }
/* The switch that hands the choosing over sits in the header, not in the rows
   of choices it removes. */
.queue .chip.autoplay { margin-left: auto; }
.queue .chip.autoplay[aria-pressed="true"] { background: var(--live); border-color: var(--live); color: var(--plate); }
.queue .readout { margin: 3px 0 0; font-size: 12px; color: var(--live); }
.queue .readout b { font-weight: 400; color: var(--ink-2); }

.queue .rows { list-style: none; margin: 0; padding: 0 0 6px; max-height: 46vh; overflow-y: auto; }
.queue .rows li { display: flex; align-items: center; gap: 11px; padding: 9px 16px; }
.queue .rows li + li { border-top: 1px solid var(--hair); }
.queue .rows .rowart { width: 34px; height: 34px; border-radius: 6px; flex: none; background-size: cover; background-position: center; }
.queue .lines { flex: 1; min-width: 0; display: block; }
.queue .lines .t, .queue .lines .m { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.queue .lines .t { font-size: 14px; }
.queue .lines .m { font-size: 12px; color: var(--ink-2); margin-top: 2px; }
.queue .dur { font-size: 12px; color: var(--ink-2); flex: none; }

/* Green is a record we decode and can hold against another for thirty-two
   bars; brick is one whose audio lives in an iframe and can only be faded.
   Nobody is told the list is mixed — it looks mixed. */
.queue .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.queue .dot.cat { background: var(--live); }
.queue .dot.yt { background: #c2705c; }

.queue .prev {
  width: 28px; height: 28px; flex: none; border-radius: 50%;
  border: 1px solid var(--hair-strong); background: none; color: var(--ink-2); cursor: pointer;
  display: grid; place-items: center; padding: 0;
}
.queue .prev svg { width: 10px; height: 10px; fill: currentColor; }
.queue .prev:hover { color: var(--ink); border-color: var(--ink); }
.queue .prev.playing { border-color: var(--live); color: var(--live); }
/* A control names the next action, not the current state: while it plays it
   offers a pause, or the only way to tell it is running is to listen. */
.queue .prev .i-pause, .queue .prev.playing .i-play { display: none; }
.queue .prev.playing .i-pause { display: block; }

/* The record the idle deck is already holding. */
.queue li.cued { background: color-mix(in srgb, var(--live) 7%, transparent); }
.queue .ondeck {
  margin-left: 8px; font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--live); border: 1px solid var(--live); border-radius: 999px; padding: 1px 7px;
  white-space: nowrap;
}
/* The place a preview would be, kept empty rather than collapsed: a row with
   no Deezer match must not sit one notch left of every other row. */
.queue .prev.empty { border-color: transparent; }

.queue .pick {
  width: 28px; height: 28px; flex: none; border-radius: 50%; cursor: pointer;
  border: 0; background: var(--accent); color: var(--accent-ink);
  font: inherit; font-size: 17px; line-height: 1; padding: 0;
}
.queue .pick:hover { filter: brightness(1.12); }

.queue .rowctl { display: flex; gap: 3px; flex: none; opacity: .5; transition: opacity .12s; }
.queue .rowctl button {
  width: 26px; height: 26px; border-radius: 7px; border: 1px solid transparent;
  background: none; color: var(--ink-2); font: inherit; cursor: pointer; padding: 0;
}
.queue .rowctl button:hover:not(:disabled) { color: var(--ink); border-color: var(--hair-strong); }
.queue .rowctl button:disabled { opacity: .35; cursor: default; }

/* The panel a person drives from. Its rows carry words rather than a glyph — Load A,
   Load B, or `on air` — so the round + this app uses everywhere else has to grow into
   a pill. The refused one keeps its place at the same width: a button that shrinks
   when it goes dead makes the row twitch at every seam, and the whole reason it is
   still on screen is that the reason should be readable. */
/* Beatomat's one list. The ask sits above the queue and has to READ as a different
   thing from it, or a shortlist of candidates is mistaken for what is going to play —
   which is the whole reason both halves carry a band with their name on it. */
/* The surface while Beatomat has it: every control still on screen and still MOVING —
   the bands swap, the filter sweeps, the fader travels, the nudge bends the pitch —
   and none of it reachable. Not "dimmed": a dimmed control reads as one that could be
   pressed if something changed. This reads as an instrument panel, which is what it is
   in this mode, and the engine refuses every one of them independently. */
.watching { cursor: default; }
.watching button, .watching [role="slider"], .watching .kn, .watching .pitchtrack {
  pointer-events: none;
}
.watching button:not(.struck), .watching .lbl { opacity: .78; }
.fader.watching .knob { cursor: default; }

.ask-open { margin-left: auto; }
.band {
  margin: 16px 16px 6px; padding-top: 10px; border-top: 1px solid var(--hair);
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2);
}
.asking { background: color-mix(in srgb, var(--ink) 3%, transparent); padding-top: 12px; }
.asking .opening-search { margin: 0 16px 12px; }
.asking .band:first-of-type { border-top: 0; padding-top: 0; }
/* The reason a record is in the list, under its own row rather than in a column of its
   own: at this width a fourth column squeezes the title, and the title is what somebody
   is reading. */
.queue .lines .why { display: block; font-size: 11px; color: var(--ink-2); margin-top: 2px; }

/* Every row control that carries a WORD rather than the round + this app uses
   everywhere else: Next and Soon on the ask, Load A / Load B and `on air` on the
   driver's panel. They were all 28px circles, so on a phone "Soon" spilled out of its
   own button and over the one beside it. */
.queue .ask .pick {
  width: auto; height: 26px; border-radius: 999px; padding: 0 10px;
  font-size: 11px; letter-spacing: .04em; text-transform: uppercase; font-weight: 600;
}
/* The refused Load keeps its place at the same width: a button that shrinks when it
   goes dead makes the row twitch at every seam, and the whole reason it is still on
   screen is that the reason should be readable. */
#colFind .pick { min-width: 58px; }
#colFind .pick:disabled {
  background: none; color: var(--ink-2); border: 1px solid var(--hair-strong);
  cursor: default; filter: none;
}
#colFind .opening-search { margin: 0 16px 14px; }
#colFind .note { padding: 0 16px; }
/* The line about the queue being kept sits at the BOTTOM here, under the records,
   because it explains an absence and an explanation of an absence read before you
   have seen the panel is an answer to a question nobody has asked yet. */
#colFind .yours { margin-top: 14px; border-top: 1px solid var(--hair); padding-top: 12px; }
.queue li:hover .rowctl, .queue .rowctl:focus-within { opacity: 1; }
/* A control that is always visible on a device with no hover. */
@media (hover: none) { .queue .rowctl { opacity: 1; } }

.queue .empty { padding: 20px 16px 22px; text-align: center; }
.queue .empty p { margin: 0; font-size: 13px; color: var(--ink-2); line-height: 1.55; }
.queue .empty .big { color: var(--ink); font-size: 15px; margin-bottom: 6px; }
.queue .note { margin: 0; padding: 16px; font-size: 13px; color: var(--ink-2); }

/* The promise, and only when there is one to keep. */
.queue .flowing { padding: 0 16px 14px; }
.queue .strip {
  margin: 0; border: 1px dashed var(--live); border-radius: 9px;
  padding: 9px 12px; font-size: 12px; color: var(--live);
}

.queue .skeleton { padding: 6px 16px 12px; }

#deckEmpty a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
#deckEmpty b { font-weight: 700; color: var(--ink); }

/* Manual only. One row that appears and disappears; the page does not
   re-lay-out around it. */
.actions { margin: 26px 32px 0; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.seg { display: flex; align-items: center; border: 1px solid var(--hair); border-radius: 10px; overflow: hidden; }
.seg button { padding: 8px 13px; font-size: 13px; color: var(--ink-2); }
.seg button:hover { background: var(--sunk); color: var(--ink); }
.seg button[aria-pressed="true"] { background: var(--accent); color: var(--accent-ink); }
.seg .lbl { padding: 0 11px; }

/* ───────────────────── plans ───────────────────── */

/* Yearly first, priced per month, the way every subscription is sold — and
   the saving stated as a number rather than implied. */
.billing { display: flex; align-items: center; gap: 10px; margin: 20px 32px 0; }
.billing .sw { display: flex; border: 1px solid var(--hair); border-radius: 999px; padding: 3px; }
.billing .sw button { padding: 7px 16px; border-radius: 999px; font-size: 13px; color: var(--ink-2); }
.billing .sw button:hover { color: var(--ink); }
.billing .sw button[aria-pressed="true"] { background: var(--ink); color: var(--paper); font-weight: 600; }
.billing .save {
  font-variation-settings: 'wdth' 70; text-transform: uppercase; letter-spacing: .12em;
  font-size: 10px; font-weight: 700; color: var(--accent);
  border: 1px solid var(--accent); border-radius: 999px; padding: 4px 10px;
}

.plans { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin: 16px 32px 0; max-width: 760px; }
.plan-card { border: 1px solid var(--hair); border-radius: 16px; padding: 22px; background: var(--card); }
.plan-card.paid { border-color: var(--accent); }
.plan-card h3 { margin: 0; font-size: 17px; font-variation-settings: 'wdth' 116; font-weight: 600; }
.plan-card .price { margin: 12px 0 2px; font-variation-settings: 'wdth' 74; font-size: 32px; font-weight: 700; }
.plan-card .price small { font-size: 15px; font-weight: 400; color: var(--ink-2); margin-left: 3px; }
.plan-card .per { font-size: 13px; color: var(--ink-2); min-height: 19px; }
.plan-card ul { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 9px; font-size: 14px; }
.plan-card li { display: grid; grid-template-columns: 16px 1fr; gap: 9px; align-items: start; }
.plan-card li svg { width: 15px; height: 15px; margin-top: 3px; stroke: var(--accent); fill: none; stroke-width: 2.2; }
.plan-card li.off { color: var(--ink-2); }
.plan-card li.off svg { stroke: var(--ink-2); }
/* One button, wherever it appears. These used to be scoped to a plan card,
   which is how the sign-in button came out transparent. */
.cta {
  display: block; text-align: center; padding: 12px; border-radius: 10px;
  font-weight: 600; font-size: 14.5px;
}
.cta.solid { background: var(--accent); color: var(--accent-ink); }
.cta.solid:hover { filter: brightness(1.12); }
.cta.quiet { border: 1px solid var(--hair-strong); color: var(--ink-2); }
.cta.quiet:hover { color: var(--ink); }
.cta[disabled] { opacity: .5; cursor: default; }
.plan-card .cta { margin-top: 20px; }

.note { margin: 26px 32px 0; max-width: 720px; font-size: 13.5px; color: var(--ink-2); }

/* The keep row: one field and a button, opened by Save. A set worth keeping is
   worth naming — a list of "Cool Deep House" four times over is a list nobody
   can read. */
.keeprow {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 32px 16px;
}
.keeprow input {
  flex: 1; min-width: 0; max-width: 340px; height: 40px;
  border-radius: 10px; border: 1px solid var(--hair);
  background: var(--card); color: var(--ink); padding: 0 13px; font: inherit;
}
.keeprow input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.keeprow .cta { padding: 11px 20px; }
.keeprow .note { flex: 1 1 100%; margin: 0; font-size: 13px; }
.keeprow .link {
  color: var(--ink-2); font-size: 13.5px;
  text-decoration: underline; text-underline-offset: 3px;
}
.results .link { color: var(--ink-2); font-size: 12.5px; text-decoration: underline; text-underline-offset: 3px; }
.results .link:hover { color: var(--ink); }

/* ───────────────────── the credits panel ─────────────────────
   It slides in over the deck rather than appending to the bottom of the page,
   which is what it used to do: press the button, nothing appears to happen,
   and the obligation is technically met and practically invisible. */

.scrim {
  position: fixed; inset: 0; z-index: 20;
  background: color-mix(in srgb, #000 38%, transparent);
}
.creditpanel {
  position: fixed; z-index: 21;
  top: 0; right: 0; bottom: 0; width: min(400px, 100%);
  background: var(--card);
  border-left: 1px solid var(--hair);
  padding: 22px 24px calc(24px + 88px);
  overflow: auto;
  display: flex; flex-direction: column; gap: 12px;
}
.creditpanel-head { display: flex; align-items: center; gap: 12px; }
.creditpanel-head h2 { font-size: 20px; }
.creditpanel-head button {
  margin-left: auto; width: 32px; height: 32px; border-radius: 50%;
  border: 1px solid var(--hair-strong); color: var(--ink-2); font-size: 17px; line-height: 1;
}
.creditpanel-head button:hover { color: var(--ink); border-color: var(--ink-2); }
.creditpanel .hint { font-size: 13px; color: var(--ink-2); }
.creditpanel-list { display: grid; gap: 2px; }
.creditpanel-list .c {
  display: grid; gap: 2px;
  padding: 12px 0; border-top: 1px solid var(--hair);
}
.creditpanel-list .c b { font-size: 14.5px; font-weight: 600; }
.creditpanel-list .c .t { font-size: 13px; color: var(--ink-2); }
.creditpanel-list .c a {
  font-size: 12.5px; color: var(--accent);
  text-decoration: underline; text-underline-offset: 3px; justify-self: start;
}

@media (max-width: 760px) {
  /* A bottom sheet on a phone: a 400px drawer on a 390px screen is just a
     second page with a shadow. */
  .creditpanel {
    top: auto; left: 0; width: 100%; max-height: 72vh;
    border-left: 0; border-top: 1px solid var(--hair);
    border-radius: 16px 16px 0 0;
    padding-bottom: calc(24px + 88px + env(safe-area-inset-bottom));
  }
}

/* A kept set is drawn the way every other set is drawn — same card, same
   cover, same two lines — with the one control that only applies here under
   it. The profile inventing its own row was what made the list look unfinished
   next to the rest of the app. */
.kept { display: grid; gap: 6px; justify-items: start; }
.kept .card { width: 100%; }
.kept .link {
  color: var(--ink-2); font-size: 12.5px;
  text-decoration: underline; text-underline-offset: 3px;
}
.kept .link:hover { color: var(--ink); }

/* ───────────────────── search results ───────────────────── */

.results { list-style: none; margin: 0 32px; padding: 0; max-width: 760px; }
.results li {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 14px;
  align-items: center; border-top: 1px solid var(--hair);
}
.results button { width: 100%; text-align: left; padding: 11px 0; }
.results button:hover .t { color: var(--accent); }
.results .t { display: block; font-size: 14.5px; }
.results .why { display: block; font-size: 12.5px; color: var(--ink-2); }
.results .len { color: var(--ink-2); }

/* The picture on a row you are choosing from. A background rather than an
   <img> — see cover.js: a layer that fails simply does not paint, and this
   record's own drawn cover is already underneath it, so a row is never a hole.
   Square, because every cover in this app is square; a 16:9 YouTube thumbnail
   is cropped to its middle, and that is the accepted cost. */
/* Scoped to the row's own text button and the builder's row body — NOT to
   `.results button`, which also catches the round Add control and the reorder
   arrows, whose glyphs are centred by text-align and slide to the left edge
   the moment a stray `display: flex` reaches them. */
.results .row, .results li > div { display: flex; align-items: center; gap: 12px; }
.results li > div { padding: 11px 0; }
.rowart {
  flex: none; width: 48px; height: 48px; border-radius: 6px;
  background-color: var(--hair); background-size: cover; background-position: center;
}
/* min-width: 0 is what lets a long YouTube title be cut instead of pushing the
   Add button off the end of the row. */
.lines { min-width: 0; flex: 1; }
.results .t, .results .why {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The wait, drawn: rows in the geometry of the answers, so the page commits to
   an answer and shows where it will land. CSS only — nothing runs per frame
   for a wait that is usually under a second. */
.skeleton .rowart, .skeleton .bar {
  background-color: var(--hair);
  animation: skelpulse 1.4s ease-in-out infinite;
}
.skeleton .bar { display: block; height: 10px; border-radius: 3px; }
.skeleton .bar.t { width: 58%; margin-bottom: 8px; }
.skeleton .bar.why { width: 32%; height: 9px; }
@keyframes skelpulse { 0%, 100% { opacity: .3; } 50% { opacity: .7; } }
@media (prefers-reduced-motion: reduce) {
  .skeleton .rowart, .skeleton .bar { animation: none; opacity: .45; }
}

/* ───────────────────── signing in ─────────────────────
   Deliberately narrow and deliberately quiet: an account is not the point of
   this product, it is what it takes to keep a set. */

.signin { max-width: 380px; margin: 8px 32px 0; }
/* A profile is not a login form. 380px is a column cut for two fields stacked
   and a button under them; the profile's paragraph is prose, and prose set to a
   form's width next to a full-width shelf of sets reads as a column that got
   stuck. Its buttons sit side by side rather than filling the column. */
.signin.profile { max-width: 620px; }
.signin.profile .hint { max-width: 46ch; }
.signin.profile .row .cta { width: auto; min-width: 150px; }
.signin .hint { font-size: 13.5px; color: var(--ink-2); margin-bottom: 18px; }
.signin .hint.small { margin: 6px 0 0; font-size: 12.5px; }
.signin .who { font-size: 19px; font-weight: 600; margin: 4px 0 14px; }
.signin form { display: grid; gap: 6px; }
.signin label { margin-top: 12px; }
.signin input {
  height: 42px; border-radius: 10px;
  border: 1px solid var(--hair); background: var(--card); color: var(--ink);
  padding: 0 13px; font: inherit; font-size: 15px;
}
.signin input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.signin .cta { margin-top: 18px; border: 0; width: 100%; }
.signin .row { display: flex; gap: 10px; }
.signin .row .cta { margin-top: 0; }
.signin .row .cta.quiet { border: 1px solid var(--hair-strong); }
.signin .err {
  background: var(--sunk); border-radius: 10px; padding: 10px 13px;
  font-size: 13.5px; margin-bottom: 14px;
}
/* A link that is a button, because it changes what this screen is rather than
   where you are. */
.signin .link {
  color: var(--accent); text-decoration: underline; text-underline-offset: 3px;
  font-size: 13.5px; padding: 0;
}

/* ───────────────────── room lights ───────────────────── */

/* Top right of every screen, in the sticky row the search field lives in. */
.roomsw { display: flex; align-items: center; gap: 9px; margin-left: auto; }
.roomsw .silk { padding: 0; }
.roomsw .sw {
  display: flex; gap: 2px;
  border: 1px solid var(--hair); border-radius: 999px; padding: 2px;
  background: var(--card);
}
.roomsw button {
  width: 26px; height: 22px; border-radius: 999px;
  display: grid; place-items: center; color: var(--ink-2);
}
.roomsw button svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; }
.roomsw button:hover { color: var(--ink); }
.roomsw button[aria-pressed="true"] { background: var(--ink); color: var(--paper); }

/* ───────────────────── narrower than a desk ─────────────────────
   Two steps down. At a laptop the rail keeps its icons and the decision panel
   is what goes, because it is the widest thing that is not an instrument. On a
   phone the rail stops being a column at all: it becomes the bottom bar, the
   player sits above it, and the console stacks — two decks side by side inside
   390px are two decks nobody can read. */

@media (max-width: 1180px) {
  .app { grid-template-columns: 64px minmax(0, 1fr); }
  .rail .item span, .rail .silk, .rail .acct, .mark span { display: none; }
  .rail .item { justify-content: center; }
  .mark { justify-content: center; padding: 2px 0 20px; }
  .roomsw .silk { display: none; }
  .console { grid-template-columns: minmax(0, 1fr) 120px minmax(0, 1fr); }
  /* And with the strips in it. Without this the decision panel's 258px column was
     still being reserved on a laptop — for an element that is not displayed at this
     width — so at 900px each deck got 164px and its title ran under the knobs. */
  .console.handson { grid-template-columns: minmax(0, 1fr) 210px minmax(0, 1fr); }
  .doing { display: none; }
  .pad, .setline, .timeline, .console, .upnext, .actions, .plans,
  .note, .billing, .signin, .results, .shelf, .shelf-head, .find, .grid,
  .keeprow { margin-left: 20px; margin-right: 20px; }
  .find { padding-left: 0; padding-right: 0; }
  .shelf, .shelf-head { padding-left: 0; padding-right: 0; }
}

@media (max-width: 760px) {
  .app {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto auto;
  }

  /* The rail becomes the bottom bar. The sources are hidden rather than
     squeezed in: every one of them currently leads to the same list, so a
     phone loses nothing by not showing four ways to reach it. */
  .rail {
    grid-row: 3; grid-column: 1;
    flex-direction: row; align-items: center; justify-content: space-around;
    border-right: 0; border-top: 1px solid var(--hair);
    padding: 6px 8px calc(6px + env(safe-area-inset-bottom));
    gap: 0; overflow: visible;
  }
  .rail .mark, .rail hr, .rail .grow, .rail .acct,
  .rail .item[data-view="catalog"], .rail .item[data-view="known"],
  .rail .item.soon { display: none; }
  /* The nav wrappers stop being boxes so every tab is a direct child of the
     bar and they space out as one row. Otherwise two groups get half the width
     each and four icons sit in two clumps. */
  .rail nav { display: contents; }
  /* width:100% is right for a column and wrong for a bar: it stretched the
     account tab across everything the nav did not take. */
  .rail .item { padding: 8px 16px; margin: 0 !important; width: auto; }
  .rail .item svg { width: 21px; height: 21px; }

  .bar {
    grid-row: 2; grid-column: 1; padding: 0 14px; gap: 10px;
    grid-template-columns: minmax(0, 1fr) auto;
  }
  /* The set name takes the row; the length and Credits share the next one.
     Credits pulls left against the meta rather than floating alone. */
  /* Eleven blocks across 350px is about thirty pixels each — enough for a
     shape, not for a word. The names cut to "Obe" and "Enam" are noise, and
     the shape is the information anyway. */
  .timeline .blk { font-size: 0; padding: 0 2px; }
  .setline { padding-bottom: 10px; gap: 6px 12px; }
  .setline h1 { flex: 1 1 100%; }
  .setline .credits { margin-left: auto; }
  .setline .silk:empty { display: none; }
  .bar .np #barArt { width: 40px; height: 40px; }
  .bar .rnd { width: 36px; height: 36px; }
  .bar .auto { height: 36px; padding: 0 13px; }
  /* The clock is the first thing to go: the bar's job on a phone is the record
     and the transport. */
  .bar .right { display: none; }
  /* And on a phone the mode lamp goes with it. It is a READ-ONLY duplicate of the
     switch in the deck's header two centimetres above, and it was costing the one
     thing the bar exists to say: at 320px the title had been squeezed to "N". */
  .bar .auto.lamp { display: none; }
  .seambar { grid-template-columns: minmax(0, 1fr) 64px minmax(0, 1fr); gap: 8px; }
  .seambar .under { font-size: 11.5px; }

  .find { gap: 8px; padding-top: 12px; padding-bottom: 12px; }
  .find input { max-width: none; }

  /* The starter block stacks, and its covers become a strip under the text
     rather than a column fighting it for width. */
  .starter { grid-template-columns: minmax(0, 1fr); gap: 16px; padding: 18px; }
  .starter h1 { font-size: 22px; }
  .starter .go { width: 100%; justify-content: center; }
  .starter .covers { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .starter .covers img { width: 100%; height: auto; aspect-ratio: 1; }

  h1.wide { font-size: 22px; }
  h2.wide { font-size: 17px; }

  .card, .card .cover, .tile, .soon-slot { width: 146px; }
  .card .cover, .soon-slot { height: 146px; }
  .tile { height: 84px; font-size: 14px; }

  /* One deck above the other, with the mixer as a row between them. */
  .console { grid-template-columns: minmax(0, 1fr); }
  .mixer {
    border-left: 0; border-right: 0;
    border-top: 1px solid var(--plate-hair); border-bottom: 1px solid var(--plate-hair);
    grid-template-columns: auto 1fr auto; align-items: center;
    display: grid; gap: 16px; padding: 14px 16px; justify-items: stretch;
  }
  .mixer { grid-template-columns: minmax(0, 1fr); justify-items: stretch; }
  .vu { height: 34px; }
  .count b { font-size: 21px; }

  .plans { grid-template-columns: minmax(0, 1fr); }
  .signin { max-width: none; }
  .actions { gap: 8px; }
  .actions .seg button { padding: 8px 11px; font-size: 12.5px; }

  /* ── the manual surface, on a phone ──────────────────────────────────────
     The console is four columns — deck, mixer, deck, strips — and `.handson`
     outranks the one-column `.console` above it, so it never had a phone rule
     at all. At 375px that laid the channel strips straight over deck A's
     title and the bars-to-the-mix number over its cue pads. Everything else
     here follows from the same width: the surface shipped 2026-08-13 and was
     only ever looked at on a desktop. */
  .console.handson { grid-template-columns: minmax(0, 1fr); }

  /* A cue pad is the one control on this screen a finger chases mid-record,
     and four of them across a 76px strip were 15px wide. */
  .pads { gap: 7px; }
  .pads button { padding: 11px 0; font-size: 13px; }

  /* `.seg` clips rather than scrolls, so the two effects at the end of the
     rack — Crush and Reverse — were not merely off screen, they were
     unreachable, with nothing saying so. It wraps to a second line instead. */
  #fxRow { flex-wrap: wrap; }
  .actions .seg button { min-width: 32px; }

  /* Find a record: a dot plus two pill buttons left about a hundred pixels for
     the title, so every row on the screen you choose a record from read "NG…".
     The two buttons take a line of their own under it. */
  #colFind .rows li, #sugRows li { flex-wrap: wrap; }
  #colFind .rows li .ask, #sugRows li .ask { width: 100%; justify-content: flex-end; }
  #sugRows .lines .t, #sugRows .lines .m { white-space: normal; overflow: visible; text-overflow: clip; }

  /* Up next: the marks that say what is about to happen — On deck, and where
     the blend enters — sit inside a nowrap, ellipsised title, so on a phone
     they were not on the screen at all. The title wraps instead, and the
     duration goes: the header already says how long the whole list is. */
  /* Wrapping alone was not enough: `.t` still carried overflow:hidden and an
     ellipsis, so the entry clause came out as "e…" followed by "at 0:01 · 32
     bars" on the line under it. The clause gets a line of its own instead —
     it is a sentence about the next few minutes, not a suffix to a title. */
  .queue .lines .t { white-space: normal; overflow: visible; text-overflow: clip; }
  /* And the readings under it. "VELVET SWEEP · 128 B…" drops the key, on the one
     screen in this app that exists to say which records go together. */
  .queue .lines .m { white-space: normal; overflow: visible; text-overflow: clip; }
  .queue .rows li .t .entry { display: block; margin-left: 0; margin-top: 3px; }
  /* A 4px rail with a 14px handle is a mouse target. On a phone the whole fader
     becomes the thing you grab. */
  .fader .track { height: 6px; border-radius: 3px; }
  .fader .knob { top: -11px; width: 20px; height: 28px; border-radius: 6px; }
  .fader { padding: 10px 0; }

  /* One scroll on a phone. A 46vh box inside a page that also scrolls means the
     buttons on the last visible row are cut off by an edge nothing draws. */
  .queue .rows { max-height: none; }
  .queue .rows li { gap: 9px; }
  .queue .dur { display: none; }

  /* The ask is the only way anything gets proposed under Beatomat, so it does
     not get pushed onto a line of its own against the right-hand edge. */
  .ask-open { margin-left: 0; width: 100%; }
}

/* ───────────────────── starting a set ─────────────────────
   Two stages on one address. The first is a single decision and two doors; the
   second is a workbench — records on the left, your set on the right. */

.starter-new { margin: 8px 32px 0; max-width: 720px; }
.starter-new .row { display: flex; gap: 10px; margin-top: 18px; flex-wrap: wrap; }
.starter-new .cta { display: inline-flex; align-items: center; gap: 8px; }
.starter-new .cta svg { width: 15px; height: 15px; fill: currentColor; }

/* A button that replaces something has to say what it replaces, and the line is
   part of the control rather than a legend somewhere below it — the choice is
   made at the button, so the consequence belongs there too. */
.cta .under {
  display: block; font-size: 11.5px; opacity: .72; font-weight: 400;
  letter-spacing: 0; margin-top: 2px;
}
.cta:has(.under) {
  display: inline-flex; flex-direction: column; align-items: flex-start;
  gap: 0; padding-top: 9px; padding-bottom: 9px; height: auto; text-align: left;
}
.cta:has(.under) > span:first-child { display: inline-flex; align-items: center; gap: 8px; }
.picked .row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
.picked .row .cta { margin-top: 0; }
.starter-new .hint.small { margin-top: 14px; max-width: 54ch; }

/* The genres. A chip is a filter, not a button that does something: it says
   what the list under it is, and exactly one is ever pressed. */
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.chip {
  border: 1px solid var(--hair-strong); background: var(--card); color: var(--ink);
  border-radius: 999px; padding: 7px 14px; font: inherit; font-size: 13px;
  cursor: pointer;
}
.chip:hover { border-color: var(--accent); }
.chip[aria-pressed="true"] {
  background: var(--accent); border-color: var(--accent); color: #06231d; font-weight: 600;
}

.builder {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 28px; margin: 8px 32px 0; align-items: start;
}
.builder h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .12em;
  color: var(--ink-2); font-variation-settings: 'wdth' 70; margin-bottom: 4px; }
/* The lists scroll, and on this platform the scrollbar is an OVERLAY: it takes
   no width in the layout and paints straight over whatever sits at the right
   edge — which is the Add control and the reorder arrows. The padding is a lane
   kept clear for it; scrollbar-gutter holds the same lane on the platforms
   where the bar does take width, so the rows do not jump when one appears. */
.builder .results {
  margin: 12px 0 0; max-width: none; max-height: 52vh;
  overflow-y: auto; padding-right: 14px; scrollbar-gutter: stable;
}
.builder .note { margin: 14px 0 0; }

.findrow { display: flex; gap: 8px; margin-top: 16px; }
.findrow input {
  flex: 1; min-width: 0; height: 40px; border-radius: 10px;
  border: 1px solid var(--hair); background: var(--card); color: var(--ink);
  padding: 0 13px; font: inherit; font-size: 14px;
}
.findrow input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.findrow .cta { margin: 0; width: auto; padding: 0 16px; height: 40px; }

/* One round control per row, because "add this" is one action. Disabled means
   it is already in the set — a tick, not a plus, so the row explains itself. */
.results .add {
  width: 30px; height: 30px; border-radius: 50%; padding: 0;
  border: 1px solid var(--hair-strong); background: var(--card); color: var(--ink);
  font-size: 15px; line-height: 1; cursor: pointer; text-align: center;
  /* Centred by the box itself rather than by text-align, so no rule elsewhere
     can knock the glyph off the middle of the circle. */
  display: inline-flex; align-items: center; justify-content: center;
}
.results .add:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.results .add:disabled { opacity: .45; cursor: default; border-style: dashed; }

/* Reorder and remove. Arrows rather than drag-and-drop: they work with a
   keyboard, they work on a phone, and they need no pointer choreography. */
.rowctl { display: flex; gap: 2px; align-items: center; }
.rowctl button {
  width: 27px; height: 27px; padding: 0; border-radius: 7px;
  border: 1px solid transparent; background: none; color: var(--ink-2);
  font-size: 13px; line-height: 1; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.rowctl button:hover:not(:disabled) { border-color: var(--hair-strong); color: var(--ink); }
.rowctl button:disabled { opacity: .3; cursor: default; }

.picked .cta { margin-top: 18px; display: inline-flex; align-items: center; gap: 8px; }
.picked .cta svg { width: 15px; height: 15px; fill: currentColor; }
.picked .cta:disabled { opacity: .45; cursor: default; }
.results.picks li { counter-increment: pick; }
.results.picks .t::before {
  content: counter(pick) '. '; color: var(--ink-2); font-variant-numeric: tabular-nums;
}
.results.picks { counter-reset: pick; }

/* A phone has one column, and the set you are building goes under the records
   you are choosing from — the same order as the reading. */
@media (max-width: 900px) {
  .builder { grid-template-columns: 1fr; gap: 20px; }
  .builder .results { max-height: none; }
}
@media (max-width: 980px) {
  .starter-new, .builder { margin-left: 20px; margin-right: 20px; }
}

/* A record chosen under a crate that has since changed. Dimmed rather than
   marked in a colour: it is not a warning, it is a fact about when it was
   picked, and Up next never drops or reorders it. */
.queue .rows li.promised { opacity: .55; }
.queue .rows li.promised .t .promised {
  font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase;
  font-weight: 650; margin-left: 6px; opacity: .8;
}

/* What the engine will do at the seam about to happen. Quiet: it is a fact
   about the next few minutes, not a control. */
.queue .rows li .t .entry {
  font-size: 10.5px; letter-spacing: .02em; font-weight: 550;
  margin-left: 8px; opacity: .75;
}

/* ── opening a cold deck ──────────────────────────────────
   The panel that replaces the shortlist while nothing is on the decks. It
   asks for a record rather than a genre, so the artwork is the largest thing
   on it: recognising the record at a glance is the whole function of the
   shelf, not decoration. */
.opening { padding: 0 16px 16px; }
.opening .lede { margin: 0 0 14px; font-size: 13px; color: var(--ink-2); line-height: 1.5; }

.opening-search {
  display: flex; align-items: center; gap: 10px;
  border: 1px solid var(--hair-strong); border-radius: var(--r-ctl, 10px);
  padding: 0 12px; margin-bottom: 14px;
}
.opening-search svg { flex: none; width: 15px; height: 15px; fill: none; stroke: var(--ink-2); stroke-width: 2; }
.opening-search input {
  flex: 1; min-width: 0; font: inherit; font-size: 14px; padding: 11px 0;
  border: 0; background: none; color: var(--ink); outline: none;
}
.opening-search input::placeholder { color: var(--ink-2); }

.opening .searching { margin: -6px 0 14px; font-size: 13px; color: var(--ink-2); }
.opening .found { display: flex; flex-direction: column; gap: 6px; margin: -6px 0 14px; }
.opening .found .result {
  display: flex; gap: 10px; align-items: baseline; text-align: left; cursor: pointer;
  font: inherit; font-size: 14px; padding: 10px 13px; min-width: 0;
  border: 1px solid var(--hair); border-radius: var(--r-ctl, 10px);
  background: var(--card); color: var(--ink);
}
.opening .found .result:hover { border-color: var(--hair-strong); }
.opening .found .result b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.opening .found .result span { flex: none; color: var(--ink-2); font-size: 12.5px; }

.opening .shelfhead { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; }
.opening .shelfhead p {
  margin: 0; font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2);
}
.opening .shuffle { font: inherit; font-size: 12px; font-weight: 600; color: var(--accent); background: none; border: 0; padding: 0; cursor: pointer; }

.opening .openers { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.opening .opener {
  display: flex; flex-direction: column; text-align: left; cursor: pointer;
  font: inherit; color: var(--ink); min-width: 0; padding: 8px;
  background: var(--sunk); border: 1px solid var(--hair); border-radius: 12px;
}
.opening .opener:hover { border-color: var(--hair-strong); }
.opening .opener .art {
  width: 100%; aspect-ratio: 1; object-fit: cover; margin-bottom: 8px;
  border-radius: 8px; background: var(--plate);
}
.opening .opener b { font-size: 13px; font-weight: 600; line-height: 1.25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.opening .opener i { font-style: normal; font-size: 11.5px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Lower case and untracked, unlike the label style used elsewhere: at 375px
   "109 BPM · EASY GOING" wraps onto a second line, and one card two lines
   taller than its neighbour breaks the whole grid. The word is the part that
   means something, so it is the letterspacing that goes, not the word. */
.opening .opener em {
  font-style: normal; font-size: 11px; color: var(--ink-2); opacity: .8; margin-top: 3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Holds the shelf's place while the file is still coming, so the folds do not
   get shoved down the page just as somebody reaches for one. */
.opening .opener.skeleton { pointer-events: none; }
.opening .opener.skeleton .art { display: block; }
.opening .opener.skeleton .line { display: block; height: 9px; border-radius: 4px; background: var(--hair); margin-bottom: 5px; }
.opening .opener.skeleton .line.short { width: 55%; }

.opening .more { border-top: 1px solid var(--hair); margin-top: 18px; padding-top: 4px; }
/* A heading now, not a fold. Both blocks are always open: they were hidden
   when this panel lived inside a column and every row cost width, and at full
   width a fold is a tap that hides an answer for no reason. */
.opening .alt {
  margin: 14px 0 9px; font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; font-weight: 700; color: var(--ink-2);
}
/* The column's own chips scroll sideways in one row; these are blocks of ten
   and thirty-three, so they wrap. */
.opening .chips { flex-wrap: wrap; overflow: visible; }
.opening .surprise {
  font: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer;
  margin-top: 18px; padding: 10px 18px; border-radius: 999px;
  color: var(--accent); background: rgba(11, 92, 80, .10);
  border: 1px solid rgba(11, 92, 80, .28);
}
:root[data-room="off"] .opening .surprise {
  color: var(--live); background: rgba(20, 184, 146, .10); border-color: rgba(20, 184, 146, .30);
}
.opening .surprise:hover { border-color: var(--accent); }
:root[data-room="off"] .opening .surprise:hover { border-color: var(--live); }

/* Full width now, so the shelf spreads rather than stacking: six across on a
   desktop reads as one shelf you scan, which is what it is. */
@media (min-width: 700px) { .opening .openers { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .opening .openers { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
/* The lede and the search stop running the whole width of a wide screen: a
   line of text 1300px long is a line nobody finishes. */
@media (min-width: 900px) {
  .opening .lede, .opening-search, .opening .found { max-width: 640px; }
}
