/* ═══════════════════════════════════════════════════════════════
   THE CHASSIS — case, plate, furniture, control rail
   ═══════════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; }

/* `hidden` must win over any display a module sets on itself */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;

  /* THE OVERSCROLL GAP.
     Pull past the end of the page and the browser keeps going for a moment
     — the rubber band. What it shows in that moment is not the body, it is
     the canvas behind everything, and with no background of its own that
     canvas was falling back to flat black. So the machine appeared to end
     and a void appeared under it.

     Painting the same field on `html` means the bounce reveals more of the
     same surround instead of a hole. The gradient is repeated rather than
     inherited because a background set on `html` is what the canvas
     actually uses. */
  background: radial-gradient(120% 90% at 50% 0%, #4A443C 0%, #2E2A25 55%, #1C1917 100%);
  background-color: #1C1917;

  /* But at night that surround IS black, so matching it was never going to
     fix the complaint — there is no colour that makes an empty 150px of
     nothing look deliberate. The page has to stop instead.

     `overscroll-behavior-y: none` ends the scroll at the end of the page.
     It matters more here than on an ordinary site because the transport is
     fixed to the bottom: during a bounce the content slides and the dock
     does not, so the machine appears to come apart for a moment. Stopping
     the bounce keeps the housing welded to the near edge, which is what a
     held object does.

     Side effect worth knowing: this also disables pull-to-refresh. For a
     page with no feed to refresh that is a feature, not a loss. */
  overscroll-behavior-y: none;
}

body {
  margin: 0;
  min-height: 100vh;
  padding: clamp(0.5rem, 2vw, 2.25rem);
  /* The viewport tag says `viewport-fit=cover`, which lets the page draw
     under the notch and the status bar — that is what makes the background
     reach the edges of the glass. The cost is that *content* will happily
     slide under the clock too, which is exactly what happened: opening a
     panel scrolled the chassis up and put the toggle switches behind the
     signal icons. These insets are the rent for going edge to edge. */
  padding-top: calc(clamp(0.5rem, 2vw, 2.25rem) + env(safe-area-inset-top, 0px));
  padding-left: calc(clamp(0.5rem, 2vw, 2.25rem) + env(safe-area-inset-left, 0px));
  padding-right: calc(clamp(0.5rem, 2vw, 2.25rem) + env(safe-area-inset-right, 0px));
  font-family: var(--font-ui);
  color: var(--ink);
  /* a gradient leaves background-color transparent, which breaks contrast
     tooling and leaves nothing to fall back on — set both, always */
  background:
    radial-gradient(120% 90% at 50% 0%, #4A443C 0%, #2E2A25 55%, #1C1917 100%);
  background-color: #1C1917;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

h1, h2, h3, p, ul, ol, dl, dd, figure, fieldset { margin: 0; }
ul, ol { padding: 0; list-style: none; }
fieldset { border: 0; padding: 0; min-width: 0; }
button, input, textarea, select { font: inherit; color: inherit; }

:where(a) { color: var(--teal-ink); text-underline-offset: 3px; }

:is(button, a, input, textarea, [tabindex]):focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-radius: var(--r-xs);
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.skip-link {
  position: absolute; left: 50%; top: 0;
  transform: translate(-50%, -120%);
  z-index: 300;
  min-height: var(--tap-min);
  padding: 0.8rem 1.2rem;
  background: var(--enamel); color: var(--teal-ink);
  font-size: 0.7rem; letter-spacing: var(--track-label); text-transform: uppercase;
  border: 2px solid var(--teal); border-top: 0;
  box-shadow: var(--raise-2);
  transition: transform var(--t-quick) var(--ease-settle);
}
.skip-link:focus { transform: translate(-50%, 0); }

/* ── THE CASE ──────────────────────────────────────────────── */
.console {
  position: relative;
  width: min(1240px, 100%);
  padding: clamp(0.75rem, 1.4vw, 1.15rem);
  border-radius: var(--r-xl);
  background:
    linear-gradient(158deg, var(--steel-light) 0%, var(--steel) 22%, #9A9A9A 52%, var(--steel) 78%, #CFCFCF 100%);
  box-shadow:
    0 42px 90px -30px rgba(0, 0, 0, 0.78),
    0 2px 0 rgba(255, 255, 255, 0.5) inset,
    0 -2px 0 rgba(0, 0, 0, 0.35) inset;
}

/* corner machine screws */
.screw {
  position: absolute;
  width: 19px; height: 19px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 34% 30%, #F0E9D4 0%, var(--brass) 45%, var(--brass-dark) 100%);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.55), inset 0 -1px 2px rgba(0, 0, 0, 0.4);
  z-index: 40;
}
.screw::after {
  content: '';
  position: absolute; inset: 42% 16%;
  background: rgba(40, 30, 10, 0.62);
  border-radius: 1px;
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.28);
}
.screw--tl { top: 9px;  left: 9px;  }
.screw--tr { top: 9px;  right: 9px; }
.screw--bl { bottom: 9px; left: 9px; }
.screw--br { bottom: 9px; right: 9px; }
.screw--tl::after { transform: rotate(18deg); }
.screw--tr::after { transform: rotate(-42deg); }
.screw--bl::after { transform: rotate(66deg); }
.screw--br::after { transform: rotate(-8deg); }

/* ── NAMEPLATE ─────────────────────────────────────────────── */
.nameplate {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--gap-lg); flex-wrap: wrap;
  padding: 0.5rem clamp(1rem, 3vw, 2.4rem) 0.75rem;
}
.nameplate__title {
  font-family: var(--font-ui); font-weight: 700;
  font-size: clamp(0.85rem, 1.7vw, 1.15rem);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: #3A3732;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55), 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.nameplate__model {
  margin-top: 2px;
  font-size: 0.6rem; letter-spacing: var(--track-label); text-transform: uppercase;
  color: #3B3833;
}
.nameplate__switches { display: flex; gap: var(--gap-md); }

/* rocker switches */
.toggle {
  display: inline-flex; align-items: center; gap: 0.55rem;
  padding: 0.45rem 0.7rem 0.45rem 0.5rem;
  min-height: var(--tap-min);
  background: linear-gradient(#DCD8D0, #B9B4AB);
  border: 1px solid rgba(0, 0, 0, 0.28);
  border-radius: var(--r-key);
  box-shadow: var(--raise-1);
  cursor: pointer;
}
.toggle__lever {
  position: relative;
  width: 30px; height: 18px;
  background: var(--bakelite);
  border-radius: 2px;
  box-shadow: var(--inset-2);
  overflow: hidden;
}
.toggle__lever::after {
  content: '';
  position: absolute; inset: 2px 2px 2px 52%;
  background: linear-gradient(#F2EEE4, #C4BDB0);
  border-radius: 1px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  transition: inset var(--t-tap) var(--ease-mech);
}
.toggle[aria-checked='false'] .toggle__lever::after { inset: 2px 52% 2px 2px; }
.toggle__label {
  font-stretch: 78%;
  font-size: 0.58rem; letter-spacing: var(--track-label); text-transform: uppercase;
  color: #33312C;
}
.toggle[aria-checked='true'] { box-shadow: var(--raise-1), 0 0 0 1px rgba(201, 162, 39, 0.45); }

/* ── THE BOARD (pegboard plate) ────────────────────────────── */
.board {
  position: relative;
  overflow: hidden;
  padding: clamp(1rem, 2.4vw, 2.1rem);
  display: grid;
  gap: var(--gap-bay);
  border-radius: var(--r-lg);
  background-color: var(--enamel-shade);
  background-image:
    radial-gradient(var(--perf-dot) 1.1px, transparent 1.2px),
    radial-gradient(120% 80% at 50% 0%, rgba(255, 255, 255, 0.55), transparent 60%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.3), rgba(120, 105, 80, 0.12));
  background-size: var(--perf-size) var(--perf-size), 100% 100%, 100% 100%;
  box-shadow:
    inset 0 2px 6px rgba(0, 0, 0, 0.3),
    inset 0 -2px 4px rgba(255, 255, 255, 0.4),
    0 0 0 1px rgba(0, 0, 0, 0.3);
}

/* generated surface wear — nothing here looks new, everything looks kept */
.grain {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 1;
}
.grain__fine { opacity: 0.055; mix-blend-mode: multiply; }
.grain__coarse { opacity: 0.09; mix-blend-mode: multiply; }

/* routed PCB traces, behind everything */
.traces {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  color: var(--teal-mid);
  opacity: 0.8;
  pointer-events: none;
}
/* The span across the reels is the tape itself, so it is brown and wide —
   a ribbon, not a conductor. The drops off it stay teal: those are guides. */
.traces__tape {
  stroke: var(--tape);
  stroke-width: 7;
  opacity: 0.9;
}

/* min-width:0 lets a bay shrink below its content's min-content width.
   Without it a single wide module (the reels, a long readout) sizes the
   whole grid track and pushes the board off the right edge on a phone. */
.bay { position: relative; z-index: 2; min-width: 0; }
.board { grid-template-columns: minmax(0, 1fr); }

.bay--status {
  display: grid;
  grid-template-columns: auto auto 1fr 1fr auto;
  align-items: end;
  gap: var(--gap-module);
}

.bay--deck {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1.25fr);
  grid-template-areas:
    'reels scope'
    'transport transport'
    'path path';
  gap: var(--gap-module) var(--gap-bay);
  padding: clamp(0.8rem, 1.5vw, 1.2rem);
  border-radius: var(--r-md);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.42), rgba(163, 148, 120, 0.16));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.75),
    inset 0 -1px 0 rgba(0, 0, 0, 0.16),
    0 0 0 1px rgba(60, 50, 30, 0.16);
}
.deck__reels { grid-area: reels; }
.scope-bezel { grid-area: scope; }
.transport { grid-area: transport; }
.tape-path { grid-area: path; }

/* engraved captions, stamped into the plate */
.engraved {
  font-size: 0.66rem;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--engrave);
  text-shadow: var(--engrave-text);
}

/* ── CONTROL RAIL ──────────────────────────────────────────── */
.rail {
  position: relative;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: clamp(0.35rem, 0.9vw, 0.75rem);
  padding: clamp(0.6rem, 1.4vw, 0.95rem) clamp(0.5rem, 1.6vw, 1.4rem) 0.55rem;
}

/* All six positions are on the panel at this width, so there is nothing
   behind MORE and no reason to show it. Removed rather than hidden — a
   control that opens nothing should not be announced either.

   Scoped to `.rail` on purpose: `.rail__key` sets `display: flex` and is
   declared after this, so at equal specificity it would win and MORE would
   leak onto the desktop panel as a seventh key. */
.rail .rail__more { display: none; }
.rail__key {
  font-stretch: 82%;
  position: relative;
  display: flex; align-items: center; justify-content: center; gap: 0.5rem;
  min-height: var(--tap-min);
  padding: 0.55rem 0.4rem;
  font-size: clamp(0.66rem, 0.95vw, 0.74rem);
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--teal-ink);
  background: linear-gradient(180deg, var(--enamel) 0%, var(--enamel-deep) 100%);
  border: 1px solid rgba(60, 50, 30, 0.3);
  border-radius: var(--r-key);
  box-shadow: var(--raise-2);
  cursor: pointer;
  transition: transform var(--t-tap) var(--ease-mech), box-shadow var(--t-tap) var(--ease-mech);
}
.rail__key::after { /* chip legs */
  content: '';
  position: absolute; left: 12%; right: 12%; bottom: -5px; height: 5px;
  background: repeating-linear-gradient(90deg, var(--steel-dark) 0 3px, transparent 3px 9px);
  border-radius: 0 0 2px 2px;
  opacity: 0.65;
}
.rail__key:hover { box-shadow: var(--raise-3); }
.rail__key:active,
.rail__key[aria-expanded='true'] {
  transform: translateY(2px);
  box-shadow: var(--inset-2);
  background: linear-gradient(180deg, var(--enamel-deep), var(--enamel-shade));
}
.rail__lamp {
  width: 7px; height: 7px; border-radius: 50%;
  background: #8D9A8F;
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.5);
  transition: background var(--t-quick), box-shadow var(--t-quick);
}
.rail__key[aria-expanded='true'] .rail__lamp {
  background: var(--amber);
  box-shadow: 0 0 6px rgba(201, 162, 39, 0.9), 0 0 12px rgba(201, 162, 39, 0.55);
}

/* bottom edge connector */
.edge-connector {
  height: 16px;
  margin: 0.4rem clamp(2rem, 12vw, 9rem) 0.15rem;
  background:
    repeating-linear-gradient(90deg,
      var(--brass) 0 7px,
      rgba(0, 0, 0, 0.35) 7px 8px,
      #8E8129 8px 12px,
      rgba(0, 0, 0, 0.2) 12px 13px);
  border-radius: 0 0 3px 3px;
  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.45);
}

/* The instruction decal is for the QR visitor, who arrives on a phone. On a
   desktop the whole machine is visible at once and the controls explain
   themselves, so the label would be clutter on the panel. Genuinely removed
   here rather than hidden from sight only — a sentence a sighted desktop
   visitor does not need is not one a screen-reader user needs read aloud. */
.board__decal { display: none; }

.colophon {
  font-size: 0.6rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: rgba(240, 236, 226, 0.6);
  text-align: center;
}
.colophon kbd {
  font-family: var(--font-ui); font-size: 0.9em;
  padding: 1px 5px;
  border: 1px solid rgba(240, 236, 226, 0.4);
  border-radius: 3px;
}

/* ── RESPONSIVE — the board becomes a control strip ────────── */
@media (max-width: 1080px) {
  .bay--status { grid-template-columns: auto auto 1fr; }
  .module--vu { grid-column: 3; grid-row: 1 / span 2; justify-self: end; }
}

@media (max-width: 860px) {
  .bay--deck {
    grid-template-columns: 1fr;
    grid-template-areas: 'scope' 'reels' 'transport' 'path';
  }
  .rail { grid-template-columns: repeat(3, 1fr); }
}

/* ── MOBILE · the board becomes a vertical control strip ─────
   Not a shrunk board. Every module gets the full width and stacks in
   the order you'd reach for it: what it is, then what it's doing,
   then the controls.                                                  */
@media (max-width: 620px) {
  body { padding: 0.4rem; }
  .console { border-radius: var(--r-md); padding: 0.55rem; }
  .board { padding: 0.85rem 0.7rem; }
  .screw { width: 13px; height: 13px; }

  .nameplate { padding: 0.35rem 0.5rem 0.6rem; gap: 0.6rem; }
  .nameplate__switches { width: 100%; justify-content: space-between; gap: 0.35rem; }
  .toggle { flex: 1 1 0; justify-content: center; padding: 0.45rem 0.4rem; }

  /* one column — three lamps and a five-digit counter never fit side by side */
  .bay--status {
    grid-template-columns: 1fr;
    align-items: stretch;
    gap: var(--gap-md);
  }
  .module { justify-items: stretch; }
  .module--counter { justify-items: start; }
  .module--lamps { justify-self: stretch; }
  /* space-around, not space-between — the lamp bloom needs room at the edges */
  .lamps { justify-content: space-around; width: 100%; }
  /* THE VU GOES FULL WIDTH.
     On a phone the meter sat centred at 214px with dead board either side —
     a small instrument marooned in a wide empty bay. A real meter is fitted
     to the width of its panel. The bezel stretches, the dial scales with it,
     and the one moving needle on the machine becomes worth looking at. */
  .module--vu { grid-column: auto; grid-row: auto; justify-self: stretch; }
  .bay--status .vu-bezel { width: 100%; }
  .bay--status .vu { width: 100%; }

  .colophon { display: none; }
}

/* ── MOBILE DECK ─────────────────────────────────────────────
   The player is the reason anyone is here, so it gets fixed properly
   rather than allowed to wrap into whatever shape is left over.      */
/* NOTE ON SPECIFICITY: modules.css loads after this file, so a bare `.scope`
   or `.key__cap` here loses to its base rule no matter what the media query
   says. Everything below is scoped to its bay so it wins on specificity
   rather than on source order. */
@media (max-width: 700px) {
  /* A 16:7 tube instead of a 1000:232 letterbox. At phone widths the wide
     ratio collapsed the screen to ~69px and left the waveform no room. */
  .bay--deck .scope { aspect-ratio: 16 / 7; }
  .bay--deck .scope__legend { font-size: clamp(0.68rem, 2.6vw, 0.88rem); }

  /* seven keys, one row, like the deck they came from — no orphans */
  .bay--deck .transport { justify-content: center; }
  .bay--deck .transport__buttons {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: clamp(2px, 0.8vw, 5px);
    width: 100%;
  }
  .bay--deck .key { min-width: 0; }
  .bay--deck .key__cap,
  .bay--deck .key--wide .key__cap { width: 100%; height: 46px; }
  .bay--deck .key__label { font-size: 0.56rem; letter-spacing: 0.05em; }

  .bay--deck .transport__meters { width: 100%; justify-content: space-around; gap: 1rem; }
}

/* the plate's label/value pairs need a full line to themselves on a phone */
@media (max-width: 520px) {
  .bay--plate .plate__specs { grid-template-columns: 1fr; gap: 0.35rem; }
}

/* ═══════════════════════════════════════════════════════════════
   PHONE FIRST — THE MACHINE BECOMES PORTABLE
   ═══════════════════════════════════════════════════════════════

   The visitor this block is written for is standing on a street corner
   beside the telephone, one hand on the phone, in daylight, having just
   scanned a QR code. They did not search for us. They have given us
   about eight seconds.

   The old phone layout was not broken — it was correctly shrunk and
   wrongly ordered. On a laptop the whole machine is in view and the eye
   picks its own route through it. On a phone you only ever see one slice
   at a time, so the order of the slices becomes a story told to someone
   who cannot skip ahead. Ours described the machine for a full screen
   before offering the thing the machine does: PLAY sat 1.0 screens below
   the fold and the first screen held nothing you could press.

   So this is a re-ordering, not a redesign. No colour, texture, sound or
   motion rule changes here. Same machine, packed for carrying.
   Findings and measurements: backend/09-MOBILE.md
   ═══════════════════════════════════════════════════════════ */
@media (max-width: 620px) {
  /* Height reserved for the docked transport. One number, used by both the
     spacer and the panel offset, so they can never drift apart. */
  /* Measured height of the docked housing plus the 10px it floats above the
     bottom edge. Used only as bottom clearance so the last thing on the page
     is not permanently parked underneath it. */
  :root { --dock-h: 148px; }

  /* ── 1 · THE RUNNING ORDER ──────────────────────────────────
     Deck first: screen, reels, controls. The instrument cluster is
     wonderful and nobody arrived for it — it reads as context once you
     already have sound, and as an obstacle before. `order` moves the
     boxes without touching the markup, so the desktop layout and the
     order a screen reader announces both stay exactly as they were. */
  .board__decal { order: 0; }
  .bay--deck    { order: 1; }
  .bay--status  { order: 2; }
  .bay--plate   { order: 3; }

  /* ── 2 · THE INSTRUCTION DECAL ──────────────────────────────
     Printed, not engraved — so sentence case, no letter-spacing, no
     width-axis compression. A stranger reads this; the machine's own
     legends are for someone who already knows what they are looking at. */
  .board__decal {
    display: block;
    margin: 0 0 0.15rem;
    font-size: 1.02rem;
    line-height: 1.35;
    letter-spacing: 0;
    font-stretch: 100%;
    color: var(--ink);
    text-align: center;
    text-wrap: balance;
  }

  /* ── 3a · THE RUNNING ORDER, CORRECTED AGAIN ────────────────
     The instrument cluster was sitting between the deck and the plate:
     369px of counters, lamps and needles standing between a visitor and
     the person who left the message. Screen and reels, then who left it
     and what they said, then the machinery. The machine is the vessel,
     not the point. */
  .bay--plate  { order: 2; }
  .bay--status { order: 3; }

  /* ── 3 · THE TRANSPORT TRAVELS ──────────────────────────────
     A machine that sits on a desk puts its controls under your gaze. A
     machine you *carry* — a Nagra, a Walkman, a field recorder — puts
     them at the near edge, under the thumb of the hand holding it. So on
     a phone the key bed docks to the bottom of the screen.

     This is not a tab bar wearing a costume: it is the same bakelite
     housing with the same brass fixings it has on the desktop board,
     moved to where the hand actually is. Nothing new was invented. */
  /* Every bay is `position: relative; z-index: 2`, so each one opens its own
     stacking context. A z-index on the dock can therefore only compete with
     its siblings *inside* .bay--deck — from the outside the whole deck is
     still just "layer 2", and .bay--status comes later in the markup, so it
     was painting straight over the docked keys. The layer has to be lifted on
     the bay, not on the thing inside it. */
  .bay--deck { z-index: 50; }

  .bay--deck .transport__buttons {
    position: fixed;
    /* NOT edge to edge. A band welded to the screen edges reads as browser
       chrome — a strip the phone put there, cropped by the glass. Inset on
       all four sides it reads as what it is: a housing resting on the
       machine, with its own edges, its own corners and air around it.
       `max()` keeps it clear of the notch in landscape too. */
    left: max(10px, env(safe-area-inset-left));
    right: max(10px, env(safe-area-inset-right));
    /* stacked directly on top of the rail — `--rail-h` is measured in
       main.js because a sibling cannot read a sibling's height in CSS */
    bottom: calc(10px + var(--rail-h, 66px) + env(safe-area-inset-bottom, 0px));
    z-index: 60;
    display: grid;
    /* PLAY is most of what anyone does out here, so it gets a row of its
       own and twice the width. Seven identical keys is desk logic — it
       assumes a user browsing a machine rather than someone who wants to
       hear a voice. */
    /* Three keys, the way every music app on a phone does it. REW, FF and
       STOP are not missing — the waveform below is already a scrubber, and
       dragging it is both faster and more precise than nudging with a key.
       Apple Music and Spotify ship no rewind buttons at all for the same
       reason. They stay on the desktop board, where there is room for the
       whole transport and a mouse to use it with. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr);
    grid-template-areas: 'prev play next';
    gap: 7px;
    width: auto;
    /* the upper tier of one housing — square where it meets the rail below */
    border-radius: var(--r-md) var(--r-md) 0 0;
    /* horizontal room for the brass fixings to sit beside the keys rather
       than on top of them */
    padding: 0.7rem 1.15rem 0.6rem;
    /* Lifted off the page rather than stuck to it. The dark ring is the
       housing's own edge catching light; the long soft shadow is the gap
       between it and the board underneath. Both together are what stop it
       reading as a flat sticker. */
    box-shadow:
      0 10px 26px -6px rgba(20, 14, 6, 0.55),
      0 2px 6px rgba(20, 14, 6, 0.4),
      0 0 0 1px rgba(0, 0, 0, 0.5),
      inset 0 1px 0 rgba(255, 255, 255, 0.14);
  }

  /* The brass fixings stay where they are on the desktop bed — centred on
     the short sides, holding the housing down. On the full-bleed version
     they sat marooned in the top corners with nothing to fasten to. */
  .bay--deck .transport__buttons::before,
  .bay--deck .transport__buttons::after { top: 50%; margin-top: -5px; }
  .bay--deck .transport__buttons::before { left: 5px; }
  .bay--deck .transport__buttons::after  { right: 5px; }

  #key-prev  { grid-area: prev; }
  #key-next  { grid-area: next; }

  /* PLAY and PAUSE share the middle cell and swap on state, so the visitor
     sees one button that does the obvious thing — again, the music-app
     behaviour. Both keep their own label and their own accessible name;
     nothing is relabelled by CSS, which would lie to a screen reader. */
  #key-play, #key-pause { grid-area: play; }
  .bay--deck.is-rolling  #key-play  { display: none; }
  .bay--deck:not(.is-rolling) #key-pause { display: none; }

  /* off the phone panel entirely — the waveform does this job */
  #key-rew, #key-ff, #key-stop { display: none; }

  /* ── 4 · NOTHING SMALLER THAN A FINGERTIP ───────────────────
     Apple's floor is 44px, Google's is 48. Twenty-six controls were
     under it, the transport keys at 33–35px wide. This is an
     accessibility fix that happens to also be a mobile one. */
  .bay--deck .key { min-width: 0; }
  .bay--deck .key__cap,
  .bay--deck .key--wide .key__cap { width: 100%; height: 48px; }
  .bay--deck .key__label { font-size: 0.6rem; letter-spacing: 0.06em; }

  /* ── 5 · ROOM AT THE BOTTOM, INSIDE THE MACHINE ─────────────
     The dock floats over the page, so without clearance the last thing on
     the page sits underneath it and cannot be reached.

     That clearance used to live on `body`, and it was visible: scroll to
     the end and the chassis stopped, then ~150px of bare black, then the
     dock. The dock looked like it was floating over a hole rather than
     resting on the machine — the same blank-tail fault as the panels, in
     a different place.

     Putting the same height on `.console` instead means the steel carries
     on down behind the keys. Identical clearance, no void: the dock now
     sits ON something, which is what a control panel does. */
  body { padding-bottom: 0.4rem; }
  .console { padding-bottom: calc(var(--dock-h) + 0.5rem); }

  /* An open panel fills the board and owns the screen — the dock would
     float over someone's reading. It is also unreachable behind an inert
     board, so leaving it visible would be a lie about what is pressable. */
  body:has(.panel.is-open) .bay--deck .transport__buttons { display: none; }
}

/* ── PHONE · READABLE AT ARM'S LENGTH ────────────────────────
   Outdoors, held at arm's length, often by someone older than the person
   who designed it. The desktop sizes are correct for a screen 60cm away
   on a desk and too small for a street. */
@media (max-width: 620px) {
  .bay--plate .plate__title { font-size: clamp(1.35rem, 6vw, 1.75rem); }
  .bay--plate .plate__specs { font-size: 1rem; }
  .bay--plate .plate__note  { font-size: 1rem; line-height: 1.55; }

  /* ── THE RAIL DOCKS UNDER THE TRANSPORT ─────────────────────
     It was 1.64 screens down the page. Nothing important should ever be
     reached by scrolling — that is the single rule every music app on a
     phone obeys, and the reason nobody was finding the archive.

     So the rail becomes the lower tier of the same housing: transport on
     top, functions beneath, one object at the near edge. Three positions
     fit; the other four fold out from behind MORE. */
  .rail {
    position: fixed;
    left: max(10px, env(safe-area-inset-left));
    right: max(10px, env(safe-area-inset-right));
    bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    z-index: 61;
    display: grid;
    /* Six tracks, not three. LISTEN / ABOUT / MORE take two each, so they
       fill one row exactly and never move — the thumb learns one place and
       keeps it. The four fold-out positions take three each, so they fill
       two whole rows above without leaving an orphan sitting next to
       LISTEN. Three columns could not do that: four items never divide
       evenly into three. */
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 6px;
    margin: 0;
    padding: 0.55rem 1.15rem 0.7rem;
    border-radius: 0 0 var(--r-md) var(--r-md);
    background-color: var(--bakelite);
    background-image: linear-gradient(160deg, var(--bakelite-light), var(--bakelite) 60%, var(--bakelite));
    box-shadow:
      0 10px 26px -6px rgba(20, 14, 6, 0.55),
      0 0 0 1px rgba(0, 0, 0, 0.5);
  }
  /* the milled groove where the two tiers meet */
  .rail::before {
    content: '';
    position: absolute; left: 0.9rem; right: 0.9rem; top: 0;
    height: 1px;
    background: rgba(0, 0, 0, 0.55);
    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.07);
  }

  .rail__key {
    grid-column: span 2;
    min-height: 46px;
    font-size: 0.78rem;
    padding: 0.4rem 0.2rem;
    /* legends printed on dark bakelite, like the transport labels above */
    color: #EFE9DA;
    text-shadow: none;
    background-color: var(--housing-key);
    background-image: linear-gradient(var(--housing-key), var(--housing-key-deep));
  }
  .rail__key[aria-expanded='true'] {
    background-image: linear-gradient(var(--housing-key-deep), var(--housing-key));
  }

  /* The chip legs read as a component soldered onto a pale board. On the
     dark housing they hang off the bottom edge with nothing to solder to. */
  .rail__key::after { display: none; }

  /* The fold-out is gone. All six sit on one row now that each key is a
     symbol above a word, so there is nothing to hide and no MORE to hide
     it behind — see the one-row block in panels.css. Leaving these
     `display: none` rules behind would keep four functions invisible with
     nothing left to reveal them, which is how a control disappears from a
     product without anyone deciding to remove it. */

  /* an open panel owns the screen and the board behind it is inert — a
     visible control that cannot be pressed is a lie about the machine */
  body:has(.panel.is-open) .rail { display: none; }
}

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

/* ── COLD START ────────────────────────────────────────────────
   The half-second before the supply comes up. Not a loading screen —
   a machine that has not been switched on yet. */
.board, .scope, .readout, .counter, .vu {
  transition: filter 620ms var(--ease-settle), opacity 620ms var(--ease-settle);
}
.console.is-cold .board { filter: saturate(0.3) brightness(0.88); }
.console.is-cold .scope,
.console.is-cold .readout,
.console.is-cold .counter { opacity: 0.12; }
.console.is-cold .lamp__glass { filter: brightness(0.5); }

@media (prefers-reduced-motion: reduce) {
  .console.is-cold .board,
  .console.is-cold .scope,
  .console.is-cold .readout,
  .console.is-cold .counter,
  .console.is-cold .lamp__glass { filter: none; opacity: 1; }
}


/* ── MAKER'S PLATE ─────────────────────────────────────────────
   A footer in machine language: an engraved manufacturer badge on
   the chassis. Same ink family as the nameplate above it. */
.maker {
  display: grid; gap: 0.3rem; justify-items: center;
  padding: 0.7rem 1rem 0.9rem;
}
.maker__line {
  font-size: 0.62rem; letter-spacing: var(--track-label); text-transform: uppercase;
  font-stretch: 82%;
  color: #3A3732;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
}
.maker__links {
  display: flex; gap: 0.6rem; align-items: center;
  font-size: 0.62rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: #3B3833;
}
.maker__links a { color: inherit; }
.maker__about {
  background: none; border: 0; padding: 0;
  font: inherit; letter-spacing: inherit; text-transform: inherit;
  color: inherit; text-decoration: underline; text-underline-offset: 3px;
  cursor: pointer;
}
[data-theme='dark'] .maker__line { color: #B8B6B0; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); }
[data-theme='dark'] .maker__links { color: #B2B0AA; }

/* ── PHONE · THE LAST OF THE SMALL TARGETS ───────────────────
   Appended at the end so these win on source order rather than needing
   `!important`. Two groups were still under the 44px floor:

   the contribution chips, which are hardware and keep their bevel; and
   the maker's plate links, which are 22px of underlined text. That
   second group matters more than it looks — "remove my recording" is on
   it, and a takedown route that is hard to hit on a phone is a takedown
   route that does not really exist. */
@media (max-width: 620px) {
  .support__amounts .chip { min-height: 44px; }

  .maker__links a,
  .maker__about {
    display: inline-block;
    padding: 0.6rem 0.15rem;
    min-height: 44px;
    /* a two-line link's box is only as wide as its longest word, which put
       "write to us" a pixel under the floor */
    min-width: 44px;
    text-align: center;
  }
  .maker__links { gap: 0.1rem 0.9rem; line-height: 1.5; }
}

/* ── THE FOLLOW MARK ─────────────────────────────────────────
   On the maker's plate, where a maker's mark belongs. It is the one link
   on the whole board that leaves the machine, and the QR visitor's last
   step — scan, hear a voice, want more — so it sits where the journey
   ends rather than three taps deep inside a panel.

   Styled as an engraved stamp on the chassis, not a social button: the
   plate is brushed steel, and a coloured pill glued to it would read as
   somebody else's badge stuck on the case. Same ink as the plate's own
   text, same weight, lit only on hover. */
.maker__follow { margin: 0 0 0.55rem; }

.follow {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: var(--tap-min);
  padding: 0.5rem 0.9rem 0.5rem 0.7rem;
  border: 1px solid rgba(58, 55, 50, 0.32);
  border-radius: 999px;
  /* The plate's own ink, both themes — #3A3732 by day, #B8B6B0 at night.
     The first pass hardcoded a pale cream, which is correct on the dark
     chassis and nearly invisible on the light one. The chassis is steel
     in both themes; only the LIGHT on it changes, so the ink has to
     change with it like every other label on the plate. */
  color: #3A3732;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.45);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  transition: color var(--t-quick) var(--ease-mech),
              border-color var(--t-quick) var(--ease-mech),
              background-color var(--t-quick) var(--ease-mech);
}
.follow__mark { width: 17px; height: 17px; flex: 0 0 auto; }

.follow:hover,
.follow:focus-visible {
  color: #1F1D19;
  border-color: rgba(58, 55, 50, 0.65);
  background-color: rgba(58, 55, 50, 0.08);
}

[data-theme='dark'] .follow {
  border-color: rgba(240, 236, 226, 0.3);
  color: #B8B6B0;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.6);
}
[data-theme='dark'] .follow:hover,
[data-theme='dark'] .follow:focus-visible {
  color: #F0EEE9;
  border-color: rgba(240, 236, 226, 0.6);
  background-color: rgba(240, 236, 226, 0.09);
}

/* On a phone it is the last thing on the page and the whole point of the
   QR code, so it gets the full width and reads as a proper destination
   rather than a footnote. */
@media (max-width: 620px) {
  .maker__follow { margin-bottom: 0.8rem; }
  .follow {
    display: flex;
    justify-content: center;
    width: 100%;
    min-height: 48px;
    font-size: 0.8rem;
  }
}

/* ═══════════════════════════════════════════════════════════════
   THE GATE — one screen, then the machine
   ═══════════════════════════════════════════════════════════════

   Strictly one screen. `100dvh` rather than `100vh` because on a phone
   the browser's own chrome slides in and out as you scroll, and `vh`
   measures the tallest state — so a "full screen" panel sized in vh is
   taller than the screen for most of its life, and the button ends up
   under the address bar. `dvh` measures what is actually visible.

   Dark whatever the theme. This is the machine unpowered: the enamel is
   not lit yet, and switching the room's lights on does not change that.
   ═══════════════════════════════════════════════════════════ */
.gate {
  position: fixed;
  inset: 0;
  z-index: 400;
  /* TWO REGIONS, STACKED — not one stack with the machine behind it.
     Overlaid, the button landed on the head block and the wordmark cut
     across the tape: every element fighting for the same centre. The
     machine gets the upper field, the words get the lower one, and
     neither has to apologise for the other. */
  display: grid;
  grid-template-rows: 1fr auto;
  align-items: center;
  justify-items: center;
  padding: calc(2rem + env(safe-area-inset-top, 0px)) 1.5rem
           calc(2rem + env(safe-area-inset-bottom, 0px));
  height: 100dvh;
  background:
    radial-gradient(120% 90% at 50% 30%, #171614 0%, #0C0B0A 60%, #060605 100%);
  opacity: 1;
  transition: opacity 620ms var(--ease-settle);
}
.gate.is-leaving { opacity: 0; pointer-events: none; }

/* nothing behind it scrolls while it is up */
.is-gated, .is-gated body { overflow: hidden; }

.gate__inner {
  display: grid;
  justify-items: center;
  gap: clamp(1.1rem, 3.5vh, 2rem);
  /* `min()` not a bare 34rem: 34rem is 544px, wider than any phone, so
     the block overflowed the right edge and clipped the first line. */
  max-width: min(34rem, 100%);
  text-align: center;
}

.gate__mark {
  margin: 0;
  font-size: 0.68rem;
  font-stretch: 78%;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: rgba(233, 228, 216, 0.5);
}

.gate__quote {
  margin: 0;
  display: grid;
  gap: clamp(0.7rem, 2vh, 1.15rem);
}
.gate__quote p {
  margin: 0;
  font-size: clamp(1.02rem, 2.6vh, 1.4rem);
  line-height: 1.5;
  letter-spacing: 0;
  color: #E9E4D8;
  text-wrap: balance;
}
.gate__quote em { font-style: normal; color: var(--teal-light); }

/* The resolving line reserves its own height before it fills, so the
   layout does not jump as characters arrive. */
.gate__resolve { min-height: 3em; color: rgba(233, 228, 216, 0.78); }

.gate__enter {
  margin-top: clamp(0.3rem, 2vh, 1rem);
  min-height: 52px;
  padding: 0.85rem 2.4rem;
  border: 0;
  border-radius: 999px;
  background-color: var(--teal);
  background-image: linear-gradient(180deg, var(--teal-light), var(--teal-mid));
  color: #0C2422;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  cursor: pointer;
  box-shadow: 0 10px 26px -10px rgba(0, 0, 0, 0.8);
  transition: transform var(--t-tap) var(--ease-mech), filter var(--t-quick) var(--ease-mech);
}
.gate__enter:hover { filter: brightness(1.06); }
.gate__enter:active { transform: translateY(1px); }
.gate__enter:focus-visible { outline: var(--focus-ring); outline-offset: 3px; }

.gate__foot {
  margin: 0;
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  color: rgba(233, 228, 216, 0.38);
}

@media (prefers-reduced-motion: reduce) {
  .gate { transition: none; }
  .gate__resolve { min-height: 0; }
}

/* ── THE MACHINE ON THE DOOR ─────────────────────────────────
   Big, and the point of the screen. At rest there is nothing here but
   the wordmark and this, so it can take the room — the restraint that
   was right when a quote shared the space is just timidity once the
   quote is gone.

   Sized in vmin rather than vw so it stays a machine rather than a
   letterbox: on a wide monitor vw alone would push the reels off both
   edges, and on a tall phone it would shrink them to nothing. vmin
   follows whichever dimension is actually scarce. */
.gate__machine {
  grid-row: 1;
  display: grid;
  place-items: center;
  width: 100%;
  min-height: 0;                 /* let the row actually shrink on a phone */
  overflow: hidden;              /* and clip rather than push the words down */
  pointer-events: none;
}

.gate__deck {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* One source of truth for the reel size, so the tape can be anchored to
     it instead of guessed at with percentages. */
  /* SIZED BY WHICHEVER RUNS OUT FIRST, WIDTH OR HEIGHT.
     A reel is square, so a width-only rule breaks the moment the window
     is wide and short: two reels at 47vw on a 1600x900 screen would be
     752px tall each and leave 150px for the words. `min()` of the two
     takes whichever dimension is actually scarce, which is what keeps
     this composed at every shape instead of only at the one I happened
     to be testing.

     60vh, not 100, because the machine only owns the upper region — the
     wordmark and the button need the rest. */
  /* THE ROOT CAUSE OF ALL THREE REPORTS.
     47vw for each of two reels is 94vw — but the gate has 1.5rem of
     padding on each side, so only about 94vw MINUS 3rem is actually
     available. The deck overflowed by that difference, and because flex
     items shrink by default the reels silently rendered smaller than
     --reel while the tape stayed anchored to the full value. That is
     why the tape overshot the flanges, why the head landed on top of a
     reel, and why the machine was cropped on a phone: one overflow,
     three symptoms.

     Measuring against the space that actually exists fixes all of them.
     Divided by 2.08 rather than 2 to leave room for the head and a
     hair of margin. */
  --reel: clamp(110px, min(calc((100vw - 3rem) / 2.08), 58vh), 900px);

  /* Gap stays 0. A NEGATIVE GAP DOES NOTHING — CSS clamps it to zero and
     fails silently, which is exactly what happened when I tried to tuck
     the flanges in with it. Overlap has to come from margin, and it does,
     on the head below. */
  gap: 0;
  opacity: 0.68;
  transition: opacity 1100ms var(--ease-settle), filter 1100ms var(--ease-settle);
}

/* AS BIG AS THE ROOM ALLOWS.
   Sized off the width rather than vmin now: vmin was letting a short
   landscape window shrink reels that had plenty of horizontal space to
   fill. The reels are the screen, so they take the screen.

   42vw each puts the pair plus the gap and the head at roughly 96% of
   the width — filling it, with just enough margin that the flanges do
   not touch the glass. The 640px ceiling is there so a large monitor
   gets a machine and not a mural. */
/* `flex: none` so a reel can never quietly shrink below --reel. If the
   sizing is ever wrong again it will overflow visibly instead of
   producing a tape that no longer meets its hubs — a loud failure is
   worth more than a silent one. */
.gate__reel-mount { width: var(--reel); flex: none; }
.gate__reel-mount .reel { width: 100%; height: auto; display: block; }

/* THE TAPE RUNS BETWEEN THE REELS AND NOWHERE ELSE.
   Full-bleed it read as a stray dash across the screen — a horizontal
   rule nobody asked for, which is exactly what it looked like. Tape
   spans hub to hub because that is the only place tape can be. */
.gate__tape {
  position: absolute;
  /* Anchored to the hubs. Percentages of the deck let the ends overshoot
     the flanges as the reels grew, and a line sticking out past the
     machine on both sides is the same stray dash as before, twice. Half
     a reel in from each edge IS the hub, at any size. */
  left: calc(var(--reel) / 2);
  right: calc(var(--reel) / 2);
  top: 27%;
  height: clamp(4px, 0.7vw, 11px);
  border-radius: 4px;
  background: linear-gradient(180deg, var(--tape-light), var(--tape));
  opacity: 0.95;
}

/* The head sits ON TOP of the flanges rather than between them, which
   is where it sits on the machine — the tape wraps around it, so it has
   to be in front of the reels, not in a lane of its own. */
.gate__head {
  width: clamp(18px, 2.6vw, 52px);
  align-self: center;
  position: relative;
  z-index: 2;
  /* This is the tuck. Negative margin is the one that actually works,
     and it is proportional to the reel so the overlap holds at every
     size instead of drifting as the machine grows. */
  margin-inline: calc(var(--reel) * -0.05);
}
.gate__head span {
  display: block;
  height: clamp(34px, 6vw, 96px);
  border-radius: 7px 7px 4px 4px;
  background: linear-gradient(180deg, var(--teal-light), var(--teal-mid));
  box-shadow: 0 4px 14px -4px rgba(0, 0, 0, 0.6);
}

/* Once the words arrive the machine steps back behind them — it has
   said what it had to say by then, and text over a bright moving
   surface is unreadable however good the surface is. */
/* Once the quote appears it needs the room, so the machine gives some
   back. Without this the deck kept its full height, overflowed its grid
   row and the words landed on top of the reels. */
.gate[data-stage='ringing'] .gate__deck {
  opacity: 0.34;
  --reel: clamp(90px, min(calc((100vw - 3rem) / 2.08), 34vh), 460px);
}
.gate.is-pulsing .gate__deck { animation: gate-swell 2s var(--ease-settle); }
@keyframes gate-swell {
  0%   { opacity: 0.34; }
  12%  { opacity: 0.52; }
  100% { opacity: 0.34; }
}

.gate__inner {
  grid-row: 2;
  position: relative;
  z-index: 1;
  padding-bottom: clamp(1rem, 5vh, 3rem);
}

/* At rest the wordmark carries the screen alone, so it stops being a
   caption and becomes the title. */
.gate[data-stage='quote'] .gate__mark {
  font-size: clamp(0.78rem, 2.2vmin, 1.05rem);
  letter-spacing: 0.42em;
  color: rgba(233, 228, 216, 0.82);
}
.gate[data-stage='quote'] .gate__inner { gap: clamp(2rem, 6vh, 4rem); }

/* ── THE BUTTON THAT BECOMES A PICKUP ────────────────────── */
.gate__enter.is-answer {
  background-image: linear-gradient(180deg, #F0C860, var(--brass));
  color: #241F0F;
  animation: gate-waiting 6s var(--ease-settle) infinite;
}
/* It breathes only while the line is actually ringing — a lamp may
   breathe when something is live, and this is the one thing that is. */
@keyframes gate-waiting {
  0%, 34%, 100% { box-shadow: 0 10px 26px -10px rgba(0,0,0,0.8); }
  8%            { box-shadow: 0 10px 26px -10px rgba(0,0,0,0.8), 0 0 0 10px rgba(240, 200, 96, 0.14); }
}

.gate__ringing {
  margin: 0;
  font-family: var(--font-screen);
  font-size: 0.72rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--brass);
}

@media (prefers-reduced-motion: reduce) {
  .gate.is-pulsing .gate__deck, .gate__enter.is-answer { animation: none; }
  .gate__deck { transition: none; }
}

/* On a phone in portrait the machine gives up height before the words do.
   A door whose only control is below the fold is not a door. */
/* A short window has less room ABOVE the reels, not less room across.
   Trimming their width here was solving the wrong axis — the fix is to
   let the machine sit closer to the words, not to shrink it. */
@media (max-height: 700px) {
  .gate__deck { --reel: clamp(110px, min(40vw, 52vh), 340px); }
  .gate__inner { padding-bottom: clamp(0.5rem, 2vh, 1.5rem); }
}
