/* ══════════════════════════════════════════════════════════════════════════
   THE MACHINE — every rule the pre-Nuxt index.html carried in its <style>
   block, moved here unchanged. Global and unscoped on purpose: the class names
   ARE the design (see CLAUDE.md), and scoped styles would rewrite the selectors
   the layout, the touch targets and the materials all depend on.

   The only rule that is new is `#__nuxt`, immediately below. Three comments
   further down had `<Cassette>`/JSX in them and now name the component.
   ══════════════════════════════════════════════════════════════════════════ */

/* Nuxt mounts the app into a <div id="__nuxt"> between <body> and the machine.
   `display: contents` takes that div out of the layout entirely, so `.device`
   (or `.ob-phone`) is still a direct flex item of the centring body and its
   scale/negative-margin arithmetic is untouched. */
#__nuxt { display: contents; }

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

  /* ── NATIVE INTERACTION BASE ─────────────────────────────────
     This machine is a fixed frame pretending to be an object, not a web page.
     Four things here are what stop it feeling like a page, and all four are
     invisible when they're working:
       · no blue tap flash on any surface — the buttons have their own pressed
         states and the flash lands on top of them
       · `touch-action: manipulation` drops the 300ms tap delay and double-tap
         zoom. It deliberately keeps *pinch* zoom, which `user-scalable=no`
         would take away — a fixed 402px frame is exactly where someone might
         need to zoom in, so we kill the gesture that misfires and leave the
         one that helps
       · no rubber-banding — the whole device sliding under a swipe is the
         single loudest "this is a web page" tell
       · safe-area padding on `body`, never inside `.device`, whose 874px is
         entirely spoken for (see the height budget in CLAUDE.md) */
  * { -webkit-tap-highlight-color: transparent; }

  html, body { overscroll-behavior: none; }

  /* The dark surround is whatever height is spare once the machine has its
     874px, up to 32px — never more, never less than nothing. It exists to give
     the object some air on a desktop, and it must not cost the machine any
     scale: at exactly 874px of viewport this resolves to 0, so `--fit` is 1 and
     the 44px touch targets are still 44px. Fixing it at 32px instead would
     shrink the machine on a phone whose screen is exactly the size we designed
     for. `.device` divides by the same variable, so the two can't drift. */
  :root { --surround: clamp(0px, calc((100svh - 874px) / 2), 32px); }
  /* One voice for everything the machine says in ink: rows, headers, sheets,
     modals, the customiser, the keys' silkscreen. The exceptions are all
     material, not copy — VT323 stays behind glass (the LCD, the invite readout,
     the ON AIR panel), the cassette labels keep the tape's own fonts, and the
     wordmark is Newsreader. Nothing lit is Figtree. */
  :root { --ui-font: 'Figtree', 'Helvetica Neue', Arial, sans-serif; }
  /* Titles are the wordmark's face at its natural tracking — a modal's or a
     sheet's title, regular weight, sentence case, 0%. It stops at titles: the
     bar above a list and its month bands are stamped labels on the chassis and
     stay in the UI face's tracked caps. */
  :root { --head-font: 'Newsreader', Baskerville, 'Hoefler Text', Georgia, serif; }

  /* The walnut itself, so the wood panel and the INBOX bar can't drift — see
     the comment above .wood-panel for what each layer does. Every SVG is 402
     wide (the frame's width, twice each: <svg> and <rect>) and paints at its
     intrinsic size, so anything narrower than the frame is covered without a
     seam and a shallower bar just shows the top of the grain. */
  :root {
    --walnut:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='402' height='58'%3E%3Cfilter id='wl' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.002 0.85' numOctaves='1' seed='23'/%3E%3CfeColorMatrix values='0 0 0 0 0.055 0 0 0 0 0.028 0 0 0 0 0.012 0.6 0 0 0 -0.24'/%3E%3C/filter%3E%3Crect width='402' height='58' filter='url(%23wl)'/%3E%3C/svg%3E"),
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='402' height='58'%3E%3Cfilter id='wf' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.0026 0.06' numOctaves='2' seed='5'/%3E%3CfeColorMatrix values='0 0 0 0 0.075 0 0 0 0 0.038 0 0 0 0 0.016 1.1 0 0 0 -0.45'/%3E%3C/filter%3E%3Crect width='402' height='58' filter='url(%23wf)'/%3E%3C/svg%3E"),
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='402' height='58'%3E%3Cfilter id='wg' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.0035 0.32' numOctaves='4' seed='11'/%3E%3CfeColorMatrix values='0.40 0 0 0 0.10 0.26 0 0 0 0.055 0.16 0 0 0 0.025 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='402' height='58' filter='url(%23wg)'/%3E%3C/svg%3E"),
      linear-gradient(175deg, #402615 0%, #2f1a0d 100%);
  }

  body {
    background: #1a1010;
    min-height: 100svh;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: calc(var(--surround) + env(safe-area-inset-top)) env(safe-area-inset-right)
             calc(var(--surround) + env(safe-area-inset-bottom)) env(safe-area-inset-left);
    font-family: var(--ui-font);
    background-image: radial-gradient(ellipse at center, #281818 0%, #0d0808 100%);
    touch-action: manipulation;
  }

  /* ── KEYFRAMES ─────────────────────────────────────────────── */
  /* Reel spin lives in tape-parts.css (`hubSpin`) — every tape in the app now
     draws its hubs through the shared cassette, so there is nothing left here to
     rotate. */
  /* The transcript's cursor and the LCD's RECORDING line — a cursor and a lit
     readout, the two things that should flash. No lamp on this machine does:
     every dot is dim or lit, so `ledBlinkAmber`, `ledBlinkRec` and `onAirPulse`
     are gone with the three that used to. */
  @keyframes ledBlink     { 0%,49% { opacity:1; } 50%,100% { opacity:0.1; } }
  @keyframes shake {
    0%,100% { transform:translateX(0); }
    15%  { transform:translateX(-5px); } 30%  { transform:translateX(5px); }
    45%  { transform:translateX(-4px); } 60%  { transform:translateX(4px); }
    75%  { transform:translateX(-2px); } 90%  { transform:translateX(2px); }
  }
  /* A heard message doesn't get deleted — it files itself onto its own tape */
  @keyframes fileAway {
    0%   { transform:translateX(0);     opacity:1; }
    100% { transform:translateX(26px);  opacity:0; }
  }
  @keyframes waveBar    { 0%,100% { height:4px; } 50% { height:var(--h,18px); } }
  @keyframes waveBarRec {
    0%  { height:4px; } 20% { height:var(--h2,28px); } 40% { height:6px; }
    65% { height:var(--h,22px); } 80% { height:8px; } 100% { height:4px; }
  }
  @keyframes modalIn    { from { transform:scale(0.88) translateY(12px); opacity:0; } to { transform:scale(1) translateY(0); opacity:1; } }
  @keyframes grainMove {
    0%  { transform:translate(0,0); }   10% { transform:translate(-2%,-3%); }
    20% { transform:translate(3%,1%); } 30% { transform:translate(-1%,4%); }
    40% { transform:translate(4%,-2%); }50% { transform:translate(-3%,2%); }
    60% { transform:translate(2%,-4%); }70% { transform:translate(-4%,1%); }
    80% { transform:translate(1%,3%); } 90% { transform:translate(3%,-1%); }
    100%{ transform:translate(0,0); }
  }
  @keyframes sentFadeIn { from { transform:scale(0.9); opacity:0; } to { transform:scale(1); opacity:1; } }
  @keyframes slideDown  { from { max-height:0; opacity:0; } to { max-height:300px; opacity:1; } }
  @keyframes responderGlow {
    0%,100% { background:rgba(57,255,20,0.08); }
    50%     { background:rgba(57,255,20,0.22); }
  }

  /* ── DEVICE SHELL ───────────────────────────────────────────── */
  /* The frame stays 402×874 in *layout* terms and is scaled to fit the window,
     rather than being made fluid. That's the only version where the height
     budget below still means anything: every number in it (58/762/44/10, the
     LCD's 73/88/154, the 44px touch targets) is a fixed px, so a fluid frame
     would stretch the message list while leaving the furniture the same size
     and the proportions would drift with the window. Scaling moves all of it
     together, and the iPhone 17 ratio is exact at every size because a uniform
     scale can't change it.

     `--fit` is the largest scale that still fits, capped at 1 so the machine is
     never blown up past its natural size — dividing a length by a length yields
     a unitless number, so this needs no JS and no resize listener. Width is in
     the min() too: a narrow window has to shrink the frame for the same reason
     a short one does. `svh` not `dvh`, so a mobile URL bar appearing or hiding
     doesn't rescale the machine mid-demo — it's sized for the *smallest*
     viewport and then stays put. The negative margin gives back the layout
     space the un-scaled box would otherwise still reserve, which is what stops
     the page scrolling under a shrunk machine. */
  .device {
    --fit-h: calc((100svh - var(--surround) * 2
                   - env(safe-area-inset-top) - env(safe-area-inset-bottom)) / 874px);
    --fit-w: calc((100vw
                   - env(safe-area-inset-left) - env(safe-area-inset-right)) / 402px);
    --fit: min(1, var(--fit-h), var(--fit-w));
    transform: scale(var(--fit));
    transform-origin: top center;
    margin-bottom: calc(874px * (var(--fit) - 1));

    width: 402px;
    height: 874px; /* iPhone 17 / 17 Pro logical points, 1:2.17 */
    display: flex;
    flex-direction: column;
    background: linear-gradient(170deg, #dab8b4 0%, #c8a09c 40%, #bc9090 100%);
    border-radius: 14px 14px 18px 18px;
    box-shadow:
      0 0 0 1px #9a7070, 4px 4px 0 #9a7070, 8px 8px 0 #845858,
      12px 12px 0 #6a4040, 0 24px 60px rgba(0,0,0,0.7),
      inset 0 1px 0 rgba(255,230,228,0.6), inset 0 -2px 4px rgba(0,0,0,0.2);
    position: relative;
    overflow: hidden;
    user-select: none;
  }

  /* ── SCREEN LAYOUT ───────────────────────────────────────────── */
  .screen-body {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .screen-home,
  .screen-friends,
  .screen-radio {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
  }
  .device::before {
    content: '';
    position: absolute; inset: -50%;
    width: 200%; height: 200%;
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E");
    opacity: 0.04; pointer-events: none;
    animation: grainMove 0.4s steps(1) infinite; z-index: 100;
  }

  /* ── WOOD PANEL ─────────────────────────────────────────────── */
  /* Walnut veneer, drawn rather than photographed — no image assets in this
     file. Three stretched fractal-noise layers do the work, coarse to fine:
     `wg` lays down the base grain colour, `wf` the broad cathedral figure in
     dark alpha over it, and `wl` the crisp fine lines on top. All three use an
     anisotropic baseFrequency — low across, high down — which is what makes
     the streaks run horizontally; equal x/y turns the grain back into plain
     noise. Above them, a top-lit sheen and the fine ribbing the nav slot also
     carries. */
  .wood-panel {
    height: 58px;
    background:
      repeating-linear-gradient(91deg, transparent 0px, transparent 2px, rgba(0,0,0,0.04) 2px, rgba(0,0,0,0.04) 3px),
      linear-gradient(180deg, rgba(255,236,214,0.07) 0%, rgba(255,236,214,0.01) 38%, rgba(0,0,0,0.12) 80%, rgba(0,0,0,0.32) 100%),
      var(--walnut);
    border-radius: 14px 14px 0 0;
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 18px;
    box-shadow: inset 0 -4px 8px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,220,190,0.14);
  }
  /* One wordmark, not a brand line plus a model number — the model lives on the
     machine's own cassette label instead. */
  /* Newsreader Medium, tracked -5% — that's the type spec for the mark, not a
     guess to be nudged. */
  .wordmark {
    font-family: 'Newsreader', Baskerville, 'Hoefler Text', Georgia, serif;
    font-size: 27px; font-weight: 500; line-height: 1;
    letter-spacing: -0.05em; color: #f4e7d4;
    text-shadow: 0 1px 2px rgba(0,0,0,0.6), 0 0 14px rgba(255,225,190,0.18);
  }

  /* ── NAV TABS ────────────────────────────────────────────────── */
  /* Sits at the foot of the device, directly under the transport panel. Those
     are two rows of controls stacked, so they must not read as one strip: the
     transport is raised light plastic inset from the sides, and this is a dark
     slot cut full-bleed into the chassis — the seam faces up, the well is
     shadowed inwards, and the same fine ribbing as the wood panel runs across
     it. Inside the slot sit three dark metallic-brown keys with 3px of slot
     floor showing around them, so they read as objects in a well rather than a
     painted-on strip. Different material from the transport, not a second row
     of the same buttons.

     The whole thing is held at 44px — the layout budget in CLAUDE.md counts on
     it, and any extra here comes straight out of the message list. */
  .nav-tabs {
    display: flex; gap: 3px;
    height: 44px; box-sizing: border-box;
    padding: 3px;
    background:
      repeating-linear-gradient(91deg, transparent 0px, transparent 2px, rgba(0,0,0,0.10) 2px, rgba(0,0,0,0.10) 3px),
      linear-gradient(180deg, #150908 0%, #0d0504 100%);
    border-top: 2px solid #0a0404;
    box-shadow:
      inset 0 3px 7px rgba(0,0,0,0.75),
      inset 0 -1px 0 rgba(255,180,150,0.05);
  }
  /* Raised key: lit along its top edge, dark along the bottom, a hard specular
     break at 45% for the metal, and fine vertical striae for the brushed
     finish. It casts down onto the slot floor — that shadow is what makes it a
     separate object, so the pressed state has to drop it. */
  .nav-tab {
    flex: 1; position: relative;
    display: flex; align-items: center; justify-content: center;
    padding: 0 4px;
    border: none; border-radius: 3px;
    cursor: pointer;
    font-family: var(--ui-font);
    font-size: 10px; font-weight: 500;
    letter-spacing: 0.1em; text-transform: uppercase;
    text-indent: 0.1em; /* soaks up the trailing letter-space so the label sits centred */
    color: #cdb497;
    background:
      repeating-linear-gradient(90deg, rgba(255,236,214,0.022) 0px, rgba(255,236,214,0.022) 1px, transparent 1px, transparent 3px),
      linear-gradient(180deg, #6b4731 0%, #573621 45%, #3d2415 46%, #2b180d 100%);
    box-shadow:
      inset 0 1px 0 rgba(255,224,196,0.22),
      inset 1px 0 0 rgba(255,224,196,0.05),
      inset -1px 0 0 rgba(0,0,0,0.45),
      inset 0 -1px 0 rgba(0,0,0,0.6),
      0 1px 0 rgba(0,0,0,0.75),
      0 2px 4px rgba(0,0,0,0.55);
    text-shadow: 0 -1px 0 rgba(0,0,0,0.65);
    transition: color 0.12s, box-shadow 0.1s, background 0.1s, padding-top 0.06s;
  }
  /* The key is 36px tall because the slot is held at 44px and takes 3px of floor
     plus its 2px seam out of it. 36 is under the 44px minimum, so the target
     grows to fill the slot it sits in — 5px up through the floor and seam, 3px
     down — which is how a real tab bar behaves anyway: you hit the bar, not the
     glyph. It stops at the slot's own edges, so it takes nothing from the
     transport panel above and doesn't reach into the bottom strip. The expander
     goes on ::before because .nav-tab.active::after is already the lit seam. */
  .nav-tab::before { content: ''; position: absolute; inset: -5px 0 -3px; }
  /* Hover is a mouse affordance and nothing else. A touch browser fires :hover
     on tap and then leaves it on until you touch something else, so an
     unguarded hover state sticks — a key still lit after the tab changed. Every
     hover rule in this file sits behind this query, and every one has a pressed
     twin outside it that a finger gets instead. */
  @media (hover: hover) {
    .nav-tab:hover:not(.active) { color: #eddcc4; filter: brightness(1.12); }
  }
  /* Held down but not yet the current tab. The key travels the same way the
     active one does — cast shadow gone, shadowed in from every edge, label
     nudged into the well — so a touch that never had a hover state still gets
     the metal moving under the finger. Darker overall than its neighbours, per
     the pressed-key rule; brightening the face would read as highlighted. */
  .nav-tab:active:not(.active) {
    color: #fff4e4;
    padding-top: 2px;
    box-shadow:
      inset 0 3px 6px rgba(0,0,0,0.85),
      inset 0 9px 11px -7px rgba(0,0,0,0.7),
      inset 0 -1px 0 rgba(255,214,176,0.12);
  }
  /* Pressed: the cast shadow goes, the well is shadowed in from every edge, and
     the label is nudged 1px down into it. The key has to end up *darker overall*
     than its raised neighbours — light only on the bottom lip, where it would
     bounce off the floor of the recess. Brighten the whole face instead and it
     reads as merely highlighted. Cream goes lighter, not a different hue. */
  .nav-tab.active {
    color: #fff4e4;
    padding-top: 2px;
    background:
      repeating-linear-gradient(90deg, rgba(255,236,214,0.012) 0px, rgba(255,236,214,0.012) 1px, transparent 1px, transparent 3px),
      linear-gradient(180deg, #150b05 0%, #1e1108 45%, #2a1810 86%, #452a17 100%);
    box-shadow:
      inset 0 3px 6px rgba(0,0,0,0.9),
      inset 0 9px 11px -7px rgba(0,0,0,0.75),
      inset 3px 0 5px -3px rgba(0,0,0,0.8),
      inset -3px 0 5px -3px rgba(0,0,0,0.8),
      inset 0 -1px 0 rgba(255,214,176,0.14);
    text-shadow: 0 1px 2px rgba(0,0,0,0.85), 0 0 10px rgba(255,190,130,0.4);
  }
  /* Lit seam along the top of the active key — light catching the rim of the
     recess, and it points up at the screen the key opens */
  .nav-tab.active::after {
    content: '';
    position: absolute; top: 0; left: 16%; right: 16%; height: 2px;
    background: linear-gradient(90deg, transparent, #c86030, transparent);
    border-radius: 1px;
  }

  /* ── LED INDICATORS ─────────────────────────────────────────── */
  /* Two lamps, both of which mean something: green MSG for messages waiting,
     red REC while the mic is open. No power lamp — the machine being on is
     obvious from the rest of the screen, and it was holding the green.
     Unlit is a dark tint of the lamp's own colour plus a faint inset
     highlight, so it still reads as a lens with a dead bulb behind it rather
     than a hole in the panel. */
  /* Neither lamp blinks. A light is lit or it's dim, and that's the whole
     vocabulary — anything flashing reads as the machine doing something, so a
     blink with no activity behind it is a lie.

     Each lamp is a domed lens, not a flat dot: the radial gradient curves the
     glass (bright off-centre, mid-tone, dark at the rim), the inset shadow
     seats it in the wood, and the ::after is the specular pin-light. Lit adds
     a bloom onto the panel around it. Dim keeps the dome and a fainter
     highlight — glass still catches light with the bulb dead. */
  .led-strip { display:flex; align-items:center; gap:13px; }
  .led-item  { display:flex; flex-direction:column; align-items:center; gap:3px; }
  .led-dot   {
    position:relative; width:10px; height:10px; border-radius:50%;
    border:1px solid rgba(0,0,0,0.55);
    box-shadow: inset 0 -1px 2px rgba(0,0,0,0.5), 0 1px 1px rgba(0,0,0,0.45);
  }
  .led-dot::after {
    content:''; position:absolute; top:1px; left:1.5px;
    width:3.5px; height:2.5px; border-radius:50%;
    background:rgba(255,255,255,0.8); transform:rotate(-18deg);
  }
  /* MSG — green. Lit while messages are waiting, dim at zero. */
  .led-dot.messages {
    background:radial-gradient(circle at 50% 36%, #b6ffc9 0%, #2ff05c 46%, #0b8a29 100%);
    box-shadow: inset 0 -1px 2px rgba(0,0,0,0.35), 0 0 7px rgba(34,255,85,0.85), 0 0 14px rgba(34,255,85,0.38);
  }
  .led-dot.messages.off {
    background:radial-gradient(circle at 50% 36%, #2e5f39 0%, #163a20 55%, #091d0f 100%);
    box-shadow: inset 0 -1px 2px rgba(0,0,0,0.5), 0 1px 1px rgba(0,0,0,0.45);
  }
  .led-dot.messages.off::after { background:rgba(255,255,255,0.4); }
  /* REC — red. Lit only while the mic is open, dim otherwise. */
  .led-dot.rec-led.active {
    background:radial-gradient(circle at 50% 36%, #ffc0b2 0%, #ff2a2a 46%, #9a0606 100%);
    box-shadow: inset 0 -1px 2px rgba(0,0,0,0.35), 0 0 7px rgba(255,40,40,0.85), 0 0 14px rgba(255,0,0,0.38);
  }
  .led-dot.rec-led.inactive {
    background:radial-gradient(circle at 50% 36%, #602222 0%, #3a0d0d 55%, #1e0505 100%);
    box-shadow: inset 0 -1px 2px rgba(0,0,0,0.5), 0 1px 1px rgba(0,0,0,0.45);
  }
  .led-dot.rec-led.inactive::after { background:rgba(255,255,255,0.4); }
  /* Same type as the transport keys' labels (.btn-label), stepped down: these
     name two lamps, they don't label a control you press. */
  .led-label {
    font-family: var(--ui-font); font-size:7px; font-weight:bold;
    letter-spacing:1.2px; text-transform:uppercase;
    color:#bda398; text-shadow:0 1px 2px rgba(0,0,0,0.5);
  }

  /* ── CASSETTE (HOME) ────────────────────────────────────────── */
  .cassette-section { padding:12px 14px 8px; display:flex; flex-direction:column; align-items:center; }
  /* Carousel host — side padding moves onto the scroller so cards can centre */
  .cassette-section.carousel-host { padding-left:0; padding-right:0; align-items:stretch; }

  /* ── FRIEND CAROUSEL (HOME) ─────────────────────────────────── */
  .tape-carousel {
    display:flex; gap:10px; width:100%;
    padding:0 76px;                       /* (402 − 250) ÷ 2 — lets the first and
                                             last card reach the centre */
    overflow-x:auto; overflow-y:hidden;
    scroll-snap-type:x mandatory;
    -webkit-overflow-scrolling:touch;     /* native momentum swipe on iPad */
    touch-action:pan-x;                   /* let the browser own the pan */
    overscroll-behavior-x:contain;        /* swiping past the last tape mustn't page-back */
    scrollbar-width:none; cursor:grab;
  }
  .tape-carousel::-webkit-scrollbar { display:none; }
  .tape-carousel.dragging { cursor:grabbing; scroll-snap-type:none; }
  /* while a take is recording or in review the deck can't be changed */
  .tape-carousel.locked   { overflow-x:hidden; cursor:default; }
  .carousel-item {
    flex:0 0 auto; scroll-snap-align:center; cursor:pointer;
    opacity:0.6; transform:scale(0.96);
    transition:opacity 0.18s ease, transform 0.18s ease;
  }
  .carousel-item.loaded { opacity:1; transform:scale(1); cursor:default; }
  /* The card's width is TAPE_W.deck, not a rule here — `Cassette` sets its own
     width inline, so a stylesheet can't reach it. See TAPE_W for why 250. */
  /* The loaded tape's glow ring. Stacked drop-shadows rather than a box-shadow:
     the shell is an SVG inside the box, so a box-shadow draws a rectangle around
     the card and the ring floats off the cassette's rounded corners. A
     drop-shadow follows the artwork's own silhouette. The last one restates
     tape-parts.css's contact shadow, which setting `filter` here replaces. */
  .carousel-item.loaded .tape {
    filter:
      drop-shadow(0 0 2px rgba(212,112,48,0.95))
      drop-shadow(0 0 9px rgba(212,112,48,0.55))
      drop-shadow(0 6px 16px rgba(40,26,20,0.5));
  }

  /* ── LCD DISPLAY ────────────────────────────────────────────── */
  .lcd-section {
    margin:0 16px; background:#0a1a0a; border-radius:6px;
    border:3px solid #2a1a18;
    box-shadow:inset 0 0 24px rgba(0,100,0,0.35),inset 0 2px 4px rgba(0,0,0,0.6),0 2px 10px rgba(0,0,0,0.5),0 0 0 1px rgba(180,80,60,0.2);
    overflow:hidden; position:relative;
  }
  .lcd-section::after {
    content:''; position:absolute; inset:0;
    background:repeating-linear-gradient(0deg,transparent 0px,transparent 3px,rgba(0,0,0,0.12) 3px,rgba(0,0,0,0.12) 4px);
    pointer-events:none; z-index:10;
  }
  .lcd-header { padding:7px 12px 4px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #0d2a0d; }
  .lcd-status { font-family:'VT323',monospace; font-size:20px; color:#39cc14; letter-spacing:2px; text-shadow:0 0 8px rgba(57,255,20,0.5); white-space:nowrap; }
  /* a long line (ALL MESSAGES · N WAITING) draws in a narrower cell rather than
     pushing the clock off the glass */
  .lcd-status.tight { font-size:18px; letter-spacing:0.5px; }
  .lcd-time   { font-family:'VT323',monospace; font-size:20px; color:#268c0e; letter-spacing:1px; }
  .waveform-container { height:52px; display:flex; align-items:center; justify-content:center; padding:6px 10px; gap:2px; overflow:hidden; }
  .wave-bar {
    width:5px; min-height:3px;
    background:linear-gradient(to top,#1a6e08,#39ff14); border-radius:2px 2px 1px 1px;
    box-shadow:0 0 4px rgba(57,255,20,0.4); transition:height 0.05s;
  }
  .wave-bar.playing   { animation:waveBar var(--dur,0.6s) ease-in-out infinite; animation-delay:var(--delay,0s); }
  .wave-bar.recording { animation:waveBarRec var(--dur2,0.4s) ease-in-out infinite; animation-delay:var(--delay,0s); }
  .transcript-area   { height:64px; padding:4px 10px 7px; border-top:1px solid #0d2a0d; overflow:hidden; position:relative; }
  .transcript-scroll { height:100%; overflow-y:hidden; }
  .transcript-text   { font-family:'VT323',monospace; font-size:18px; color:#2eaa0e; line-height:1.3; text-shadow:0 0 4px rgba(57,255,20,0.3); letter-spacing:0.5px; word-break:break-word; }
  .transcript-cursor { display:inline-block; width:9px; height:15px; background:#39ff14; margin-left:2px; vertical-align:middle; animation:ledBlink 0.7s step-end infinite; }
  .rec-prompt { font-family:'VT323',monospace; font-size:19px; color:#ff5555; text-shadow:0 0 8px rgba(255,68,68,0.6); letter-spacing:1px; animation:ledBlink 1s step-end infinite; }
  .lcd-error  { font-family:'VT323',monospace; font-size:18px; color:#ffb347; text-shadow:0 0 8px rgba(255,150,40,0.45); letter-spacing:1px; line-height:1.3; }
  .lcd-error-sub { font-family:'Courier New',monospace; font-size:9px; color:#9a7328; letter-spacing:0.5px; margin-top:3px; }
  .review-lcd-line { font-family:'VT323',monospace; font-size:18px; color:#2eaa0e; text-shadow:0 0 4px rgba(57,255,20,0.3); letter-spacing:0.5px; display:flex; justify-content:space-between; gap:10px; }
  .review-timer    { font-family:'VT323',monospace; color:#39ff14; text-shadow:0 0 6px rgba(57,255,20,0.45); flex-shrink:0; }
  .review-lcd-sub  { font-family:'Courier New',monospace; font-size:9px; color:#1e7a0a; letter-spacing:0.5px; margin-top:5px; }

  /* Idle LCD — nothing playing, recording or in review, so the waveform and the
     empty transcript window fold away and the message list takes the 81px back.
     Same glass, scanlines and glow, just shorter. The two collapsed heights are
     the tallest thing each state can hold: one status line, or an amber error
     with its hint underneath — neither may clip, and neither forces an expand. */
  .waveform-container,
  .transcript-area { transition:height 180ms ease, padding 180ms ease; }
  .lcd-section.collapsed .waveform-container { height:0; padding-top:0; padding-bottom:0; }
  .lcd-section.collapsed .transcript-area { height:35px; }
  .lcd-section.collapsed.has-error .transcript-area { height:50px; }

  /* ── MESSAGE LIST (HOME) ────────────────────────────────────── */
  .messages-section {
    flex: 1; min-height: 0;
    margin:10px 16px 0;
    border:2px solid #b89090; border-radius:6px;
    background:linear-gradient(160deg,#eddcd8 0%,#e0c8c4 100%);
    box-shadow:inset 0 1px 0 rgba(255,240,238,0.7),inset 0 -1px 3px rgba(0,0,0,0.08);
    overflow:hidden;
    display:flex; flex-direction:column;
  }
  .messages-rows-scroll {
    flex: 1; min-height: 0;
    overflow-y: auto; scrollbar-width: none;
    overscroll-behavior-y: contain;   /* reaching the end of a tape must not drag the device */
  }
  .messages-rows-scroll::-webkit-scrollbar { display: none; }
  /* The same walnut as the wood panel at the top of the machine, so the list
     reads as sitting in the chassis rather than on a painted strip. Only the
     top-lit sheen is restated over the grain — the panel's ribbing belongs to
     the panel and the nav slot, and at 28px tall it would be noise.
     Tracked caps in the UI face, not the heading serif: this bar is a stamped
     label on the chassis and Newsreader read as a magazine standfirst over the
     messages under it. Nothing on the right either way — see the template. */
  .messages-header {
    background:
      linear-gradient(180deg, rgba(255,236,214,0.06) 0%, rgba(255,236,214,0.01) 45%, rgba(0,0,0,0.14) 100%),
      var(--walnut);
    padding:6px 12px; font-family: var(--ui-font); font-size:16px; font-weight:700; color:#ffd9b0;
    letter-spacing:2px; display:flex; justify-content:space-between; align-items:center;
    box-shadow:inset 0 1px 0 rgba(255,220,190,0.12), inset 0 -3px 6px rgba(0,0,0,0.4);
    text-shadow:0 1px 2px rgba(0,0,0,0.55);
  }
  /* Month band inside a tape's history — sticky so you always know how far back
     you've scrolled. Kept slim: the list window is only ~75px in this frame, so
     a band must not cost a whole row. */
  .msg-month-header {
    position:sticky; top:0; z-index:3;
    background:linear-gradient(90deg,#3a2018 0%,#2a1210 100%);
    padding:2px 12px 3px; font-family: var(--ui-font); font-size:12px; font-weight:700; color:#d4a060;
    letter-spacing:2.5px; line-height:1.1;
  }
  /* min-height carries the 44px target itself — a friend's row is one line, and
     padding alone would leave it at 33 */
  .message-row {
    display:flex; align-items:center; gap:10px; padding:9px 12px; min-height:44px; cursor:pointer;
    border-bottom:1px solid rgba(160,80,80,0.12); transition:background 0.15s; position:relative;
    box-sizing:border-box;
  }
  .message-row:last-child { border-bottom:none; }
  @media (hover: hover) { .message-row:hover { background:rgba(200,120,100,0.1); } }
  /* Pressed twin. A row is the biggest tap target on the screen and it starts
     audio — with hover unavailable it was the one surface that gave no feedback
     at all between the tap and the sound. */
  .message-row:active { background:rgba(200,120,100,0.18); }
  .message-row.selected { background:linear-gradient(90deg,rgba(180,90,80,0.18) 0%,rgba(180,90,80,0.06) 100%); border-left:3px solid #b85858; padding-left:9px; }
  .message-row.shaking { animation:shake 0.45s ease; }
  /* Marked heard — slides off the queue towards its own tape */
  .message-row.filing { animation:fileAway 0.32s ease forwards; pointer-events:none; }
  .msg-led { width:8px; height:8px; border-radius:50%; flex-shrink:0; border:1px solid rgba(0,0,0,0.25); }
  .msg-led.unread { background:#22cc44; box-shadow:0 0 5px #22cc44; }
  .msg-led.played { background:#a08888; }
  .msg-info  { flex:1; min-width:0; }
  .msg-name  { font-family: var(--ui-font); font-size:13px; font-weight:bold; color:#2a1010; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .msg-meta  { font-family: var(--ui-font); font-size:10px; color:#7a5050; letter-spacing:0.3px; margin-top:2px; }
  /* Standing in for the name on a friend's tape — same slot, lighter voice, since
     a date shouldn't shout the way a person's name does */
  .msg-when  { font-weight:normal; font-size:12px; color:#5a3230; letter-spacing:0.3px; }
  /* A running time is a readout, not copy — it keeps VT323 like the LCD's own
     digits, which is also why it doesn't need Figtree's tabular niceties. */
  .msg-duration { font-family:'VT323',monospace; font-size:17px; color:#8a5050; flex-shrink:0; }
  .messages-empty { font-family: var(--ui-font); font-size:10px; color:#9a7070; letter-spacing:1px; text-align:center; padding:16px 12px; }
  /* The little buttons on a row — one shell, the ink says what it does */
  /* 24px, up from 19. Two 44×44 targets need 88px of row plus a gap between
     them, and 19px keys 10px apart span only 48 — no amount of invisible
     expansion fits, the keys themselves have to move. Growing them to 24 and
     opening the gap to 28 puts the centres 52px apart, which is 44 of target
     each with 8px of dead row between. This is the one place the HIG minimum
     costs visible artwork; everything else on the machine gets there with a
     pseudo-element and looks identical. */
  .msg-row-btn {
    flex-shrink:0; width:24px; height:24px; padding:0; border-radius:4px; cursor:pointer;
    position:relative;                  /* anchors the hit-area expander below */
    border:1px solid rgba(120,60,55,0.45);
    background:linear-gradient(180deg,#f0e2de 0%,#dcc8c4 100%);
    box-shadow:0 1px 0 rgba(120,60,55,0.35), inset 0 1px 0 rgba(255,255,255,0.8);
    font-family:'Courier New',monospace; font-size:12px; font-weight:bold; line-height:1;
    display:flex; align-items:center; justify-content:center;
    transition:transform 0.07s, box-shadow 0.07s, background 0.12s, color 0.12s;
  }
  .msg-row-btn:active { transform:translateY(1px); box-shadow:inset 0 1px 3px rgba(0,0,0,0.3); }
  /* 24px key, 44×44 target. 11px a side, not 10: `inset` resolves against the
     padding box, and the 1px border means that box is 22px, not 24. */
  .msg-row-btn::after { content:''; position:absolute; inset:-11px; }
  /* The gap that makes the two targets legal. Base row gap is 10px; 18 more
     takes it to 28, so with 10px of expansion either side the ✓ and ✕ targets
     end up 8px apart. That separation is the point, not a leftover: one of these
     deletes a message for real, and a target that occasionally deletes when you
     meant to file is worse than a small one. Only the queue shows both keys — on
     a friend's tape the ✕ is alone and needs no offset. */
  .msg-check-btn + .msg-delete-btn { margin-left:18px; }
  .msg-delete-btn        { color:#8a3030; }
  .msg-delete-btn:active { background:linear-gradient(180deg,#b8564e 0%,#8e332b 100%); color:#ffe4e0; }
  @media (hover: hover) {
    .msg-delete-btn:hover { background:linear-gradient(180deg,#b8564e 0%,#8e332b 100%); color:#ffe4e0; }
  }
  /* Mark heard — only on the machine's own tape, where the row leaving the list
     is the whole point. Green like the unread LED it puts out. */
  .msg-check-btn         { color:#2c7030; font-size:14px; }
  .msg-check-btn:active  { background:linear-gradient(180deg,#4f9c52 0%,#2a6c2e 100%); color:#eaffe6; }
  @media (hover: hover) {
    .msg-check-btn:hover { background:linear-gradient(180deg,#4f9c52 0%,#2a6c2e 100%); color:#eaffe6; }
  }
  /* Outgoing reply — my own voice on someone else's tape. Context, not the main
     event: inset from the left, narrower, dimmer, and no unread LED at all. An
     incoming message has to stay the loudest thing in the list. */
  /* min-height:44px, stated rather than arrived at — its ✕ can only claim a 44px
     target if the row it sits in is at least that deep, and at 38px the expander
     had to stay short to avoid stealing taps from the rows either side. Setting
     it directly beats padding arithmetic, which landed a pixel short once the
     font metrics were in. Still visually subordinate: the inset, the dimmer type
     and the dashed rule do that work, not the height. */
  .message-row.outgoing {
    margin-left:30px; margin-right:10px; padding:6px 10px; min-height:44px; opacity:0.7;
    background:rgba(120,70,60,0.04);
    border-bottom:1px dashed rgba(160,80,80,0.10);
  }
  .message-row.outgoing:active { background:rgba(160,90,80,0.12); opacity:1; }
  @media (hover: hover) {
    .message-row.outgoing:hover { background:rgba(160,90,80,0.07); opacity:0.9; }
  }
  .message-row.outgoing.playing {
    opacity:1; background:rgba(160,90,80,0.09);
    border-left:2px solid rgba(160,90,80,0.5); padding-left:8px;
  }
  .msg-out-mark { flex-shrink:0; width:8px; text-align:center; font-family:'Courier New',monospace; font-size:11px; color:#a08078; }
  .message-row.outgoing .msg-name     { font-size:11px; color:#7a5a52; letter-spacing:1.5px; }
  .message-row.outgoing .msg-meta     { font-size:9px;  color:#9a7a72; }
  .message-row.outgoing .msg-duration { font-size:14px; color:#a58080; }
  /* 0.85, not 0.65. These sit inside a row that is already at 0.7, so the two
     opacities multiply and the old value left the keys at 0.46 — legible only
     once a mouse found them. A control mustn't need hover to look like a
     control, and the row stays subordinate on its inset and its dimmer type. */
  .message-row.outgoing .msg-row-btn { width:16px; height:16px; font-size:9px; opacity:0.85; }
  /* 16px key, 44×44 target. It can take the full expansion in both directions
     now the row is 44px deep, and its only neighbour is the duration, which is
     inert — outgoing rows carry a ✕ and nothing else. */
  .message-row.outgoing .msg-row-btn::after { inset:-15px; }
  .message-row.outgoing:active .msg-row-btn { opacity:1; }
  @media (hover: hover) {
    .message-row.outgoing:hover .msg-row-btn { opacity:1; }
  }
  /* Review panel — takes the message list's slot while a take is loaded */
  .review-panel   { flex:1; min-height:0; display:flex; flex-direction:column; }
  .review-row     { cursor:default; padding:7px 12px 7px 9px; background:linear-gradient(90deg,rgba(180,90,80,0.18) 0%,rgba(180,90,80,0.06) 100%); border-left:3px solid #b85858; }
  /* The take under review isn't a tappable row — PLAY and the buttons below it
     are what drive it. So it opts out of both of `.message-row`'s state layers,
     the pressed one included, or pressing anywhere on it would suggest it does
     something. */
  .review-row:hover,
  .review-row:active { background:linear-gradient(90deg,rgba(180,90,80,0.18) 0%,rgba(180,90,80,0.06) 100%); }
  .review-actions { display:flex; gap:6px; padding:7px 12px 9px; flex-shrink:0; }
  /* These three opt out of the 44px min-height the other .stop-rec-btn instances
     take. In review the LCD is at its full 154px, which leaves this panel 75px
     for the take row *and* these buttons — at 44px each they'd eat 60 of it and
     crush the row above them. So they stay ~33px and buy the target back with a
     vertical expander: 6px up under the review row, which is inert by design,
     and 6px down into the panel's own bottom padding. Sideways they're already
     ~102px, and expanding that way would collide with each other. */
  .review-actions .stop-rec-btn { min-height:0; padding:9px 4px; font-size:9px; letter-spacing:0.5px; position:relative; }
  .review-actions .stop-rec-btn::after { content:''; position:absolute; inset:-8px 0; }

  /* ── CONTROL PANEL ──────────────────────────────────────────── */
  .control-panel {
    margin:12px 16px 16px;
    background:linear-gradient(160deg,#b8b0b0 0%,#a09898 50%,#988888 100%);
    border-radius:8px; border:2px solid #887070; padding:12px 10px 10px;
    box-shadow:inset 0 1px 0 rgba(255,240,240,0.5),inset 0 -2px 4px rgba(0,0,0,0.2),0 2px 8px rgba(0,0,0,0.25);
  }
  .buttons-row { display:flex; justify-content:space-between; gap:6px; }
  .btn-wrap    { display:flex; flex-direction:column; align-items:center; gap:5px; flex:1; }
  .btn-label   { font-size:9px; letter-spacing:1.5px; color:#f0e0dc; text-transform:uppercase; font-family: var(--ui-font); font-weight:bold; text-shadow:0 1px 2px rgba(0,0,0,0.5); }
  .ctrl-btn {
    width:100%; height:48px; border:none; border-radius:5px 5px 4px 4px; cursor:pointer;
    font-family: var(--ui-font); font-size:15px; font-weight:bold;
    position:relative; transition:transform 0.07s,box-shadow 0.07s;
    outline:none; -webkit-tap-highlight-color:transparent;
  }
  .ctrl-btn.normal {
    background:linear-gradient(180deg,#f0ece8 0%,#ddd4cc 60%,#ccc4b8 100%); color:#3a2020;
    box-shadow:0 5px 0 #8a7878,0 6px 2px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.9),inset 0 -2px 3px rgba(0,0,0,0.1);
  }
  .ctrl-btn.red {
    background:linear-gradient(180deg,#d84848 0%,#b83030 60%,#a02020 100%); color:#ffe8e8;
    box-shadow:0 5px 0 #681010,0 6px 2px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,190,190,0.5),inset 0 -2px 3px rgba(0,0,0,0.2);
  }
  .ctrl-btn.dark {
    background:linear-gradient(180deg,#484040 0%,#302828 60%,#201818 100%); color:#e8dcd8;
    box-shadow:0 5px 0 #100808,0 6px 2px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,220,210,0.1),inset 0 -2px 3px rgba(0,0,0,0.3);
  }
  .ctrl-btn:active,.ctrl-btn.pressed { transform:translateY(4px) !important; box-shadow:0 1px 0 #8a7878,0 1px 1px rgba(0,0,0,0.2),inset 0 2px 5px rgba(0,0,0,0.18) !important; }
  .ctrl-btn.red:active,.ctrl-btn.red.pressed { box-shadow:0 1px 0 #681010,0 1px 1px rgba(0,0,0,0.2),inset 0 2px 5px rgba(0,0,0,0.25) !important; }
  .ctrl-btn.dark:active,.ctrl-btn.dark.pressed { box-shadow:0 1px 0 #100808,0 1px 1px rgba(0,0,0,0.2),inset 0 2px 5px rgba(0,0,0,0.3) !important; }
  @media (hover: hover) { .ctrl-btn:hover:not(:active) { filter:brightness(1.07); } }
  /* SPEED button while 1.5× is engaged */
  .ctrl-btn.dark.lit { color:#ffce7a; text-shadow:0 0 9px rgba(255,180,80,0.7); }
  .btn-icon { font-size:14px; display:block; line-height:1; }

  /* ── MODALS ─────────────────────────────────────────────────── */
  .modal-backdrop { position:absolute; inset:0; background:rgba(0,0,0,0.65); display:flex; align-items:center; justify-content:center; z-index:200; border-radius:14px; backdrop-filter:blur(2px); }
  .modal-card { width:288px; background:linear-gradient(160deg,#ded0cc 0%,#cebcb8 100%); border-radius:10px; border:2px solid #a88080; padding:20px; box-shadow:0 16px 48px rgba(0,0,0,0.6),inset 0 1px 0 rgba(255,240,238,0.6); animation:modalIn 0.2s ease-out; }
  .modal-title { font-family: var(--head-font); font-size:24px; font-weight:400; color:#2a1010; letter-spacing:0; text-align:center; margin-bottom:16px; text-shadow:0 1px 0 rgba(255,240,238,0.6); }
  /* min-height, not padding: these are the primary buttons in a modal and they
     were 26–31px tall. Nothing invisible would do here — a stack of full-width
     buttons has neighbours above and below, so expanding their targets would
     overlap them. They get to be 44px for real. */
  .modal-cancel { width:100%; min-height:44px; padding:9px; background:#8a6060; border:none; border-radius:5px; color:#f0e4e0; font-family: var(--ui-font); font-size:11px; letter-spacing:2px; cursor:pointer; box-shadow:0 2px 0 #5a3838; transition:transform 0.07s; }
  .modal-cancel:active { transform:translateY(1px); }
  .stop-rec-options { display:flex; flex-direction:column; gap:8px; }
  .stop-rec-btn { width:100%; min-height:44px; padding:12px 10px; border-radius:6px; border:2px solid #a08080; cursor:pointer; font-family: var(--ui-font); font-size:10px; font-weight:bold; letter-spacing:1px; text-transform:uppercase; text-align:center; transition:transform 0.07s,box-shadow 0.07s; }
  .stop-rec-btn.send    { background:linear-gradient(160deg,#4a8a3a 0%,#336624 100%); color:#d0f0c0; box-shadow:0 3px 0 #224416,inset 0 1px 0 rgba(255,255,255,0.15); }
  .stop-rec-btn.keep    { background:linear-gradient(160deg,#e0d0cc 0%,#c8b8b4 100%); color:#2a1010; box-shadow:0 3px 0 #886060,inset 0 1px 0 rgba(255,240,238,0.5); }
  .stop-rec-btn.discard { background:linear-gradient(160deg,#8a3030 0%,#621818 100%); color:#ffd0d0; box-shadow:0 3px 0 #3a0808,inset 0 1px 0 rgba(255,180,180,0.15); }
  .stop-rec-btn:active { transform:translateY(2px); box-shadow:none !important; }
  @media (hover: hover) { .stop-rec-btn:hover { filter:brightness(1.08); } }
  .sent-confirm-card { animation:sentFadeIn 0.18s ease-out; text-align:center; }
  .sent-icon  { font-size:32px; margin-bottom:10px; display:block; }
  .sent-title { font-family: var(--head-font); font-size:22px; font-weight:400; color:#3a2a10; letter-spacing:0; margin-bottom:6px; text-shadow:0 1px 0 rgba(255,255,255,0.5); }
  .sent-sub   { font-family: var(--ui-font); font-size:10px; color:#7a6040; letter-spacing:0.5px; }

  /* ── BOTTOM STRIP ───────────────────────────────────────────── */
  .bottom-strip { height:10px; background:linear-gradient(90deg,#6a1e10 0%,#4e1008 50%,#6a1e10 100%); border-radius:0 0 18px 18px; box-shadow:inset 0 2px 4px rgba(0,0,0,0.35); }

  /* ══════════════════════════════════════════════════════════════
     TAPE SLOT — what wraps the shared cassette
     The cassette's own artwork is tape-parts.css. Nothing in this file draws a
     shell, a reel or a label any more: a tape is `Cassette.vue`, and
     `.tape-slot` is the frame around it that carries the
     things the artwork has no opinion about — whether it can be tapped, the
     pencil in its corner, the settle animation. .seg-tabs is the segmented
     control, used by the customiser. The friends screen has no LCD of its own.
     ══════════════════════════════════════════════════════════════ */

  /* No overflow:hidden, and nothing that starts a new stacking context beyond
     what's needed: the cassette hangs a drop-shadow outside its own box and the
     pencil hangs its hit area outside this one. */
  .tape-slot { position: relative; }

  /* A tape you can tap — every friend's cassette (opens what you can do with it)
     and your own (hands out your link). The smallest of them is 100px tall and
     161 wide, so the target is already well past 44 on both axes. The lift is the
     mouse flourish; the press is what a finger gets. */
  .tape-slot.tappable { cursor: pointer; transition: transform 0.14s, filter 0.14s; }
  @media (hover: hover) {
    .tape-slot.tappable:hover { transform: translateY(-3px); filter: brightness(1.06); }
  }
  .tape-slot.tappable:active { transform: translateY(1px); filter: brightness(1.08); }

  /* Freshly saved tape spinning into place on the shelf */
  @keyframes tapeSettle {
    0%   { transform: scale(0.9) rotate(-3deg); }
    55%  { transform: scale(1.04) rotate(1.2deg); }
    100% { transform: scale(1) rotate(0deg); }
  }
  .tape-slot.settling { animation: tapeSettle 0.6s cubic-bezier(0.2,0.9,0.3,1); }

  /* Segmented control */
  /* No `overflow: hidden` here on purpose. It used to clip the tabs' square
     corners to the container's radius, but it also clipped their hit-area
     expanders, which is what keeps a 25px-tall tab tappable. The end tabs round
     their own outer corners instead, so the control looks the same and the
     targets survive. */
  /* No margin of its own — the customiser page places it (`.tp-tabs`), which is
     the only screen that uses this control. */
  .seg-tabs {
    display: flex;
    border-radius: 5px;
    border: 1.5px solid #b89090;
  }
  .seg-tab-btn:first-child { border-radius: 4px 0 0 4px; }
  .seg-tab-btn:last-child  { border-radius: 0 4px 4px 0; }
  /* ~120×26 visible, ~120×46 target. The expansion is vertical only — the tabs
     are wide already and sit shoulder to shoulder, so there's nothing to gain
     sideways and a neighbour to steal from. Up goes into the gap under the tape's
     stage and down into the control's own bottom margin; both are inert, and both
     are big enough to take the 10px (22 above, 16 below). */
  .seg-tab-btn::after { content: ''; position: absolute; inset: -10px 0; }
  .seg-tab-btn {
    flex: 1; padding: 7px 4px; position: relative;
    font-family: var(--ui-font); font-size: 10px; font-weight: bold;
    letter-spacing: 1.5px; text-transform: uppercase; text-align: center;
    cursor: pointer; border: none;
    background: linear-gradient(160deg, #e0ccc8 0%, #d0b8b4 100%);
    color: #8a5050;
    border-right: 1.5px solid #b89090;
    transition: background 0.12s, color 0.12s;
  }
  .seg-tab-btn:last-child { border-right: none; }
  .seg-tab-btn.active {
    background: linear-gradient(160deg, #2a1a18 0%, #1a0e0c 100%);
    color: #ffbb88;
  }
  .seg-tab-btn:active:not(.active) { background: linear-gradient(160deg, #c0a8a4 0%, #b09894 100%); }
  @media (hover: hover) {
    .seg-tab-btn:hover:not(.active) { background: linear-gradient(160deg, #d0b8b4 0%, #c0a8a4 100%); }
  }

  /* ══════════════════════════════════════════════════════════════
     TAPES SCREEN
     Every cassette here is `Cassette.vue`. What this file sets
     is how wide each one is; the 1.604 ratio gives it its height, and every
     colour comes from the module's own part maps.
     ══════════════════════════════════════════════════════════════ */

  /* MY TAPE — same panel material as the shelf below it, but it doesn't flex:
     the cassette takes the panel's width and the shelf gets everything left
     over. */
  .my-tape-panel {
    flex: 0 0 auto;
    margin: 10px 16px 0;
    border: 2px solid #b89090; border-radius: 6px;
    background: linear-gradient(160deg,#eddcd8 0%,#e0c8c4 100%);
    box-shadow: inset 0 1px 0 rgba(255,240,238,0.7), inset 0 -1px 3px rgba(0,0,0,0.08);
    overflow: hidden;
    display: flex; flex-direction: column;
  }
  .my-tape-body { padding: 11px; }

  /* The two keys under your own tape — SHARE LINK and EDIT TAPE. The nav's
     material and voice: a slot cut into the surface with the seam facing up, the
     well shadowed inwards, and two raised keys with 3px of floor showing round
     them. The metal is pink instead of brown because this panel is pink, so the
     keys read as part of the tray they're cut into rather than as the nav
     escaped up the screen — same construction, this machine's other colourway.
     They're 44px tall rather than the nav's 36 + expander: the panel clips its
     overflow, so a pseudo-element reaching past the slot would be thrown away
     against that edge. The keys carry the minimum themselves instead.

     The well is cream rather than the nav's near-black. The nav is a slot cut
     into the dark chassis, so its floor is that wood in shadow; this one is cut
     into a pale pink tray, and borrowing the dark floor drew a hard black frame
     round the keys that read as a stroke rather than a recess. Cream keeps the
     depth — a shade darker than the key faces, shadowed in from the top seam —
     without the outline. */
  .my-tape-keys {
    display: flex; gap: 3px;
    padding: 3px;
    background:
      repeating-linear-gradient(91deg, transparent 0px, transparent 2px, rgba(150,105,105,0.07) 2px, rgba(150,105,105,0.07) 3px),
      linear-gradient(180deg, #e2cbc6 0%, #ecdcd6 100%);
    border-top: 1px solid rgba(255,250,246,0.65);
    box-shadow:
      inset 0 2px 4px rgba(122,80,80,0.22),
      inset 0 -1px 0 rgba(255,250,246,0.5);
  }
  .my-tape-key {
    flex: 1; min-height: 44px; position: relative;
    display: flex; align-items: center; justify-content: center;
    padding: 0 4px;
    border: none; border-radius: 3px;
    cursor: pointer;
    font-family: var(--ui-font);
    font-size: 10px; font-weight: 500;
    letter-spacing: 0.1em; text-transform: uppercase;
    text-indent: 0.1em;  /* soaks up the trailing letter-space, same as the nav */
    color: #4a2026;
    background:
      repeating-linear-gradient(90deg, rgba(255,255,255,0.05) 0px, rgba(255,255,255,0.05) 1px, transparent 1px, transparent 3px),
      linear-gradient(180deg, #f4c8cc 0%, #e6aab2 45%, #d38f99 46%, #c07a86 100%);
    box-shadow:
      inset 0 1px 0 rgba(255,248,248,0.75),
      inset 1px 0 0 rgba(255,248,248,0.25),
      inset -1px 0 0 rgba(122,80,80,0.14),
      inset 0 -1px 0 rgba(122,80,80,0.22),
      0 1px 0 rgba(140,92,92,0.35),
      0 2px 4px rgba(122,80,80,0.28);
    text-shadow: 0 1px 0 rgba(255,248,248,0.5);
    transition: color 0.12s, box-shadow 0.1s, background 0.1s, padding-top 0.06s;
  }
  @media (hover: hover) {
    .my-tape-key:hover { filter: brightness(1.06); }
  }
  /* Pressed: the cast shadow goes, the well is shadowed in from every edge and
     the label drops 1px into it — the nav's travel, so a finger gets the same
     movement here. Darker overall than at rest, never brighter. */
  .my-tape-key:active {
    color: #2e1014;
    padding-top: 2px;
    background:
      repeating-linear-gradient(90deg, rgba(255,255,255,0.03) 0px, rgba(255,255,255,0.03) 1px, transparent 1px, transparent 3px),
      linear-gradient(180deg, #b8737f 0%, #c47f8a 45%, #d0919a 86%, #e0a8af 100%);
    box-shadow:
      inset 0 3px 6px rgba(92,50,55,0.45),
      inset 0 9px 11px -7px rgba(92,50,55,0.35),
      inset 3px 0 5px -3px rgba(92,50,55,0.32),
      inset -3px 0 5px -3px rgba(92,50,55,0.32),
      inset 0 -1px 0 rgba(255,240,240,0.4);
    text-shadow: none;
  }

  .friends-panel { margin-bottom: 16px; }   /* no control panel below to space it */
  .friends-grid  {
    display: grid; gap: 12px; padding: 12px;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    align-content: start;
  }
  /* Empty collection — the ghost is the only thing on screen */
  .friends-empty { flex: 1; display: flex; align-items: center; justify-content: center; }
  .friends-empty .ghost-tape { width: 168px; }

  /* Ghost outline cassette — the always-present ADD entry. Not a `<Cassette>`:
     it's the absence of one. It borrows the module's ratio so it sits level with
     the real tapes beside it in the grid, and draws itself as an outline. */
  .ghost-tape {
    aspect-ratio: 1.604;
    border-radius: 8px; border: 2px dashed rgba(122,80,80,0.5);
    background: rgba(255,255,255,0.10);
    box-shadow: inset 0 1px 0 rgba(255,240,238,0.4);
    cursor: pointer;
    display: flex; flex-direction: column;
    transition: transform 0.14s, background 0.14s, border-color 0.14s;
  }
  @media (hover: hover) {
    .ghost-tape:hover {
      transform: translateY(-3px);
      background: rgba(255,255,255,0.2);
      border-color: rgba(122,80,80,0.8);
    }
  }
  /* The lift is a mouse flourish; the press is what a finger sees. It needs the
     background and border change too now, or tapping the ghost cassette — the
     first thing the demo touches — acknowledges nothing. */
  .ghost-tape:active {
    transform: translateY(0);
    background: rgba(255,255,255,0.24);
    border-color: rgba(122,80,80,0.9);
  }
  .ghost-reel-zone { flex: 1; display: flex; align-items: center; justify-content: center; gap: 22px; padding: 11px 14px 6px; }
  .ghost-reel  { width: 30px; height: 30px; border-radius: 50%; border: 2px dashed rgba(122,80,80,0.45); }
  .ghost-label {
    min-height: 30px; flex-shrink: 0; padding: 5px 6px 6px;
    display: flex; align-items: center; justify-content: center;
    border-top: 1.5px dashed rgba(122,80,80,0.4);
    font-family: 'VT323', monospace; font-size: 13px; letter-spacing: 0.5px;
    color: #7a5050; text-align: center; line-height: 1.05;
  }

  /* ── ADD-FRIEND / INVITE FLOW (modal) ─────────────────────── */
  .invite-card { width: 316px; }
  .invite-sub {
    font-family: var(--ui-font); font-size: 9px; color: #7a5050;
    letter-spacing: 0.5px; text-align: center; margin: -11px 0 14px; line-height: 1.4;
  }
  /* A text field is a touch target too — this one was 35px. It grows for real
     rather than behind a pseudo-element: an overlay on an input swallows the tap
     that should place the caret. */
  .invite-name-input {
    width: 100%; min-height: 44px; padding: 10px 11px; margin-bottom: 14px;
    background: linear-gradient(180deg, #f4ece8 0%, #e6dad6 100%);
    border: 2px solid #a88080; border-radius: 5px;
    box-shadow: inset 0 2px 4px rgba(0,0,0,0.18);
    font-family: var(--ui-font); font-size: 14px; font-weight: bold;
    color: #2a1010; letter-spacing: 1px; outline: none;
  }
  .invite-name-input::placeholder { color: #ab8f8f; font-weight: normal; letter-spacing: 2px; }
  .invite-name-input:focus { border-color: #8a5a4a; box-shadow: inset 0 2px 4px rgba(0,0,0,0.18), 0 0 0 2px rgba(200,120,80,0.25); }
  /* A pasted link is longer than a name and reads as one string, so it drops the
     bold and the wide tracking the name field uses to look stamped. */
  .paste-link-input { font-weight: normal; letter-spacing: 0.3px; }
  .paste-link-input::placeholder { letter-spacing: 0.5px; }

  /* Link field — same LCD recipe as the machine's other screens */
  .invite-lcd {
    background: #0a1a0a; border: 3px solid #2a1a18; border-radius: 6px;
    box-shadow: inset 0 0 18px rgba(0,100,0,0.3), 0 2px 8px rgba(0,0,0,0.4);
    overflow: hidden; position: relative; margin-bottom: 10px;
  }
  .invite-lcd::after {
    content: ''; position: absolute; inset: 0;
    background: repeating-linear-gradient(0deg, transparent 0px, transparent 3px, rgba(0,0,0,0.1) 3px, rgba(0,0,0,0.1) 4px);
    pointer-events: none; z-index: 5;
  }
  .invite-lcd-inner { padding: 8px 11px 10px; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; }
  .invite-lcd-inner::-webkit-scrollbar { display: none; }
  .invite-lcd-label { font-family: 'Courier New', monospace; font-size: 8px; color: #1e7a0a; letter-spacing: 1.5px; margin-bottom: 4px; }
  .invite-lcd-url   { font-family: 'VT323', monospace; font-size: 17px; color: #39ff14; text-shadow: 0 0 7px rgba(57,255,20,0.45); letter-spacing: 0.5px; white-space: nowrap; }

  .invite-copy-btn {
    width: 100%; min-height: 44px; padding: 11px; margin-bottom: 15px;
    border-radius: 5px; border: 2px solid #887070; cursor: pointer;
    background: linear-gradient(180deg, #f0ece8 0%, #ddd4cc 60%, #ccc4b8 100%); color: #3a2020;
    box-shadow: 0 4px 0 #8a7878, inset 0 1px 0 rgba(255,255,255,0.9);
    font-family: var(--ui-font); font-size: 11px; font-weight: bold; letter-spacing: 2px;
    transition: transform 0.07s, box-shadow 0.07s, background 0.12s, color 0.12s;
  }
  @media (hover: hover) { .invite-copy-btn:hover:not(:active) { filter: brightness(1.05); } }
  .invite-copy-btn:active { transform: translateY(3px); box-shadow: 0 1px 0 #8a7878; }
  .invite-copy-btn.copied {
    background: linear-gradient(180deg, #5aa246 0%, #3d7a2c 100%); color: #eaffdc;
    border-color: #2c5a1e; box-shadow: 0 4px 0 #224416, inset 0 1px 0 rgba(255,255,255,0.25);
  }
  .invite-copy-btn.failed {
    background: linear-gradient(180deg, #b8564e 0%, #8e332b 100%); color: #ffe0dc;
    border-color: #5c1610; box-shadow: 0 4px 0 #3a0808, inset 0 1px 0 rgba(255,190,190,0.2);
  }

  /* Why you'd keep sharing. Copy only — nothing is gated on the count yet. */
  .unlock-note {
    margin: 0 0 14px; padding: 9px 10px;
    border: 1.5px dashed rgba(122,80,80,0.5); border-radius: 5px;
    background: rgba(255,255,255,0.35);
    font-family: var(--ui-font); font-size: 9px; line-height: 1.5;
    color: #6a4038; letter-spacing: 0.3px; text-align: center;
  }

  /* Their tape, shown in the popup you get from tapping it. Read-only, so it
     carries no pencil and isn't tappable in here either. */
  .friend-menu-preview { display: flex; justify-content: center; margin: 0 0 14px; }

  /* A friend's tape opens a sheet that rises out of the foot of the machine
     rather than a card floating in the middle. It's the machine's own plastic,
     not a system sheet: same body as a modal card, top corners rounded to the
     chassis radius, and the device's overflow clips the bottom two.
     There's no close key — tapping off it is the only way out, which is why
     nothing in here can read as a step you have to finish. */
  .sheet-backdrop { align-items: flex-end; }
  .tape-sheet {
    position: relative; width: 100%;
    padding: 15px 20px 22px;
    border: 2px solid #a88080; border-bottom: none;
    border-radius: 14px 14px 0 0;
    background: linear-gradient(160deg, #ded0cc 0%, #cebcb8 100%);
    box-shadow: 0 -14px 40px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,240,238,0.6);
    animation: sheetIn 0.22s cubic-bezier(0.2,0.9,0.3,1);
  }
  @keyframes sheetIn { from { transform: translateY(100%); } to { transform: translateY(0); } }
  /* Grab rail. A moulded plastic ridge, not a system grabber — and inert: the
     sheet doesn't drag, it's dismissed by tapping off it. */
  .sheet-rail {
    width: 46px; height: 4px; margin: 0 auto 12px; border-radius: 2px;
    background: linear-gradient(180deg, #b08c8c 0%, #8a6060 100%);
    box-shadow: inset 0 1px 0 rgba(255,240,238,0.45);
  }

  /* Bin, top right of the sheet — the whole of DELETE FRIEND reduced to an icon,
     with a confirmation behind it because it can't be undone. No plastic key
     around it: it's a bare brown vector on the sheet, so the only thing that
     reads as a control here is the green one. Same target trick as the pencil on
     a tape — 20px of visible glyph, 44px of hit area — and it expands into the
     sheet's own padding and over the title, which is text. The nearest real
     target is GO TO VOICE NOTES, more than 8px clear below: this one deletes for
     real, so that gap is not negotiable. */
  .sheet-bin-btn {
    position: absolute; top: 13px; right: 15px; z-index: 4;
    width: 20px; height: 20px; padding: 0; cursor: pointer;
    border: none; background: none;
    color: #6b4731;
    display: flex; align-items: center; justify-content: center;
    transition: color 0.1s, transform 0.07s;
  }
  .sheet-bin-btn::after { content: ''; position: absolute; inset: -12px; }
  .bin-glyph { width: 100%; height: 100%; display: block; }
  @media (hover: hover) { .sheet-bin-btn:hover { color: #8a3030; } }
  .sheet-bin-btn:active { color: #8a3030; transform: translateY(1px); }

  /* The sheet's keys, and the confirmation's, are labelled in the nav's voice —
     Figtree medium, 10% tracking, caps — not the Courier the recording flow uses. */
  .tape-sheet .stop-rec-btn,
  .confirm-card .stop-rec-btn,
  .confirm-card .modal-cancel {
    font-family: var(--ui-font);
    font-size: 11px; font-weight: 500;
    letter-spacing: 0.1em; text-transform: uppercase;
    text-indent: 0.1em;   /* soaks up the trailing letter-space so it sits centred */
  }

  /* ── SPINES (REC PICKER) ────────────────────────────────────
     A cassette seen edge-on, the way they look stacked on a shelf: the shell
     plastic as a frame with its label band running the length of it.
     This is the one silhouette the shared module doesn't draw — it renders the
     *face* of a tape at a fixed 1.604, and a spine is the same object turned
     ninety degrees, so no width would produce it. The shape is local; every
     colour, the label band and the face still come out of tapeParts(), so a
     spine is the same tape as the shelf's (see TapeSpine).
     46px tall, which clears the 44 minimum on its own, with 8px of shelf between
     each: picking the wrong one starts a take for the wrong person, and
     STOP-then-DELETE is a worse recovery than a gap. */
  .spine-shelf {
    display: flex; flex-direction: column; gap: 8px;
    max-height: 226px;                     /* ~4 spines, then it scrolls */
    overflow-y: auto; overscroll-behavior-y: contain; scrollbar-width: none;
    padding: 2px;                          /* room for the lift on hover */
  }
  .spine-shelf::-webkit-scrollbar { display: none; }
  .tape-spine {
    height: 46px; flex-shrink: 0; position: relative;
    display: flex; align-items: stretch;
    border: 2px solid #161616; border-radius: 5px;
    box-shadow:
      inset 0 2px 4px rgba(255,255,255,0.09),
      inset 0 -2px 4px rgba(0,0,0,0.5),
      0 4px 14px rgba(0,0,0,0.55);
  }
  /* The plastic is its own layer so a see-through shell can take the module's
     own `opacity` without dimming the label sitting on it. */
  .spine-plastic { position: absolute; inset: 0; border-radius: 3px; }
  .spine-sheen {
    position: absolute; inset: 0; border-radius: 3px; pointer-events: none;
    background: linear-gradient(118deg, rgba(255,255,255,0.34) 0%, rgba(255,255,255,0.1) 34%, rgba(255,255,255,0) 52%);
  }
  .spine-label {
    position: relative; z-index: 2;
    flex: 1; margin: 6px; padding: 3px 10px; border-radius: 3px;
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    overflow: hidden;
  }
  .spine-name { line-height: 1; white-space: nowrap; }
  .spine-sub  {
    font-family: 'Courier New', monospace; font-size: 7px;
    letter-spacing: 0.3px; white-space: nowrap; line-height: 1.2;
  }

  /* Delete confirmation — sits on top of the sheet, centred, because it's a
     question and not a place. */
  .confirm-card { text-align: center; }
  .confirm-copy {
    font-family: var(--ui-font); font-size: 12px; line-height: 1.5;
    color: #2a1010; letter-spacing: 0.3px; margin: 0 0 16px;
  }

  /* Share tray — machine buttons, deliberately NOT a native share sheet */
  .share-label { font-family: var(--ui-font); font-size: 9px; color: #7a5050; letter-spacing: 0.2px; margin-bottom: 6px; }
  .share-tray  { display: flex; gap: 6px; margin-bottom: 15px; }
  .share-btn   { height: 44px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; flex: 1; }
  .share-btn-icon { font-size: 13px; line-height: 1; }
  .share-btn-text { font-family: var(--ui-font); font-size: 8px; font-weight: bold; letter-spacing: 0.5px; line-height: 1; }

  /* Two of these are also the header of a friend's bottom sheet, which is the
     other place the app names a person over their tape. */
  .customiser-who     { text-align: center; margin: 0 0 13px; }
  .customiser-email   { font-family: var(--ui-font); font-size: 10px; color: #7a5050; letter-spacing: 0.2px; margin-top: 3px; word-break: break-all; }

  /* ══════════════════════════════════════════════════════════════
     MY TAPE — THE CUSTOMISER PAGE
     A page over the whole 402×874, not a card on the shelf: it's the
     onboarding's customise step brought into the app, because the tape has
     to be big enough to type on and to drag a sticker around. So the surface is
     the onboarding's paper rather than the machine's plastic, and its palette is
     scoped to the two paper surfaces — the machine's own pink is untouched, and
     nothing here leaks out onto a screen.

     Everything from `.tp-stage` down is the customiser *body*, and the
     onboarding renders the same body from the same component (`TapeCustomiser`)
     — so this is the one stylesheet for making a tape, wherever you're making
     it. What belongs to this page alone is the page itself: the surface, the
     heading and the bar.

     The height budget, against the 874: 18 top pad, ~39 of title, the tape's own
     340 × 212 on its stage, 45 of tabs, then 74 for the bar and 20 of bottom
     pad. That leaves the options 415px, and the swatches are grids stretched to
     the page's width rather than chips at a fixed size, so a tab spends 269–308
     of it and the rest pools above the bar. .tp-scroll is still there, for a
     longer sticker tray.
     ══════════════════════════════════════════════════════════════ */
  /* The paper palette, declared on both hosts of the customiser body: the six
     values have to resolve whether the `.tp-` classes are drawn on this page or
     inside `.ob-phone`. The onboarding's own `--ob-` names are aliases of these
     (see ONBOARDING), so a colour still changes in one place. */
  .tape-page, .ob-phone {
    --tp-paper: #e9e2d7; --tp-ink: #4a2a16; --tp-ink-soft: #7a5a42;
    --tp-rule: #cfc3b4; --tp-field: #f4efe6; --tp-btn: #3a2114;
  }

  .tape-page {
    position: absolute; inset: 0; z-index: 200;
    background: var(--tp-paper); color: var(--tp-ink);
    display: flex; flex-direction: column;
    padding: 18px 20px 20px;
    animation: tapePageIn 0.34s cubic-bezier(0.22,0.72,0.24,1) both;
  }
  /* It rises out of the foot of the device rather than fading in place: the page
     is a drawer coming up over the tapes screen, so it travels its own full
     height and doesn't cross-fade — a sheet you can see through on the way up
     reads as a dialog. */
  @keyframes tapePageIn { from { transform: translateY(100%); } to { transform: none; } }

  /* A flex column, so `.tp-options` can be handed the slack the tape and the bar
     don't use rather than every group stacking against the tabs. */
  .tp-scroll {
    flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    display: flex; flex-direction: column;
    /* the page's own side padding, given back so a scrollbar and the tabs'
       hit-area expanders aren't clipped against the scroller's edges */
    margin: 0 -20px; padding: 0 20px;
  }
  .tp-scroll::-webkit-scrollbar { width: 0; }

  /* The onboarding's display heading — Newsreader Medium tracked -5%, the mark's
     own spec, not the sentence-case modal title this page replaced. */
  .tp-title {
    font-family: var(--head-font); font-weight: 500; font-size: 31px;
    letter-spacing: -0.05em; line-height: 0.98; margin-bottom: 8px;
  }

  /* The way back out, beside the heading rather than down on the bar — the bar's
     one key stays the only thing a near-miss can reach at the foot. The row is
     pulled left by 11px so the arrow sits on the page's own margin while its
     44×44 hangs over it: `.tp-scroll` is a scroller, so a pseudo-element
     expander at its top edge would be clipped and the key carries the 44
     itself. */
  .tp-head { display: flex; align-items: center; gap: 4px; margin: -6px 0 0 -11px; }
  .tp-head .tp-title { margin-bottom: 0; }
  .tp-back {
    flex: 0 0 auto; width: 44px; height: 44px; padding: 0; border: none;
    background: transparent; color: var(--tp-ink); cursor: pointer;
    display: flex; align-items: center; justify-content: center;
  }
  .tp-back:active { opacity: 0.55; transform: translateX(-1px); }
  @media (hover: hover) { .tp-back:hover { opacity: 0.7; } }

  /* Leaving with a draft up asks first, on the page's own paper — the machine's
     plastic modal belongs to the screens behind this one. It's a child of
     `.tape-page`, so it stacks over it without a z-index of its own. */
  .tp-confirm {
    position: absolute; inset: 0; z-index: 1; display: flex;
    align-items: center; justify-content: center; padding: 20px;
    background: rgba(38,20,10,0.5);
    animation: tpConfirmIn 0.16s ease-out both;
  }
  @keyframes tpConfirmIn { from { opacity: 0; } to { opacity: 1; } }
  .tp-confirm-card {
    width: 100%; max-width: 296px; background: var(--tp-field);
    border: 1.5px solid var(--tp-rule); border-radius: 14px; padding: 20px 18px 18px;
    box-shadow: 0 18px 44px rgba(38,20,10,0.42);
  }
  .tp-confirm-title {
    font-family: var(--head-font); font-weight: 400; font-size: 21px; letter-spacing: 0;
    line-height: 1.15; color: var(--tp-ink); text-align: center; margin-bottom: 16px;
  }
  /* 10px apart, over the 8px minimum: one of these throws the draft away. */
  .tp-confirm-keys { display: flex; gap: 10px; }
  .tp-confirm-keys button {
    /* Both keys carry the border — the filled one's is transparent. `flex-basis:
       0` splits the row into equal *content* boxes, so a border on one of them
       and not the other makes it 3px the wider, and two keys the same shape
       either side of a 10px gap is the whole point of the pair. */
    flex: 1 1 0; min-width: 0; min-height: 46px; padding: 0 10px; border-radius: 23px;
    border: 1.5px solid transparent;
    cursor: pointer; font-family: var(--ui-font); font-size: 14px; font-weight: 600;
  }
  .tp-confirm-keys .leave { border-color: var(--tp-rule); background: transparent; color: var(--tp-ink-soft); }
  .tp-confirm-keys .stay  { background: var(--tp-btn); color: #f4ece0; }
  .tp-confirm-keys button:active { transform: translateY(1px); }
  @media (hover: hover) { .tp-confirm-keys button:hover { filter: brightness(1.06); } }

  /* The tape's stage. `touch-action: none` because a sticker is dragged on it and
     the page would otherwise take the gesture as a scroll. */
  .tp-stage    { display: flex; justify-content: center; margin: 4px 0 46px; }
  .tp-tapewrap { position: relative; width: 100%; max-width: 340px; touch-action: none; }

  /* The segmented control keeps the machine's plastic — it's the same control
     this customiser always had, and the tab you're on has to read as pressed
     rather than as a chip that happens to be filled in. */
  .tp-tabs { margin: 0 0 18px; }

  /* `1 0 auto`, never `1 1 auto`: it may take the slack, but it must not be
     shrunk below its own content inside a scroller — that centres the groups
     outside their box and lands them on the bar. */
  /* The options hang from the top, one block under the tab that named them, and
     the slack collects in a single place above the bar. `space-evenly` was
     spending it *between* the groups instead: DESIGN's SHELL and LABEL drifted
     ~130px apart with another void under the tabs, so two sets of ten and four
     tiles read as two unrelated screens rather than one page of parts. Hanging
     them also makes the three tabs agree — the first row lands at the same y on
     DESIGN, FONT and STICKERS, so switching tabs changes the options and not
     the layout. */
  .tp-options {
    flex: 1 0 auto; display: flex; flex-direction: column;
    justify-content: flex-start; gap: 20px;
  }

  .tp-group { margin-bottom: 0; }
  .tp-glabel {
    font-family: var(--ui-font); font-size: 10px; font-weight: 600;
    letter-spacing: 0.14em; text-transform: uppercase;
    color: var(--tp-ink-soft); margin-bottom: 9px;
  }

  /* Grids, not a wrapping row of chips: a fixed chip size leaves a ragged strip
     of paper down the right of the page, and the point of a swatch is to show
     the artwork as large as the column allows. Column counts are chosen so each
     set divides evenly — 10 shells over two rows of 5, 8 labels over four rows
     of 2, 6 faces over two rows of 3, 12 stickers over three rows of 4. */
  .tp-swatches { display: grid; gap: 10px; }
  /* 16px between the shell rows, not 10: the tiles are the cassette's own
     landscape ratio and so come out under 44 tall, and the expander that makes
     up the difference eats 3px off each side of the gap. 16 leaves 10px of dead
     paper between two rows of targets. */
  .tp-sw-shells { grid-template-columns: repeat(5, 1fr); row-gap: 16px; }
  .tp-sw-labels { grid-template-columns: repeat(2, 1fr); }
  .tp-sw-fonts  { grid-template-columns: repeat(2, 1fr); }

  /* `.tp-glabel` names a group where the tab alone can't: DESIGN's two grids
     (SHELL / LABEL) and the FONT tab's size slider, which isn't the faces the
     tab names. The font chips and STICKERS stay unlabelled with no tip — the
     copy said what the tab said. */

  /* Every chip on this page carries the 44 itself. The onboarding's are smaller
     — it's a sign-up flow on a phone-shaped page rather than the machine — and a
     grid with gaps in it has no room for an expander that would overlap its
     neighbours, so the minimum is a floor here, not a pseudo-element. */
  .tp-sw-shell, .tp-sw-label, .tp-chip {
    min-width: 44px; min-height: 44px; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    transition: transform 0.08s, box-shadow 0.1s, border-color 0.1s, background 0.1s;
  }
  .tp-sw-shell:active, .tp-sw-label:active, .tp-chip:active { transform: translateY(1px); }
  @media (hover: hover) {
    .tp-sw-shell:hover, .tp-sw-label:hover, .tp-chip:hover { filter: brightness(1.04); }
  }

  /* shell: a miniature of the tape, so you pick the plastic and not a word — and
     the TILE is the cassette's 1.604 now rather than artwork padded into a
     rounded square, so the swatch is the shape of the thing it selects. The
     vertical padding that used to make the row read as tiles is gone with it:
     the silhouette does that job on its own.
     `min-height: 0` releases the 44 floor set above — a 5-up column is 64px, so
     a 1.604 rectangle is 40 tall and the missing 4 come from an expander rather
     than off the artwork. */
  .tp-sw-shell {
    position: relative; min-height: 0; aspect-ratio: 1.604;
    padding: 0; border-radius: 11px; border: 2px solid transparent;
    background: rgba(74,42,22,0.035); line-height: 0;
  }
  /* Height-first, not width-first: the tile's height is derived from its width by
     the same ratio the svg carries, so letting the svg fill the height means the
     two can't disagree by a rounding error the way `width: 100%` did against a
     2px border. `size` on ShellSwatch is only the fallback if this stops
     matching. */
  .tp-sw-shell > svg { height: 100% !important; width: auto !important; }
  /* 40 + 3 + 3 = 46. Vertical only — a 5-up column is already 64 wide. */
  .tp-sw-shell::after { content: ''; position: absolute; inset: -3px 0; }
  .tp-sw-shell.on { border-color: var(--tp-ink); background: rgba(74,42,22,0.09); }

  /* label: the chip wears the label's own artwork, from LABEL_DESIGNS, at the
     label's own 3.42 (G.label is 896 × 262) — so a printed design is
     recognisable as the piece of paper it's about to become. It says nothing:
     the artwork is the preview and a word over it was a caption on one.
     Two up rather than four: a 3.42 rectangle one-per-row is 106px tall and
     eight of those don't fit the page, and at 176px wide the chip clears 44 on
     its own with no expander needed. */
  .tp-sw-label {
    width: auto; height: auto; aspect-ratio: 3.42; padding: 0;
    border-radius: 10px; border: 2px solid var(--tp-rule); overflow: hidden;
  }
  .tp-sw-label.on { border-color: var(--tp-ink); box-shadow: 0 0 0 2px rgba(74,42,22,0.18); }

  /* lettering: the face's own name set in the face itself, big enough to judge
     it by. `nowrap` because a word that breaks stops being a specimen. */
  .tp-chip {
    height: 96px; padding: 7px 12px; border-radius: 12px; white-space: nowrap;
    border: 1.5px solid var(--tp-rule); background: var(--tp-field); color: var(--tp-ink);
  }
  .tp-chip.on { background: var(--tp-ink); border-color: var(--tp-ink); color: #f4ece0; }

  /* The size slider — how large the name is written, above the faces that write
     it. A native range styled to the page's own materials: the track is a
     groove pressed into the paper, the thumb the machine's light plastic. The
     input itself is 44 tall, so the finger's target is the whole row and not
     the 26px knob riding in it. */
  .tp-size {
    -webkit-appearance: none; appearance: none;
    display: block; width: 100%; height: 44px; margin: 0; padding: 0;
    background: transparent; cursor: pointer;
  }
  .tp-size::-webkit-slider-runnable-track {
    height: 6px; border-radius: 3px;
    background: rgba(74,42,22,0.16);
    box-shadow: inset 0 1px 2px rgba(38,20,10,0.22), 0 1px 0 rgba(255,255,255,0.55);
  }
  /* (6 − 26) / 2 — WebKit seats the thumb against the track's top edge rather
     than centring it. `border-box`, so the border doesn't grow the knob and
     shift that arithmetic. */
  .tp-size::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none; box-sizing: border-box;
    width: 26px; height: 26px; margin-top: -10px; border-radius: 50%;
    background: linear-gradient(160deg, #f8f2e8 0%, #e4d8c6 100%);
    border: 1.5px solid var(--tp-rule);
    box-shadow: 0 2px 4px rgba(38,20,10,0.28), inset 0 1px 0 rgba(255,255,255,0.75);
  }
  .tp-size:active::-webkit-slider-thumb { background: linear-gradient(160deg, #efe4d3 0%, #dcceba 100%); }
  .tp-size::-moz-range-track {
    height: 6px; border-radius: 3px;
    background: rgba(74,42,22,0.16);
    box-shadow: inset 0 1px 2px rgba(38,20,10,0.22), 0 1px 0 rgba(255,255,255,0.55);
  }
  .tp-size::-moz-range-thumb {
    box-sizing: border-box; width: 26px; height: 26px; border-radius: 50%;
    background: linear-gradient(160deg, #f8f2e8 0%, #e4d8c6 100%);
    border: 1.5px solid var(--tp-rule);
    box-shadow: 0 2px 4px rgba(38,20,10,0.28), inset 0 1px 0 rgba(255,255,255,0.75);
  }
  .tp-size:active::-moz-range-thumb { background: linear-gradient(160deg, #efe4d3 0%, #dcceba 100%); }
  @media (hover: hover) {
    .tp-size:hover::-webkit-slider-thumb { filter: brightness(1.04); }
    .tp-size:hover::-moz-range-thumb { filter: brightness(1.04); }
  }

  /* sticker tray — four across, so twelve stickers are three even rows and each
     one is big enough to tell apart before you stick it on */
  .tp-tray { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
  .tp-tray button {
    position: relative; aspect-ratio: 1; min-height: 44px; cursor: pointer; padding: 9px;
    border: 1.5px solid var(--tp-rule); border-radius: 12px; background: var(--tp-field);
    display: flex; align-items: center; justify-content: center;
  }
  .tp-tray button:not(:disabled):active { transform: scale(0.94); }
  @media (hover: hover) { .tp-tray button:not(:disabled):hover { filter: brightness(1.04); } }
  .tp-tray img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }

  /* The locked bottom row. Dimming lives on the ART rather than the button —
     nested opacity multiplies, and greying the whole tile took the padlock down
     with it, which is the one thing on the tile that has to read. */
  .tp-tray button:disabled { cursor: default; background: rgba(74,42,22,0.05); border-color: rgba(74,42,22,0.13); }
  .tp-tray button:disabled img { filter: grayscale(1) brightness(1.08); opacity: 0.40; }
  .tp-lock { position: absolute; right: 5px; bottom: 4px; width: 11px; height: auto;
    color: var(--tp-ink-soft); opacity: 0.85; }

  /* One line under the tray, saying what the locked row is for. FONT and
     STICKERS carry no group label and no tip, and this isn't a tip — it's what
     four greyed-out tiles mean. Sentence case: it's set in the screen, not
     stamped on the machine. */
  .tp-tray-note {
    margin-top: 11px; font-family: var(--ui-font); font-size: 11px; line-height: 1.35;
    color: var(--tp-ink-soft); text-align: center;
  }

  /* The bar: one 52px key across the page, well over 44 and with nothing beside
     it to be hit by mistake. */
  .tp-bar { flex: 0 0 auto; display: flex; align-items: center; padding-top: 22px; }
  .tp-cta {
    flex: 1; height: 52px; border: none; border-radius: 26px; cursor: pointer;
    background: var(--tp-btn); color: #f4ece0;
    font-family: var(--ui-font); font-size: 15px; font-weight: 600; letter-spacing: 0.01em;
  }
  .tp-cta:active { transform: translateY(1px); }
  @media (hover: hover) { .tp-cta:hover { filter: brightness(1.12); } }

  /* ══════════════════════════════════════════════════════════════
     RADIO SCREEN
     ══════════════════════════════════════════════════════════════ */

  .on-air-banner {
    margin: 12px 16px 0;
    background: linear-gradient(160deg, #0e0808 0%, #180c0a 100%);
    border-radius: 8px; border: 2px solid #3a1a14;
    box-shadow: inset 0 0 20px rgba(200,60,30,0.15), 0 3px 10px rgba(0,0,0,0.5);
    padding: 12px 14px 14px; position: relative; overflow: hidden;
  }
  .on-air-banner::after {
    content: ''; position: absolute; inset: 0;
    background: repeating-linear-gradient(0deg, transparent 0px, transparent 3px, rgba(0,0,0,0.08) 3px, rgba(0,0,0,0.08) 4px);
    pointer-events: none;
  }
  .on-air-top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
  /* A lamp, and a lamp is dim or lit — never blinking. Dark amber while the show
     is only open, and it lights red with the label beside it once the tape rolls,
     the same two states the wood panel's REC lamp has. It moulds itself with an
     inset shadow when it's out, like those do; nothing on this machine flashes to
     tell you it's idle. */
  .on-air-dot { width: 9px; height: 9px; border-radius: 50%; background: #6f4a1c; box-shadow: inset 0 -1px 2px rgba(0,0,0,0.5); flex-shrink: 0; }
  .on-air-live { font-family: 'VT323', monospace; font-size: 18px; color: #ffbb44; letter-spacing: 2px; text-shadow: 0 0 6px rgba(255,180,60,0.4); }
  .on-air-dot.rec { background: #ff4444; box-shadow: 0 0 8px #ff4444; }
  .on-air-live.rec { color: #ff6b5e; text-shadow: 0 0 6px rgba(255,90,70,0.5); }
  .on-air-rec-clock { margin-left: auto; font-family: 'VT323', monospace; font-size: 18px; color: #f0c090; letter-spacing: 1px; text-shadow: 0 0 6px rgba(240,180,120,0.3); }
  /* The LCD's thirty bars, re-lit in this glass's amber */
  .on-air-wave { height: 36px; display: flex; align-items: center; justify-content: center; gap: 2px; margin-top: 10px; overflow: hidden; }
  .on-air-wave .wave-bar { background: linear-gradient(to top, #7a2c10, #ff9a50); box-shadow: 0 0 4px rgba(255,150,80,0.4); }
  .on-air-record-btn {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    margin-top: 12px; width: 100%;
    padding: 10px 16px;
    background: linear-gradient(160deg, #c84040 0%, #a02828 100%);
    border: 1.5px solid #6a1818; border-radius: 6px;
    /* 22px of RECORD YOUR ANSWER wraps at 2px of tracking in the 338 the banner
       leaves it, and a two-line key reads as a broken one. The half point back is
       invisible at this size; the second line wasn't. */
    font-family: var(--ui-font); font-size: 22px;
    color: #ffe8e8; letter-spacing: 1.5px;
    cursor: pointer;
    box-shadow: 0 4px 0 #5a0c0c, inset 0 1px 0 rgba(255,180,180,0.15);
    transition: transform 0.07s, box-shadow 0.07s;
  }
  @media (hover: hover) { .on-air-record-btn:hover { filter: brightness(1.1); } }
  .on-air-record-btn:active { transform: translateY(3px); box-shadow: 0 1px 0 #5a0c0c; }
  .on-air-date { font-family: 'VT323', monospace; font-size: 16px; color: #886050; letter-spacing: 1px; display: block; margin-bottom: 8px; }
  /* The mic refusing — same amber prompt the LCD uses, behind this glass instead */
  .on-air-mic-err { margin-top: 10px; font-family: 'VT323', monospace; letter-spacing: 1px; }
  .on-air-mic-err .err-main { font-size: 18px; color: #ffbb44; text-shadow: 0 0 6px rgba(255,180,60,0.4); }
  .on-air-mic-err .err-sub { font-size: 16px; color: #886050; margin-top: 2px; }

  /* Review — the take on the glass: a readout line, then keys. The keep / re-record
     pair is sized AND separated (44px each, 8px gap): re-record throws the take away. */
  .on-air-review { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
  .review-readout {
    display: flex; justify-content: space-between; align-items: baseline;
    font-family: 'VT323', monospace; font-size: 18px; color: #ffbb44;
    letter-spacing: 1px; text-shadow: 0 0 6px rgba(255,180,60,0.4);
  }
  .review-readout .review-clock { color: #f0c090; }
  /* `flex: 1` belongs to the pair, not the key — the same key stands alone under
     PLAY IT BACK and under the kept answer, where it stretches to full width on
     its own as a column child */
  .on-air-review-row { display: flex; gap: 8px; }
  .on-air-review-row .on-air-review-btn { flex: 1; }
  .on-air-review-btn {
    min-height: 44px;
    display: flex; align-items: center; justify-content: center; gap: 6px;
    background: linear-gradient(160deg, #2a1410 0%, #1c0e0b 100%);
    border: 1.5px solid #3a1a14; border-radius: 6px;
    font-family: var(--ui-font); font-size: 17px; font-weight: 700;
    color: #f0c090; letter-spacing: 1.5px;
    cursor: pointer;
    box-shadow: 0 3px 0 #120806, inset 0 1px 0 rgba(240,192,144,0.08);
    transition: transform 0.07s, box-shadow 0.07s;
  }
  .on-air-review-btn.keep {
    background: linear-gradient(160deg, #c84040 0%, #a02828 100%);
    border-color: #6a1818; color: #ffe8e8;
    box-shadow: 0 3px 0 #5a0c0c, inset 0 1px 0 rgba(255,180,180,0.15);
  }
  @media (hover: hover) { .on-air-review-btn:hover { filter: brightness(1.12); } }
  .on-air-review-btn:active { transform: translateY(2px); box-shadow: 0 1px 0 #120806; }
  .on-air-review-btn.keep:active { box-shadow: 0 1px 0 #5a0c0c; }

  /* Kept — the answer as a playable object rather than a word on a key. A VT323
     readout behind a dashed rule, dimmer than the review it came out of, with the
     RE-RECORD key stepped back to the review's dark plastic under it: the prompt
     stays the loudest thing in the banner and the answer is context, the same
     order an outgoing reply keeps on a friend's tape. 44px is stated on the row —
     the whole readout is the target, and the key below is destructive, so the
     block's gap is the 8px floor and no more. */
  .on-air-answer-block { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
  .on-air-answer {
    width: 100%; min-height: 44px;
    display: flex; align-items: center; gap: 8px;
    padding: 6px 10px; text-align: left;
    background: rgba(255,180,60,0.05);
    border: 1px dashed rgba(240,192,144,0.28); border-radius: 6px;
    font-family: 'VT323', monospace; font-size: 18px;
    color: #d8a878; letter-spacing: 1px; cursor: pointer;
    transition: background 0.12s, color 0.12s;
  }
  .on-air-answer .answer-mark { color: #ffbb44; flex-shrink: 0; }
  @media (hover: hover) { .on-air-answer:hover { background: rgba(255,180,60,0.1); color: #f0c090; } }
  .on-air-answer:active { background: rgba(255,180,60,0.14); color: #f0c090; }
  .on-air-answer.playing {
    background: rgba(255,180,60,0.1); color: #ffbb44;
    text-shadow: 0 0 6px rgba(255,180,60,0.4);
  }
  .on-air-divider { border: none; border-top: 1px solid #3a1a14; margin: 6px 0 9px; }
  .on-air-topic { font-family: 'VT323', monospace; font-size: 24px; color: #f0c090; letter-spacing: 1px; text-shadow: 0 0 10px rgba(240,180,120,0.25); line-height: 1.2; }

  /* Radio session list */
  .radio-sessions {
    flex: 1; min-height: 0;
    margin: 10px 16px 10px;
    border: 2px solid #b89090; border-radius: 6px;
    overflow: hidden;
    background: linear-gradient(160deg, #eddcd8 0%, #e0c8c4 100%);
    display: flex; flex-direction: column;
  }
  .radio-sessions-scroll {
    flex: 1; min-height: 0;
    overflow-y: auto; scrollbar-width: none;
    overscroll-behavior-y: contain;
  }
  .radio-sessions-scroll::-webkit-scrollbar { display: none; }
  .radio-sessions-header { background: linear-gradient(180deg, rgba(255,236,214,0.06) 0%, rgba(255,236,214,0.01) 45%, rgba(0,0,0,0.14) 100%), var(--walnut); padding: 6px 12px; font-family: var(--ui-font); font-size: 18px; font-weight: 700; color: #ffd9b0; letter-spacing: 2px; display: flex; justify-content: space-between; align-items: center; box-shadow: inset 0 1px 0 rgba(255,220,190,0.12), inset 0 -3px 6px rgba(0,0,0,0.4); text-shadow: 0 1px 2px rgba(0,0,0,0.55); }

  .session-row { border-bottom: 1px solid rgba(160,80,80,0.12); cursor: pointer; transition: background 0.12s; }
  .session-row:last-child { border-bottom: none; }
  .session-row-header { display: flex; align-items: center; gap: 10px; padding: 9px 12px; }
  .session-row:active .session-row-header { background: rgba(200,120,100,0.16); }
  @media (hover: hover) {
    .session-row:hover .session-row-header { background: rgba(200,120,100,0.08); }
  }
  /* An opened show fills with the chassis's own dusty rose — the deep end of the
     pink the machine is moulded from, not a third material. Walnut was tried here
     and it put a second wood band directly under the PREVIOUS SHOWS bar, which
     read as far heavier than a selected row should. The rose is dark enough to
     carry the cream copy and quiet enough to leave the bar above it the only wood
     on the screen. The red edge stays as the marker it already was, deepened so
     it still separates against pink. */
  .session-row.expanded .session-row-header {
    background: linear-gradient(180deg, #b8807f 0%, #a56b6b 100%);
    border-left: 3px solid #8a4040; padding-left: 9px;
    box-shadow: inset 0 1px 0 rgba(255,235,225,0.22), inset 0 -3px 6px rgba(110,55,55,0.25);
  }
  .session-row.expanded .session-topic { color: #fff2e4; text-shadow: 0 1px 2px rgba(90,40,40,0.45); }
  .session-row.expanded .session-week { color: #f0d6ca; }
  .session-row.expanded .session-count { color: #ffe4d2; }
  .session-row.expanded .session-chevron { color: #ffe4d2; }
  /* The rose covers the tint both the hover and the press used, and this row is
     still the key that closes the show — so the opened state restates them as a
     lift rather than losing its response to a thumb. */
  @media (hover: hover) {
    .session-row.expanded:hover .session-row-header { filter: brightness(1.1); }
  }
  .session-row.expanded:active .session-row-header { filter: brightness(1.18); }

  /* No lamp on a previous show. The list is finished shows by definition, so a
     dot on one had nothing to report — the pulsing one on the newest row read as
     a broadcast that was still running. The row's own date says when it went out. */
  .session-info { flex: 1; min-width: 0; }
  /* Topics are sentences, not names — two lines before the ellipsis, so a long
     prompt doesn't cut off mid-thought. The row sits in a scroller, so the
     extra line costs nothing but scroll. */
  .session-topic { font-family: var(--ui-font); font-size: 15px; font-weight: bold; color: #2a1010; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
  .session-week  { font-family: var(--ui-font); font-size: 12px; color: #7a5050; margin-top: 2px; }
  .session-count { font-family: var(--ui-font); font-size: 18px; color: #8a5050; flex-shrink: 0; }
  .session-chevron { font-size: 12px; color: #8a6060; transition: transform 0.2s; flex-shrink: 0; }
  .session-row.expanded .session-chevron { transform: rotate(90deg); }

  .session-expanded-body {
    animation: slideDown 0.2s ease-out;
    overflow: hidden;
    background: rgba(0,0,0,0.04);
    border-top: 1px solid rgba(160,80,80,0.12);
  }
  /* Each of these plays that person's clip, so the row is the target and it was
     30px. Adjacent rows touch, so an expander would overlap the neighbour and
     make a tap between two rows ambiguous about whose voice you get — the row
     grows for real instead. It sits in a scroller, so the extra height costs
     nothing but scroll. */
  .responder-row {
    display: flex; align-items: center; gap: 10px; min-height: 44px;
    padding: 7px 16px; border-bottom: 1px solid rgba(160,80,80,0.08);
    transition: background 0.15s;
  }
  .responder-row:last-child { border-bottom: none; }
  .responder-row.active {
    background: rgba(57,204,20,0.1);
    animation: responderGlow 0.8s ease-in-out infinite;
  }
  .responder-name { font-family: var(--ui-font); font-size: 15px; font-weight: bold; color: #2a1010; flex: 1; }
  .responder-row.active .responder-name { color: #1a6a08; text-shadow: 0 0 6px rgba(57,204,20,0.3); }
  .responder-dots { flex: 1; border-bottom: 1px dotted rgba(160,80,80,0.3); margin: 0 6px; align-self: flex-end; margin-bottom: 4px; }
  /* A running time is a readout, not copy — VT323, two points over .msg-duration
     now the rest of this screen has gone up by the same two */
  .responder-dur { font-family: 'VT323', monospace; font-size: 19px; color: #8a5050; }

  /* A button shrink-wraps its copy, so this key is only as wide as what's on it
     — which is why it carries `min-height` rather than deriving 44 from padding:
     at 16px the box comes out a pixel short on its own. */
  .radio-play-btn {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    margin: 10px 14px 12px;
    padding: 10px; min-height: 44px;
    background: linear-gradient(160deg, #2a1a18 0%, #1a0e0c 100%);
    border: 1.5px solid #6a3830; border-radius: 6px;
    font-family: var(--ui-font); font-size: 16px;
    color: #ffbb88; letter-spacing: 2px; cursor: pointer;
    box-shadow: 0 3px 0 #0a0404, inset 0 1px 0 rgba(255,180,140,0.1);
    transition: transform 0.07s, box-shadow 0.07s;
  }
  @media (hover: hover) { .radio-play-btn:hover { filter: brightness(1.15); } }
  .radio-play-btn:active { transform: translateY(2px); box-shadow: 0 1px 0 #0a0404; }
  .radio-play-btn.playing { color: #ff8888; border-color: #8a3020; }

  /* ── ONBOARDING ──────────────────────────────────────────────
     The sign-up flow, lifted out of prototype-onboarding.html. It isn't a
     screen *on* the machine — it's what you see before you have one — so it
     replaces `.device` in the frame rather than rendering inside it, at the
     same 402×874 and on the same `--fit` scale, so switching between the two
     doesn't move or resize anything. Every class here is `ob-` prefixed: the
     flow's own names (`.lcd`, `.screen`, `.bar`, `.chip`) collide with the
     machine's, and the two stylesheets share one document.

     It's reached by tapping the wordmark. That's a demo shortcut, not part of
     the product's flow — this is a prototype we walk through many times a day
     and nobody wants to sign up again each hard refresh. */
  .ob-phone {
    /* aliases of the paper palette declared with `.tape-page` — the flow and the
       customiser page are the same sheet of paper, and the customiser's body is
       drawn in here too */
    --ob-paper:var(--tp-paper); --ob-ink:var(--tp-ink); --ob-ink-soft:var(--tp-ink-soft);
    --ob-rule:var(--tp-rule); --ob-btn:var(--tp-btn); --ob-field:var(--tp-field);
    --ob-lcd-bg:#0a1a0a; --ob-lcd-frame:#2a1a18; --ob-lcd-on:#39ff14; --ob-lcd-dim:#2f8f26;
    --ob-pad:20px;

    /* the same three lines as `.device`, off the same `--surround`, so the
       paper card and the machine are the same size on any window */
    --fit-h: calc((100svh - var(--surround) * 2
                   - env(safe-area-inset-top) - env(safe-area-inset-bottom)) / 874px);
    --fit-w: calc((100vw
                   - env(safe-area-inset-left) - env(safe-area-inset-right)) / 402px);
    --fit: min(1, var(--fit-h), var(--fit-w));
    transform: scale(var(--fit));
    transform-origin: top center;
    margin-bottom: calc(874px * (var(--fit) - 1));

    width: 402px; height: 874px;
    display: flex; flex-direction: column; overflow: hidden;
    padding: 18px var(--ob-pad) 20px;
    background: var(--ob-paper); color: var(--ob-ink);
    font-family: var(--ui-font);
    /* sits on the dark mat exactly as the machine does — same radius, same
       stacked shadow, so it reads as the same object arriving */
    border-radius: 14px 14px 18px 18px;
    box-shadow:
      0 0 0 1px #9a7070, 4px 4px 0 #9a7070, 8px 8px 0 #845858,
      12px 12px 0 #6a4040, 0 24px 60px rgba(0,0,0,0.7);
  }

  /* ── stepper: also the navigation, so there is only one of them ──
     Absent on the intro: there's no progress to report before you've started,
     and the machine picture wants the full height of the frame. */
  .ob-stepper { display: flex; gap: 6px; flex: 0 0 auto; margin-bottom: 24px; }
  .ob-stepper button {
    position: relative; flex: 1; height: 4px; padding: 0; border: none;
    border-radius: 2px; cursor: pointer; background: var(--ob-rule);
    transition: background 0.2s;
  }
  /* A 4px bar is not a target — open it out to the full 44 without moving the
     artwork. Asymmetric on purpose: `.ob-phone` clips its overflow, and the
     stepper sits 18px below the frame's top edge, so anything past -18px is
     thrown away against it. 18 up + 4 + 22 down is exactly 44 and every pixel
     of it lands. */
  .ob-stepper button::after { content:''; position: absolute; inset: -18px 0 -24px; }
  .ob-stepper button.done { background: var(--ob-ink-soft); }
  .ob-stepper button.on   { background: var(--ob-ink); }

  /* ── screen ── */
  .ob-screen { flex: 1; display: flex; flex-direction: column; min-height: 0;
    animation: obRiseIn 0.32s cubic-bezier(0.22,0.7,0.3,1) both; }
  @keyframes obRiseIn { from { opacity:0; transform:translateY(9px); } to { opacity:1; transform:none; } }
  /* A flex column, so a step can hand its slack to the cassette (`.ob-stage.grow`)
     or push its copy down to the bar instead of stacking everything against the
     top of an 874px frame. */
  .ob-scroll { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    display: flex; flex-direction: column;
    margin: 0 calc(var(--ob-pad) * -1); padding: 0 var(--ob-pad); }
  .ob-scroll::-webkit-scrollbar { width: 0; }
  .ob-centred { flex: 1; display: flex; flex-direction: column; justify-content: center;
    min-height: 0; gap: 32px; }

  /* ── type ──
     Newsreader Medium at -5% for display, the wordmark's own spec; Figtree for
     everything set as UI. Same split as the machine. */
  .ob-display { font-family: var(--head-font);
    font-weight: 500; letter-spacing: -0.05em; line-height: 0.98; color: var(--ob-ink); margin: 0; }
  .ob-h-intro { font-size: 46px; }
  .ob-h-step  { font-size: 31px; margin-bottom: 10px; }
  /* No standfirst line under the intro's mark any more — the display heading
     hands straight to the body copy, which is the only thing it needed to say. */
  .ob-hint { font-size: 14px; line-height: 1.55; color: var(--ob-ink-soft); margin: 0 0 24px; }

  /* ── bottom bar: round back at left, CTA at right ── */
  .ob-bar { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; padding-top: 22px; }
  .ob-back {
    width: 52px; height: 52px; flex: 0 0 auto; border-radius: 50%; cursor: pointer;
    border: 1.5px solid var(--ob-rule); background: transparent; color: var(--ob-ink);
    display: flex; align-items: center; justify-content: center;
  }
  .ob-back:active { transform: translateY(1px); }
  .ob-back svg { width: 20px; height: 20px; }
  .ob-cta {
    flex: 1; height: 52px; border: none; border-radius: 26px; cursor: pointer;
    background: var(--ob-btn); color: #f4ece0;
    font-family: var(--ui-font); font-size: 15px; font-weight: 600; letter-spacing: 0.01em;
  }
  .ob-cta:active:not(:disabled) { transform: translateY(1px); }
  .ob-cta:disabled { opacity: 0.3; cursor: not-allowed; }

  /* ── tape stage ──
     The name step's own; the customise step is on the customiser's `.tp-stage`.
     No `.grow` modifier any more — the two steps that had one now spread their
     slack across every block instead of handing all of it to the cassette. */
  .ob-stage { display: flex; justify-content: center; }
  .ob-tapewrap { position: relative; width: 100%; max-width: 340px; touch-action: none; }
  /* The name step's field IS the label, and the label band is about 35px tall —
     so the input carries the missing 9px itself. An `<input>` can't hold a
     pseudo-element expander, and `.tape-title` centres its child, so the extra
     height opens evenly above and below and the lettering doesn't move. Both
     wraps: an editable cassette is drawn here and on the customiser's stage. */
  .ob-tapewrap .tape-input, .tp-tapewrap .tape-input { min-height: 44px; }

  /* The customiser's own chips, grids and tray aren't here — the step renders
     `TapeCustomiser`, so it wears the `.tp-` classes in the block above and the
     two pages can't drift apart. */

  /* ── share ──
     Three blocks under the copy — the tape, the link, the tray — spread through
     whatever height is left, the same way the customiser spreads its options.
     The stage used to take all of it: that centred the cassette in one 290px
     void and left another above the bar, with the copy stranded at the top. The
     gap is the floor, `space-evenly` spends the rest. */
  .ob-share-body {
    flex: 1 0 auto; display: flex; flex-direction: column;
    justify-content: space-evenly; gap: 20px;
  }

  /* the machine's own LCD, so the link reads as machine output */
  .ob-lcd {
    background: var(--ob-lcd-bg); border: 3px solid var(--ob-lcd-frame); border-radius: 8px;
    box-shadow: inset 0 0 18px rgba(0,120,0,0.28), 0 2px 8px rgba(0,0,0,0.3),
                0 0 0 1px rgba(180,80,60,0.15);
    padding: 10px 12px; display: flex; align-items: center; gap: 10px; overflow: hidden;
    position: relative;
  }
  .ob-lcd::after { content:''; position: absolute; inset: 0; pointer-events: none;
    background: repeating-linear-gradient(0deg, rgba(0,0,0,0.22) 0px, rgba(0,0,0,0.22) 1px,
                                                transparent 1px, transparent 3px); }
  .ob-lcd-body { flex: 1; min-width: 0; position: relative; z-index: 1; }
  .ob-lcd-cap { font-family: 'VT323', monospace; font-size: 13px; letter-spacing: 0.22em;
    color: var(--ob-lcd-dim); line-height: 1; margin-bottom: 3px; }
  .ob-lcd-url { font-family: 'VT323', monospace; font-size: 21px; line-height: 1.05;
    color: var(--ob-lcd-on); text-shadow: 0 0 7px rgba(57,255,20,0.55);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* `.ob-lcd` clips its overflow for the scanlines, so COPY carries its own 44
     rather than reaching for it with a pseudo-element */
  .ob-lcd-copy {
    position: relative; z-index: 1; flex: 0 0 auto; cursor: pointer;
    border: 1px solid rgba(57,255,20,0.5); border-radius: 6px;
    background: rgba(57,255,20,0.10); color: var(--ob-lcd-on);
    font-family: 'VT323', monospace; font-size: 15px; letter-spacing: 0.12em;
    padding: 7px 10px; min-height: 44px; line-height: 1;
    display: flex; align-items: center; justify-content: center;
  }
  .ob-lcd-copy:active { background: rgba(57,255,20,0.22); }

  /* share targets — one row */
  .ob-targets { display: flex; gap: 8px; }
  .ob-targets button {
    flex: 1; min-width: 0; cursor: pointer; border: 1.5px solid var(--ob-rule);
    border-radius: 12px; background: var(--ob-field); padding: 10px 4px;
    display: flex; flex-direction: column; align-items: center; gap: 5px;
    font-family: var(--ui-font); font-size: 10.5px; font-weight: 500; color: var(--ob-ink);
  }
  .ob-targets button b { font-size: 18px; line-height: 1; font-weight: 400; }
  .ob-targets button.sent { border-color: #4a7a4a; background: #eef4ea; color: #3a6a3a; }
  .ob-targets button:active { transform: scale(0.97); }

  /* ── intro ──
     The machine bleeds to the top and both edges of the frame, so it escapes
     `.ob-phone`'s padding on three sides. The copy then *centres* in what's
     left rather than pinning to either end — pinned to the CTA it leaves a
     285px hole under the machine, pinned to the machine it's a top-heavy
     column. Splitting the slack is the only version that reads deliberate. */
  .ob-machine { display: block; height: auto; flex: 0 0 auto;
    width: calc(100% + var(--ob-pad) * 2);
    margin: -18px calc(var(--ob-pad) * -1) 0; }
  .ob-intro-copy { flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column; justify-content: center;
    padding: 28px 0; }

  /* The wordmark is the way in, so it presses like a key. It stays exactly as
     drawn — a hidden control is still a control, and this one's discoverability
     is deliberate (see the ONBOARDING block above). The 44px target is a
     pseudo-element, so the type doesn't move: the mark is ~27px tall inside a
     58px panel and there's room either side of it. */
  .wordmark { position: relative; cursor: pointer; -webkit-user-select: none; user-select: none; }
  .wordmark::after { content:''; position: absolute; inset: -10px; }
  .wordmark:active { transform: translateY(1px); opacity: 0.85; }
