:root {
    --bg-0: #07070a;
    --bg-1: #101014;
    --bg-2: #1a1a20;
    --metal: #2a2a32;
    --metal-hi: #3a3a44;
    --metal-lo: #16161c;
    --text: #e8e6df;
    --text-dim: #8a8a92;
    --accent: #ffb347;
    --accent-hot: #ff5b3a;
    --lcd-bg: #0c1a0c;
    --lcd: #7dff7d;
    --lcd-glow: rgba(125,255,125,0.45);
  }

  * { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; }
  body {
    font-family: 'Bricolage Grotesque', system-ui, sans-serif;
    background:
      radial-gradient(ellipse at 50% 30%, #18181e 0%, var(--bg-0) 55%, #040405 100%);
    color: var(--text);
    /* Body is now a normal-flow page (slim header → dj-stage hero →
       about → nav grid → comments → footer), not a viewport-locked
       flex-centered single-screen app. The decks get their fullscreen
       feel from .dj-stage below; everything else flows naturally. */
    min-height: 100vh;
    overflow-x: hidden;
    -webkit-tap-highlight-color: transparent;
  }

  /* Hero stage that wraps the decks. Recreates the
     "vertically-centered fullscreen app" feel that the body used to
     have, but contained so additional sections can flow normally
     below. min-height carves out (almost) a viewport, the inner flex
     stack centers the H1/intro/decks vertically. */
  .dj-stage {
    min-height: calc(100vh - 56px);   /* viewport minus the slim site header */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 24px;
    position: relative;
    width: 100%;
  }
  .dj-stage h1 {
    font-family: 'Bricolage Grotesque', sans-serif;
    font-weight: 800;
    color: var(--text);
    text-align: center;
    margin: 0 0 6px;
  }
  .dj-intro {
    color: #aaa;
    font-family: sans-serif;
    max-width: 700px;
    margin: 0 auto 18px;
    font-size: 0.95rem;
    line-height: 1.5;
    text-align: center;
  }

  /* Subtle grain overlay for depth */
  body::before {
    content: '';
    position: fixed; inset: 0;
    pointer-events: none;
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.18 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    opacity: 0.4;
    mix-blend-mode: overlay;
    z-index: 100;
  }

  /* Note: the old `header { ... }`, `header h1`, and `header .sub` rules
     were removed when the bare <header><h1>Dual Deck</h1></header> was
     replaced by the site-wide slim #bb-header bar plus a hero <h1>
     inside .dj-stage. The bare-element selector was leaking onto the
     new <header id="bb-header"> too, fighting whatever nav-styles.css
     defines. Page-title styling now lives on .dj-stage h1. */

  /* Fullscreen toggle button — small icon-only chip in the bottom-right
     corner of the viewport. This is the universal placement for
     fullscreen toggles (YouTube, Netflix, video players); puts the
     control where users instinctively look for it without competing
     with the header title or the deck console.

     Lives at body level (NOT inside header or any decks/crate
     structure) so it stays accessible from any app state. The two
     child SVGs (icon-fs-enter, icon-fs-exit) swap visibility based on
     `body.is-fullscreen`, which DJ.js toggles on fullscreenchange.

     z-index: 999 sits below the crate-overlay (1000) so the crate
     covers it cleanly when open. */
  .fullscreen-btn {
    position: fixed;
    bottom: 14px;
    right: 14px;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background:
      linear-gradient(180deg, rgba(255,255,255,0.04) 0%, rgba(0,0,0,0) 50%),
      linear-gradient(180deg, #2c2c34 0%, #1a1a20 50%, #14141a 100%);
    border: 1px solid #0a0a0e;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.14),
      inset 0 -1px 0 rgba(0,0,0,0.6),
      0 2px 6px rgba(0,0,0,0.55);
    color: var(--text-dim);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    transition: color 0.15s ease, box-shadow 0.15s ease, transform 0.08s ease;
    z-index: 999;
  }
  .fullscreen-btn:hover {
    color: var(--accent);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.18),
      inset 0 -1px 0 rgba(0,0,0,0.6),
      0 0 12px rgba(255,150,50,0.4),
      0 2px 6px rgba(0,0,0,0.55);
  }
  .fullscreen-btn:active {
    transform: translateY(1px);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.08),
      inset 0 -1px 0 rgba(0,0,0,0.4),
      0 1px 2px rgba(0,0,0,0.5);
  }
  .fullscreen-btn svg {
    width: 16px;
    height: 16px;
    display: block;
    filter: drop-shadow(0 1px 0 rgba(0,0,0,0.5));
  }
  /* Icon swap based on body.is-fullscreen state. */
  .fullscreen-btn .icon-fs-exit { display: none; }
  body.is-fullscreen .fullscreen-btn .icon-fs-enter { display: none; }
  body.is-fullscreen .fullscreen-btn .icon-fs-exit  { display: block; }

  /* Fullscreen layout adjustments.
     -------------------------------------------------------------------------
     In fullscreen we drop body padding so the deck console can use the
     full viewport width, but we KEEP the header and hint visible —
     they're useful chrome that the user wants to see. */
  body.is-fullscreen {
    padding: 0;
  }

  /* Horizontal clipping fix.
     -------------------------------------------------------------------------
     The deck-backdrop and deck-mount are 80vmin tall (capped at 780px) and
     160vmin wide (capped at 1700px), tilted with rotateX(55deg) and a
     1600px perspective. After the projection, the bottom edge of the
     trapezoid renders ~1.25× WIDER than its CSS width because the bottom
     is closer to the viewer. On a 1920×1080 fullscreen, that means a
     1700px box projects to a 2125px-wide bottom edge — 100px overflow
     on each side. The bottom-LEFT and bottom-RIGHT corners (and the
     play buttons mounted to them) get clipped by the viewport.

     Fix: cap the un-tilted width at 76vw so the projected bottom edge
     (76 × 1.25 = 95vw) leaves ~2.5vw margin on each side. The 1700px
     cap stays for very large screens where 76vw would still exceed it.
     We don't touch the height — vertical fit was OK once the chrome
     was hidden, and shrinking height too would change the box aspect
     ratio (less dramatic perspective).

     CRITICAL: the .deck (record) width must shrink by the same factor,
     otherwise the pitch faders mounted on the deck-mount's outer edges
     move inward (since the deck-mount is now narrower) while the
     records stay the same size — and the records' bottom edges (which
     after their own perspective tilt project ~1.34× wider) collide
     with the pitch faders. The deck:backdrop ratio in the original
     CSS is 58vmin:160vmin = 0.3625; we preserve that with 27.5vw:76vw
     = 0.362 here. */
  body.is-fullscreen .deck-backdrop,
  body.is-fullscreen .deck-mount {
    width: min(76vw, 1700px);
    /* Height also shrinks proportionally. Without this, the un-capped CSS
       width:height ratio drifts from the original 2:1 (160vmin:80vmin) to
       something taller, leaving too much empty steel below the records.
       The 38vw figure preserves 76:38 = 2:1, matching the original. */
    height: min(38vw, 780px);
  }
  body.is-fullscreen .deck {
    width: min(27.5vw, 624px);
  }
  /* The .crossfader is its own absolute-positioned perspective overlay (at
     top: -20, height: 80vmin, same rotateX as the backdrop). The
     .crossfader-panel sits at flex-end inside it, so the panel's rendered
     vertical position is determined by the crossfader container's bottom
     edge after the tilt. If we only shrink the backdrop but not the
     crossfader, the panel stays at its original height's bottom — which
     now sits BELOW the (newly shorter) backdrop's bottom edge. Match the
     height here so the panel lands on the steel, not under it. */
  body.is-fullscreen .crossfader {
    height: min(38vw, 780px);
  }

  /* Wrapper for both deck units — row on desktop, stacks on narrow screens. */
  .decks {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    gap: 24px 40px;
    width: 100%;
    max-width: 1360px;
    margin: 0 auto;
  }

  /* Ensure deck-units paint above the shared backdrop. */
  .deck-unit { position: relative; z-index: 1; }

  /* A single deck + its controls, laid out vertically. */
  .deck-unit {
    display: flex;
    flex-direction: column;
    align-items: center;
    flex: 0 0 auto;
    padding: 8px;
  }
  /* Note: .focused class is still applied to the active deck by JS (needed for
     keyboard routing), but we don't style it — the hint text below explains
     that keys affect the last-clicked deck. */

  /* On narrow screens, let each deck use more of the viewport. */
  @media (max-width: 720px) {
    .decks { gap: 16px; }
    .deck { width: min(78vmin, 440px); }
    .controls { width: min(78vmin, 460px); }
    /* Hide the shared backdrop when decks stack vertically — it only makes
       sense as a single continuous surface behind the side-by-side row. */
    .deck-backdrop { display: none; }
  }

  /* ===== Crossfader — mounted on the brushed-steel backdrop =====
     POSITIONING MODEL
     The crossfader shares the backdrop's EXACT coordinate system: same
     position, left, top, width, height, transform-origin, and transform.
     This means percentages inside the crossfader's box map 1:1 to the
     backdrop's surface — so placing the fader "near the bottom edge" is
     just aligning a child within its own box. Future elements can use the
     same pattern: duplicate the wrapper, align content where you want it.

     The crossfader ITSELF is a full-sized invisible overlay. A small child
     .crossfader-panel holds the visible controls and is aligned to the
     bottom center using flex. pointer-events: none on the overlay means it
     doesn't block clicks on the decks; the panel re-enables pointer events
     only where the controls are. */
  .crossfader {
    position: absolute;
    left: 50%;
    top: -20px;
    width: min(138vmin, 1480px);
    height: min(80vmin, 780px);
    transform: translateX(-50%) perspective(1600px) rotateX(55deg);
    transform-origin: center;
    z-index: 3;
    display: flex;
    justify-content: center;
    /* Align child panel to the near (bottom) edge of the backdrop. */
    align-items: flex-end;
    /* Small gap from the very bottom so the panel has some breathing room. */
    padding-bottom: 1%;
    box-sizing: border-box;
    pointer-events: none;
  }
  /* The actual visible control faceplate — positioned as a flex child
     of the full-surface overlay above. */
  .crossfader-panel {
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 8px 22px 8px;
    border-radius: 8px;
    background: linear-gradient(180deg, #2a2a32 0%, #1a1a22 100%);
    border: 1px solid #0a0a0e;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.08),
      inset 0 -1px 0 rgba(0,0,0,0.6);
    width: min(22vmin, 260px);
    pointer-events: auto;
  }
  .crossfader-title {
    font-family: 'JetBrains Mono', monospace;
    font-size: 9px;
    letter-spacing: 0.25em;
    color: var(--text-dim);
  }
  .crossfader-row {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-width: 0;
  }
  .crossfader-row .end-label {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    font-weight: 700;
    color: var(--text-dim);
    width: 10px;
    text-align: center;
    flex-shrink: 0;
  }
  .crossfader-slider {
    flex: 1 1 0;
    min-width: 0;
    width: 100%;
    margin: 0;
    padding: 0;
    -webkit-appearance: none;
    appearance: none;
    background: transparent;
    height: 22px;
    cursor: ew-resize;
  }
  .crossfader-slider::-webkit-slider-runnable-track {
    height: 6px;
    background: linear-gradient(90deg, #0a0a0e, #181820);
    border-radius: 3px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.8) inset;
  }
  .crossfader-slider::-moz-range-track {
    height: 6px;
    background: linear-gradient(90deg, #0a0a0e, #181820);
    border-radius: 3px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.8) inset;
  }
  .crossfader-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 18px; height: 28px;
    border-radius: 3px;
    margin-top: -11px;
    background: linear-gradient(180deg, #e8e6df 0%, #9c9a90 45%, #5a584e 100%);
    box-shadow:
      0 1px 0 rgba(255,255,255,0.6) inset,
      0 -1px 0 rgba(0,0,0,0.4) inset,
      0 2px 4px rgba(0,0,0,0.7);
    cursor: ew-resize;
  }
  .crossfader-slider::-moz-range-thumb {
    width: 18px; height: 28px;
    border: none;
    border-radius: 3px;
    background: linear-gradient(180deg, #e8e6df 0%, #9c9a90 45%, #5a584e 100%);
    box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset, 0 2px 4px rgba(0,0,0,0.7);
    cursor: ew-resize;
  }

  /* ===== AUTO MIX button =====
     Pill-shaped toggle below the crossfader. Off-state reads as a dark
     pill matching the crossfader-panel background; on-state lights up the
     LED dot in brand orange and rims the button with the same glow so the
     crossfader area subtly draws the eye while auto-mix is running.
     The button itself is the click target — both .automix-led and
     .automix-label are pointer-events:none so the click registers on the
     button no matter where in the pill the user taps.
     IMPORTANT: white-space: nowrap + small padding + small font keep the
     button on ONE line. Without nowrap, the panel's narrow width
     (`min(22vmin, 260px)`) on smaller screens forces "AUTO MIX" to wrap
     to two lines, which makes the panel ~22px taller — enough to push it
     up into the records. */
  .automix-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 3px 10px;
    background: linear-gradient(180deg, #1f1f28 0%, #131319 100%);
    border: 1px solid #0a0a10;
    border-radius: 999px;
    cursor: pointer;
    color: var(--text-dim);
    font-family: 'JetBrains Mono', monospace;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    white-space: nowrap;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.06),
      inset 0 -1px 0 rgba(0,0,0,0.6);
    transition: color 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease;
  }
  .automix-btn:hover {
    color: #fff;
    border-color: #2a2a34;
  }
  .automix-led {
    display: inline-block;
    width: 6px; height: 6px;
    border-radius: 50%;
    background: #2a0608;
    box-shadow: inset 0 0 2px rgba(0,0,0,0.7), 0 0 0 1px rgba(0,0,0,0.5);
    pointer-events: none;
    flex-shrink: 0;
  }
  .automix-label { pointer-events: none; }
  .automix-btn.on {
    color: #fff;
    border-color: var(--accent);
    box-shadow:
      0 0 10px rgba(255, 179, 71, 0.45),
      inset 0 1px 0 rgba(255,255,255,0.08),
      inset 0 -1px 0 rgba(0,0,0,0.6);
  }
  .automix-btn.on .automix-led {
    background: var(--accent);
    box-shadow: 0 0 6px var(--accent), 0 0 0 1px #000, inset 0 0 1px #fff8e0;
    animation: automix-led-pulse 1.4s ease-in-out infinite;
  }
  @keyframes automix-led-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
  }
  @media (prefers-reduced-motion: reduce) {
    .automix-btn.on .automix-led { animation: none; }
  }
  /* On narrow screens the decks stack vertically and the backdrop is hidden.
     Drop the perspective and place the fader below, as a normal flow element. */
  @media (max-width: 720px) {
    .crossfader {
      position: static;
      transform: none;
      width: auto;
      height: auto;
      padding: 0;
      margin: 8px auto 20px;
      display: block;
    }
    .crossfader-panel {
      width: min(86vmin, 440px);
      margin: 0 auto;
    }
    .crossfader-cluster {
      flex-direction: column;
      align-items: center;
    }
  }

  /* Crossfader cluster wraps the fader panel and flanking track counters
     in a horizontal row so they share the perspective-tilted bottom-center
     area of the deck surface. On stacked mobile, the media query above
     switches this to a column. */
  .crossfader-cluster {
    display: flex;
    align-items: flex-end;
    gap: 12px;
    pointer-events: auto;
  }

  /* Meter stack — vertical column wrapping a per-deck pair of LCD counters
     (BPM on top, TRACK below). Sits at either end of the crossfader cluster
     and shares its bottom-aligned baseline so the crossfader and the lower
     LCD line up. align-items: stretch so both LCDs render at the same width
     (set by the wider one — typically BPM with three digits). */
  /* Meter-stack — small per-deck cluster of LCD readouts (BPM + TRACK)
     mounted at the bottom-outer corner of each deck-mount, beside the
     play-wrap. ROW direction so the two LCDs sit at the same vertical
     level instead of stacking — a column was tall enough (~110px) to
     overlap the bottom of the vinyl record. align-items: stretch keeps
     both LCDs the same height even if their content differs slightly. */
  .meter-stack {
    display: flex;
    flex-direction: row;
    align-items: stretch;
    gap: 6px;
    pointer-events: auto;
  }

  /* ===== Track counter — small LCD-style readout. Two per deck (BPM
     and TRACK), placed in the .meter-stack beside the play-wrap.
     Sized to match the transport buttons (48px tall) AND kept compact
     horizontally (60px min) so that the four LCDs flanking the
     crossfader (TRACK A, BPM A, TRACK B, BPM B) don't crowd or visually
     overlap with the central crossfader panel. */
  .track-counter {
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Vertical centering inside the fixed-height chassis — content is
       smaller than 48px so we let it sit in the middle rather than top
       padding it. */
    justify-content: center;
    gap: 2px;
    padding: 0 6px;
    height: 48px;
    min-width: 60px;
    border-radius: 6px;
    background: linear-gradient(180deg, #2a2a32 0%, #1a1a22 100%);
    border: 1px solid #0a0a0e;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.08),
      inset 0 -1px 0 rgba(0,0,0,0.6);
  }
  .track-counter-label {
    font-family: 'JetBrains Mono', monospace;
    font-size: 8px;
    letter-spacing: 0.2em;
    color: var(--text-dim);
  }
  .track-counter-value {
    /* Small inset LCD — green-glow aesthetic matching the track-counter family.
       Tight padding (1px 6px) so the LCD bezel hugs the text without forcing
       the outer chassis any wider than ~60px. */
    background: linear-gradient(180deg, #081308, var(--lcd-bg));
    border: 1px solid #000;
    border-radius: 4px;
    padding: 1px 6px;
    min-width: 36px;
    text-align: center;
    box-shadow:
      0 1px 0 rgba(255,255,255,0.04) inset,
      0 -1px 3px rgba(0,0,0,0.7) inset;
    font-family: 'JetBrains Mono', monospace;
    /* 16px keeps the LCD readable while letting "128" (integer BPM) fit
       comfortably in the 60px-wide chassis. */
    font-size: 16px;
    font-weight: 700;
    letter-spacing: 0.05em;
    line-height: 1;
    color: var(--lcd);
    text-shadow: 0 0 6px var(--lcd-glow);
  }

  /* ===== Pitch fader — per-deck horizontal slider for ±16% tempo adjustment.
     Sits at the bottom of each .meter-stack (below BPM and TRACK LCDs) and
     wears the same brushed-steel chassis as the LCDs above so the column
     reads as a unified instrument panel. The slider's chrome thumb mirrors
     the crossfader's thumb (different size, same finish) for visual family.
     Track has a faint center-line marker at the 0% (no pitch shift) position
     because pitch faders are bidirectional and DJs need to see "home" at a
     glance — even with the slider centered, the horizontal eye-line breaks
     the slider track in two so the visual middle is unambiguous. */
  .pitch-fader {
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 6px 10px 8px;
    border-radius: 6px;
    background: linear-gradient(180deg, #2a2a32 0%, #1a1a22 100%);
    border: 1px solid #0a0a0e;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.08),
      inset 0 -1px 0 rgba(0,0,0,0.6);
    min-width: 96px;
  }
  .pitch-fader-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
  }
  .pitch-fader-label {
    font-family: 'JetBrains Mono', monospace;
    font-size: 8px;
    letter-spacing: 0.2em;
    color: var(--text-dim);
  }
  /* Live pitch readout — small green-glow text inline with the label rather
     than a separate LCD card, to keep the meter-stack compact. tabular-nums
     prevents the digits from jittering horizontally as values cycle. */
  .pitch-fader-value {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.05em;
    color: var(--lcd);
    text-shadow: 0 0 4px var(--lcd-glow);
    font-variant-numeric: tabular-nums;
  }
  .pitch-fader-slider {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    margin: 0;
    padding: 0;
    background: transparent;
    height: 22px;
    cursor: ew-resize;
  }
  /* Slider track — same dark inset trough as the crossfader, plus a 2px
     centerline marker at 50% so the home position is visually unambiguous
     when the thumb is parked elsewhere. Two separate gradient layers
     (background-image stack) — the linear "stripe" sits over the trough. */
  .pitch-fader-slider::-webkit-slider-runnable-track {
    height: 4px;
    background:
      linear-gradient(90deg, transparent calc(50% - 1px), rgba(255,255,255,0.35) calc(50% - 1px), rgba(255,255,255,0.35) calc(50% + 1px), transparent calc(50% + 1px)),
      linear-gradient(180deg, #0a0a0e, #181820);
    border-radius: 2px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.8) inset;
  }
  .pitch-fader-slider::-moz-range-track {
    height: 4px;
    background:
      linear-gradient(90deg, transparent calc(50% - 1px), rgba(255,255,255,0.35) calc(50% - 1px), rgba(255,255,255,0.35) calc(50% + 1px), transparent calc(50% + 1px)),
      linear-gradient(180deg, #0a0a0e, #181820);
    border-radius: 2px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.8) inset;
  }
  /* Chrome thumb — matches the crossfader's metallic-silver gradient but
     scaled down (smaller deck control, less visual weight than the master
     mix fader). margin-top centers the thumb on the 4px track. */
  .pitch-fader-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 20px;
    border-radius: 3px;
    margin-top: -8px;
    background: linear-gradient(180deg, #e8e6df 0%, #9c9a90 45%, #5a584e 100%);
    box-shadow:
      0 1px 0 rgba(255,255,255,0.6) inset,
      0 -1px 0 rgba(0,0,0,0.4) inset,
      0 2px 4px rgba(0,0,0,0.7);
    cursor: ew-resize;
  }
  .pitch-fader-slider::-moz-range-thumb {
    width: 14px;
    height: 20px;
    border: none;
    border-radius: 3px;
    background: linear-gradient(180deg, #e8e6df 0%, #9c9a90 45%, #5a584e 100%);
    box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset, 0 2px 4px rgba(0,0,0,0.7);
    cursor: ew-resize;
  }

  /* ===== Vertical pitch fader — Technics/CDJ-style ============================
     One per deck, mounted to the OUTER edge of each deck-mount overlay
     (left for deck A, right for deck B). Tall vertical slider with the
     +16% end at the TOP and -16% at the bottom — matches the convention
     of "up = faster" found on every modern CDJ.
     
     Positioning model: absolute within the perspective-tilted .deck-mount,
     so the fader inherits the deck's rotateX(55deg) tilt and looks like a
     real control mounted on the deck plinth's surface. It sits ABOVE the
     play-wrap row (which holds play/prev/next + the BPM/TRACK meter-stack),
     hence the bottom: 200px clearance.
     
     Vertical slider technique: render the input as a HORIZONTAL slider,
     then rotate -90deg via CSS transform. This is more reliable than
     writing-mode: vertical-lr — which had cross-axis-positioning quirks
     in Chrome/Safari that left the entire slider offset within the
     chassis instead of centered. With rotation, the browser uses its
     well-tested horizontal-slider layout for thumb-on-track centering.
     See the .pitch-fader-slider-vert rules for the full mechanic. */
  .pitch-fader-vert {
    position: absolute;
    top: 80px;
    bottom: 200px;
    /* Narrowed from 80px — the chassis only needs to wrap the 22px slider
       thumb plus the "+16.0%" readout text (~40px at 11px monospace).
       52px gives both elements a comfortable fit without empty padding
       eating the visual weight of the fader. */
    width: 52px;
    pointer-events: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 8px 4px 10px;
    border-radius: 6px;
    background: linear-gradient(180deg, #2a2a32 0%, #1a1a22 100%);
    border: 1px solid #0a0a0e;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.08),
      inset 0 -1px 0 rgba(0,0,0,0.6);
    box-sizing: border-box;
  }
  /* Outer-edge alignment per deck. left/right at 1.5% sits the chassis
     just inside the deck-mount's edge padding (1%) plus a hair of inset. */
  .deck-mount-a .pitch-fader-vert { left: 1.5%; }
  .deck-mount-b .pitch-fader-vert { right: 1.5%; }
  .pitch-fader-vert-label {
    font-family: 'JetBrains Mono', monospace;
    font-size: 9px;
    letter-spacing: 0.2em;
    color: var(--text-dim);
    flex: 0 0 auto;
  }
  /* Live pitch readout — small green-glow text below the fader. Same
     LCD-glow color as the BPM/TRACK readouts so the per-deck panel
     reads as one coherent instrument cluster. */
  .pitch-fader-vert-value {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.05em;
    color: var(--lcd);
    text-shadow: 0 0 4px var(--lcd-glow);
    font-variant-numeric: tabular-nums;
    flex: 0 0 auto;
  }
  /* Slider area — flex container that owns the rotated slider's positioning
     coordinate system. position: relative makes the absolutely-positioned
     input inside resolve its top/left against this box, which is sized to
     fill the remaining vertical space between the label and the value. */
  .pitch-fader-vert-area {
    flex: 1 1 auto;
    width: 100%;
    position: relative;
    min-height: 0;
  }

  .pitch-fader-slider-vert {
    /* ROTATION-BASED VERTICAL SLIDER.
       Strategy: render a normal HORIZONTAL slider, then rotate -90deg into
       vertical orientation. This sidesteps writing-mode: vertical-lr's
       cross-axis-positioning quirks where Chrome/Safari left the entire
       slider (track + thumb) anchored to one edge of the chassis instead
       of centered. With rotation, the browser uses its well-tested
       HORIZONTAL slider layout — thumb perfectly centered on track via
       the same margin-top: -X technique used everywhere else in the
       codebase. The visual rotation is just a CSS transform; pointer
       events on transformed elements are handled correctly by every
       modern browser, so dragging up/down still updates the value. */
    -webkit-appearance: none;
    appearance: none;

    /* PRE-rotation dimensions (i.e., as the browser sees it):
         width  → POST-rotation HEIGHT (the visible vertical length of the fader)
         height → POST-rotation WIDTH  (the thickness of the fader strip)
       220px sized to fit comfortably inside the chassis. On a typical 1366
       laptop (vmin=768), chassis is ~334px CSS tall and the slider area is
       ~280px after label+value+gaps; 220 leaves ~30px of breathing room at
       each end. The original 320px overflowed visibly on this viewport. */
    width: 100px;
    height: 22px;

    /* Center via translate, then rotate. translate(-50%, -50%) shifts the
       element so its center sits at top:50%, left:50% of the parent —
       this is the standard "center an absolute element" idiom. */
    position: absolute;
    top: 50%;
    left: 50%;
    /* rotate(-90deg) is counterclockwise (think: a "→" arrow becomes "↑").
       The right end of a horizontal slider (max value) ends up at the TOP
       after the rotation. So +16% lands at the top, -16% at the bottom. */
    transform: translate(-50%, -50%) rotate(-90deg);

    margin: 0;
    padding: 0;
    background: transparent;
    cursor: ns-resize;
  }

  /* Track and thumb styled as a HORIZONTAL slider — the rotation handles
     the visual orientation. This way Chrome/Safari's well-tested horizontal
     slider rendering is what produces the thumb-on-track centering, which
     means we can re-use the same margin-top: -9px trick the crossfader
     thumb uses. */
  .pitch-fader-slider-vert::-webkit-slider-runnable-track {
    width: 100%;
    height: 4px;
    /* Centerline marker at horizontal-50% of the pre-rotation slider —
       after -90deg rotation it shows up as a HORIZONTAL stripe across
       the visible vertical fader at its midpoint. The pre-rotation
       gradient is `90deg` (a vertical stripe in the horizontal slider). */
    background:
      linear-gradient(90deg, transparent calc(50% - 1px), rgba(255,255,255,0.35) calc(50% - 1px), rgba(255,255,255,0.35) calc(50% + 1px), transparent calc(50% + 1px)),
      linear-gradient(180deg, #0a0a0e, #181820);
    border-radius: 2px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.8) inset;
  }
  .pitch-fader-slider-vert::-moz-range-track {
    width: 100%;
    height: 4px;
    background:
      linear-gradient(90deg, transparent calc(50% - 1px), rgba(255,255,255,0.35) calc(50% - 1px), rgba(255,255,255,0.35) calc(50% + 1px), transparent calc(50% + 1px)),
      linear-gradient(180deg, #0a0a0e, #181820);
    border-radius: 2px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.8) inset;
  }
  /* Thumb — tall-and-narrow PRE-rotation, so AFTER -90deg rotation it
     becomes a wider-than-tall horizontal fader knob (the canonical
     Technics-style cap shape). margin-top: -9px centers the thumb on the
     4px track: (thumb-height 22 - track-height 4) / 2 = 9px. Identical
     pattern to the .crossfader-slider thumb. */
  .pitch-fader-slider-vert::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 22px;
    margin-top: -9px;
    border-radius: 3px;
    background: linear-gradient(180deg, #e8e6df 0%, #9c9a90 45%, #5a584e 100%);
    box-shadow:
      0 1px 0 rgba(255,255,255,0.6) inset,
      0 -1px 0 rgba(0,0,0,0.4) inset,
      0 2px 4px rgba(0,0,0,0.7);
    cursor: ns-resize;
    border: none;
  }
  .pitch-fader-slider-vert::-moz-range-thumb {
    width: 14px;
    height: 22px;
    border: none;
    border-radius: 3px;
    background: linear-gradient(180deg, #e8e6df 0%, #9c9a90 45%, #5a584e 100%);
    box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset, 0 2px 4px rgba(0,0,0,0.7);
    cursor: ns-resize;
  }
  /* Hide the vertical fader on stacked-mobile layouts — the deck-mount
     overlay is hidden there, so this would orphan otherwise. The
     horizontal pitch-fader styles above are still defined but no
     longer reachable — kept in CSS as legacy in case the design ever
     reverts to a cluster-based layout. */
  @media (max-width: 720px) {
    .pitch-fader-vert { display: none; }
  }

  /* ===== Deck-mounted play buttons — same overlay pattern as .crossfader.
     One full-surface invisible overlay per deck, matching the backdrop's
     coordinate system (same size, position, and perspective transform).
     A child .play-wrap inside each overlay is flex-aligned to the
     bottom-outer corner of its turntable:
       - .deck-mount-a → bottom-LEFT  corner (left turntable's outer edge)
       - .deck-mount-b → bottom-RIGHT corner (right turntable's outer edge)
     pointer-events: none on the overlays lets clicks pass through to the
     decks; the play button itself re-enables pointer events.
     The .meter-stack inside each deck-mount is also a flex sibling of
     .play-wrap; HTML order determines whether it sits to the inner or
     outer side of the play buttons. */
  .deck-mount {
    position: absolute;
    top: -20px;
    left: 50%;
    width: min(160vmin, 1700px);
    height: min(80vmin, 780px);
    transform: translateX(-50%) perspective(1600px) rotateX(55deg);
    transform-origin: center;
    z-index: 4;
    display: flex;
    align-items: flex-end;
    /* Gap between bottom-aligned flex children (.play-wrap and the
       inner-side .meter-stack). 14px puts the BPM/TRACK LCDs visually
       adjacent to the play buttons without crowding them. */
    gap: 14px;
    padding: 0 1% 1%;
    box-sizing: border-box;
    pointer-events: none;
  }
  .deck-mount-a { justify-content: flex-start; }
  .deck-mount-b { justify-content: flex-end;   }
  .deck-mount .play-wrap { pointer-events: auto; }

  /* On narrow (stacked) screens the overlays don't make sense — hide them
     and let the original in-controls play buttons show. */
  @media (max-width: 720px) {
    .deck-mount { display: none; }
  }

  /* Platter */
  .deck {
    position: relative;
    width: min(58vmin, 624px);
    aspect-ratio: 1.8 / 1;
    display: grid;
    place-items: center;
    margin: 8px 0 28px;
  }

  /* Ground shadow removed per user request. */

  /* Brushed-steel trapezoid backdrop spanning BOTH turntables.
     Lives as a child of .decks (the flex row wrapper) and is absolutely
     positioned behind the turntable row. Uses the same perspective + rotateX
     as .vinyl, so its square footprint projects as a trapezoid matching the
     records' tilt — top edge receding, bottom edge closer.

     Key sizing note: after rotateX(55deg), rendered vertical extent ≈ 57% of
     the CSS height. So to cover a ~380px tall turntable area PLUS extend
     ~100px below, we need CSS height of roughly (480/0.57) ≈ 840px. */
  .deck-backdrop {
    position: absolute;
    left: 50%;
    /* top controls where the rendered top edge lands. Negative value pulls
       it up so it sits behind the top of the records. */
    top: -20px;
    /* Wider — records + tonearms + outer-edge vertical pitch faders need
       clearance on both sides. Bumped from 138vmin/1480px to make room
       for the per-deck pitch fader chassis (~80px wide each) plus a bit
       of inset margin between the fader and the turntable. */
    width: min(160vmin, 1700px);
    /* Shorter — after rotateX(55deg), rendered vertical extent ≈ 57% of this.
       Sized so it reaches from behind the top of the records to just past
       their bottom, without invading the controls. */
    height: min(80vmin, 780px);
    transform: translateX(-50%) perspective(1600px) rotateX(55deg);
    transform-origin: center;
    /* Base brushed-steel color — horizontal gradient gives left/right
       dimensional falloff. Lighter tones so it reads as polished steel. */
    background: linear-gradient(to right,
      #4e4e56 0%,
      #7e7e88 20%,
      #9a9aa4 50%,
      #7e7e88 80%,
      #4e4e56 100%);
    border-radius: 12px;
    border: 1px solid rgba(255,255,255,0.18);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.3),
      inset 0 -1px 0 rgba(0,0,0,0.6);
    pointer-events: none;
    overflow: hidden;
    z-index: 0;
  }
  /* Vertical lighting pass FIRST (lower z) — subtle so it doesn't wash out
     the brush texture painted on top. */
  .deck-backdrop::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(to bottom,
      rgba(0,0,0,0.25) 0%,
      rgba(0,0,0,0.00) 30%,
      rgba(255,255,255,0.06) 55%,
      rgba(0,0,0,0.00) 80%,
      rgba(0,0,0,0.20) 100%);
    border-radius: inherit;
    pointer-events: none;
    z-index: 1;
  }
  /* Fine vertical brush lines painted ON TOP of the lighting so they actually
     read as metal texture rather than getting washed into the gradient. */
  .deck-backdrop::after {
    content: '';
    position: absolute;
    inset: 0;
    background: repeating-linear-gradient(to right,
      rgba(255,255,255,0.18) 0px,
      rgba(255,255,255,0.18) 1px,
      rgba(0,0,0,0.22) 1px,
      rgba(0,0,0,0.22) 2px,
      rgba(255,255,255,0.08) 2px,
      rgba(255,255,255,0.08) 3px,
      rgba(0,0,0,0.12) 3px,
      rgba(0,0,0,0.12) 4px);
    border-radius: inherit;
    pointer-events: none;
    z-index: 2;
  }

  .vinyl {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    border-radius: 50%;
    user-select: none;
    touch-action: none;
    cursor: grab;
    /* True 3D perspective — tilts the top back, widens the bottom. */
    transform: perspective(1000px) rotateX(55deg);
    transform-origin: center;
    z-index: 1;
  }
  .vinyl.scratching { cursor: grabbing; }

  /* Rotor — spins the record image + label. */
  .rotor {
    position: absolute;
    inset: 0;
    will-change: transform;
    transform-origin: center;
  }
  /* Direct-child selector — targets only the record image, not the platter img
     which lives nested inside .platter. */
  .rotor > img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
    pointer-events: none;
    -webkit-user-drag: none;
    user-select: none;
  }

  /* Platter — PNG image of the metal disc. Square container (107% of vinyl),
     so the rotation of a circle always looks like a circle — no wobble.
     Size + scaleY + translate are tuned together: 107% gives enough rim to
     show the full strobe band, scaleY(0.97) squishes the top+bottom equally
     so the top aligns with the record's top edge, and translate(0, 11px)
     pushes the result down enough to hide any remaining top overhang. */
  .platter {
    position: absolute;
    width: 107%;
    height: 107%;
    left: -3.5%;
    top: -3.5%;
    will-change: transform;
    transform-origin: center;
  }
  .platter img {
    width: 100%;
    height: 100%;
    display: block;
    pointer-events: none;
    -webkit-user-drag: none;
    user-select: none;
  }

  /* Label text — printed on the white center of the record.
     Sized to fit the label circle (~28% of record diameter). */
  .label-text {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 30%;
    height: 30%;
    transform: translate(-50%, -50%);
    pointer-events: none;
    overflow: visible;
  }

  /* Tonearm — sits inside .vinyl so it inherits the 3D perspective tilt
     (appears to lie flat on the record plane), but NOT inside .rotor, so it
     stays stationary while the record spins beneath it.
     The PNG has the pivot at ~50% X / ~55% Y and stylus at ~4% X / ~60% Y.
     Positioning puts the pivot at the back-right of the deck (upper-right in
     the tilted view).
     The four numbers below form a tuned geometric system — see below. */
  .tonearm {
    position: absolute;
    /* width controls arm length relative to record diameter.
       130% → a prominently sized arm, proportional to a real DJ deck where
       the tonearm reaches most of the way across. */
    width: 130%;
    /* left + top place the PNG so its pivot ends up at vinyl (100%, 25%) —
       the back-right corner of the deck (upper-right in the tilted view).
       Formula: left = 100 - (width/2), top = 25 - (width × origin_y).
       With width=130%, origin_y=0.50 → top = 25 - 65 = -40%. */
    left: 35%;
    top: -40%;
    /* Wrapper itself passes pointer events through — only the child img
       receives them (see .tonearm img below). Without this, the wrapper's
       full 130%-wide bounding box would eat clicks over most of the vinyl,
       blocking scratching. */
    pointer-events: none;
    touch-action: none;
    z-index: 5;
    /* transform-origin matches the PNG's internal pivot location, so rotation
       swings the arm around its hinge — pivot stays put, stylus swings.
       Measured empirically from the PNG: the pivot hub is centered at
       ~(49.55%, 50.00%) of the 2000×2000 image. 50% 50% is close enough
       that the hub stays visually pinned during rotation. */
    transform-origin: 50% 50%;
    /* Rotation is exposed as a CSS custom property so JS can animate the arm
       (prev/next nudges) without disturbing the translate part of the stack.
       -68deg = rest position at the outer groove. More negative = stylus
       swings counter-clockwise toward the label (further into the track).
       Transform stack (right-to-left application):
       1. rotate(var(--arm-angle)) — the animated swing.
       2. translate(-2%, -8%) — shifts the whole arm slightly up-and-left
          relative to the vinyl. Both pivot AND stylus move together in
          this step. */
    --arm-angle: -78deg;
    transform: translate(-2%, -8%) rotate(var(--arm-angle));
    /* Smooth swing on angle change — slightly slow so the arm visibly
       glides rather than snapping, like a weighted tonearm on real
       hardware. Only transition the transform; nothing else. */
    transition: transform 0.4s cubic-bezier(0.4, 0.0, 0.2, 1);
  }
  /* While the user is actively dragging the arm, kill the transition so
     rotation tracks the cursor in real time instead of lagging 0.4s. */
  .tonearm.dragging {
    transition: none;
  }
  .tonearm.dragging img {
    cursor: grabbing;
  }
  /* Global grab cursor during tonearm drag. The tonearm wrapper is
     pointer-events: none, and the arm swings through a wide angular range
     so the cursor ends up over lots of different elements during the drag
     (vinyl, label-click, empty space). Scoping the cursor to html via a
     class lets us enforce "grabbing" everywhere for the duration of the
     drag without touching individual element rules. */
  html.tonearm-dragging,
  html.tonearm-dragging * {
    cursor: grabbing !important;
  }
  .tonearm img {
    display: block;
    width: 100%;
    height: auto;
    -webkit-user-drag: none;
    user-select: none;
    /* Only the image receives clicks, so the arm's image rect is the hit
       target rather than the full wrapper box. Still a rectangle because
       browsers don't do alpha-based hit-testing, but much smaller than
       the 130%-wide wrapper. */
    pointer-events: auto;
    cursor: grab;
    /* Cast a soft shadow onto the record below. drop-shadow (unlike
       box-shadow) traces the PNG's actual alpha silhouette — so the thin
       arm tube, the cartridge head, and the round pivot base each cast
       their own correctly-shaped shadow, not a rectangle.
       Offset is in the arm's LOCAL coordinate space (because the parent
       .tonearm has a rotate(-68deg) transform). A down-right offset here
       translates, after rotation, to a shadow falling mostly to the lower
       side of the arm in screen space — reading as "arm floats above
       record, light from upper-left casts shadow down-right onto vinyl".
       Two stacked drop-shadows give a more natural penumbra:
         - tight tiny shadow (arm just above record — sharper close shadow)
         - wider diffuse shadow (light scattering as distance grows) */
    filter:
      drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.55))
      drop-shadow(4px 5px 4px rgba(0, 0, 0, 0.35));
  }
  .label-text .label-top {
    font-family: 'Bricolage Grotesque', system-ui, sans-serif;
    font-weight: 800;
    font-size: 10px;
    letter-spacing: 1.4px;
    fill: #14141a;
  }
  .label-text .label-bottom {
    font-family: 'JetBrains Mono', monospace;
    font-weight: 600;
    font-size: 5.5px;
    letter-spacing: 1px;
    fill: #3a3a44;
  }

  /* Spindle — vertical metal post, stands up from the record hole.
     Lives outside .vinyl so it's not affected by the 3D tilt — it's a post
     in the scene, not a feature of the record. */
  .spindle {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 6px;
    height: 14px;
    /* Anchor the BASE of the post at the spindle hole (deck center) and
       let it extend upward. */
    transform: translate(-50%, -100%);
    /* Brushed-metal gradient — dark edges, bright middle, reads as cylindrical. */
    background: linear-gradient(
      90deg,
      #1e1c18 0%,
      #4a4640 12%,
      #b8b3a4 32%,
      #f5f1e3 50%,
      #b8b3a4 68%,
      #4a4640 88%,
      #1e1c18 100%
    );
    border-radius: 3px 3px 1px 1px;
    /* Tight 1px outline only — no blurry drop-shadow, so it reads as IN the
       record rather than floating above it. Cast shadow lives on the
       collar (::after) so it's anchored to the pin's base, not offset
       away from it. */
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.45);
    z-index: 10;
    pointer-events: none;
  }
  /* Rounded chrome cap on top — convex highlight */
  .spindle::before {
    content: '';
    position: absolute;
    top: -1.5px;
    left: 50%;
    transform: translateX(-50%);
    width: 6px;
    height: 3px;
    background: radial-gradient(ellipse at 50% 70%, #fdfaec 0%, #c4bfae 55%, #6a6658 100%);
    border-radius: 50% 50% 40% 40%;
    box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.7) inset;
  }
  /* Convex metal collar at the base — shows the cylinder's cross-section
     emerging from the record hole. Mirrors the top cap, but horizontal.
     Crucially: this is ON the pin's base, not a shadow below it. */
  .spindle::after {
    content: '';
    position: absolute;
    bottom: -1px;
    left: 50%;
    transform: translateX(-50%);
    width: 7px;
    height: 2.5px;
    background:
      /* Subtle top highlight on the collar */
      linear-gradient(180deg, rgba(255, 255, 255, 0.3) 0%, transparent 55%),
      /* Same dark→bright→dark metal gradient as the shaft, so it reads as
         the rounded bottom of the cylinder */
      linear-gradient(
        90deg,
        #1e1c18 0%,
        #5a564a 20%,
        #d9d4c6 50%,
        #5a564a 80%,
        #1e1c18 100%
      );
    border-radius: 50%;
    /* Multi-layer box-shadow = collar outline PLUS the pin's cast shadow
       on the label. Because the collar is anchored AT the pin's base, the
       shadow starts exactly where pin meets label (no floating gap) and
       extends downward (matching the deck's 55° tilt toward the viewer).
       Read bottom-to-top (furthest from the collar → closest):
         - wide diffuse shadow (soft penumbra fanning out)
         - mid tight shadow (core shadow right at base)
         - hairline outline on the collar itself */
    box-shadow:
      0 0 0 0.5px rgba(0, 0, 0, 0.35),
      0 2px 2px rgba(0, 0, 0, 0.55),
      0 5px 6px rgba(0, 0, 0, 0.3);
  }

  /* Specular highlight — sits just above the record in 3D so it renders on
     the record's surface, not the platter's. Stays fixed (doesn't rotate). */
  .specular {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    pointer-events: none;
    /* translateZ just above the record (record is at z=12, this at 12.1) */
    transform: translateZ(12.1px);
    background:
      radial-gradient(ellipse 55% 45% at 30% 20%, rgba(255,255,255,0.12), rgba(255,255,255,0) 60%),
      radial-gradient(ellipse 40% 35% at 75% 82%, rgba(255,255,255,0.05), rgba(255,255,255,0) 60%);
    mix-blend-mode: screen;
    z-index: 3;
  }

  /* Control strip — on desktop the play buttons live on the deck backdrop
     (.deck-mount), so .controls has nothing to hold and is hidden. On
     narrow/stacked screens the deck-mount is hidden (see media query below)
     and .controls becomes a simple centered bar holding the fallback play
     buttons. */
  .controls { display: none; }

  /* Center: play button */
  .play-wrap { display: flex; justify-content: center; }
  .play-btn {
    position: relative;
    width: 72px; height: 72px;
    border-radius: 14px;
    border: none;
    cursor: pointer;
    /* Brushed-steel face: vertical brightness gradient (top-lit) with a
       very faint horizontal grain overlay for brushed texture. */
    background:
      repeating-linear-gradient(90deg,
        rgba(255,255,255,0.04) 0 1px,
        rgba(0,0,0,0.04) 1px 2px),
      linear-gradient(180deg, #d8dade 0%, #a8abb2 45%, #6e7278 100%);
    /* FLUSH REST STATE — the button sits level with the deck surface,
       like a key inlaid into the chassis. The effect is subtle by design:
         - a crisp 1px dark ring (the gap/seam where the button meets the
           deck cutout)
         - a soft 1px white top-inset (ambient light catching the very
           top edge of the seam)
         - a whisper-soft bottom inset (the button face's own convex curve
           falling into very slight shadow at the lower edge) */
    box-shadow:
      0 0 0 1px #0a0a0e,
      0 1px 0 rgba(255,255,255,0.4) inset,
      0 -1px 2px rgba(0,0,0,0.15) inset;
    display: grid;
    place-items: center;
    /* Release transition — snaps back quickly but with a hint of ease so
       it doesn't feel digital. Press-in uses 0s (see :active) for instant
       feedback the moment the click registers. */
    transition: box-shadow 0.08s ease-out, background 0.08s ease-out;
    /* touch-action: manipulation disables the ~50-100ms browser tap-delay
       on mobile (double-tap-zoom detection), so :active fires the instant
       the finger contacts the button. */
    touch-action: manipulation;
    color: #000;
  }
  .play-btn:active {
    /* PRESSED = button sinks INTO the deck. No translate — the illusion
       comes entirely from inner shadows that make the face appear
       recessed below the surrounding metal:
         - strong dark inset at the TOP (light is occluded by the lip of
           the deck casting shadow down onto the sunken face)
         - subtler dark inset on the LEFT (side walls of the well)
         - thin bright inset at the BOTTOM (light bouncing back up from
           inside the recess)
       Face also shifts slightly darker overall — less light reaches the
       bottom of a well than its rim. */
    background:
      repeating-linear-gradient(90deg,
        rgba(255,255,255,0.03) 0 1px,
        rgba(0,0,0,0.05) 1px 2px),
      linear-gradient(180deg, #9ea1a6 0%, #82868d 45%, #5a5e64 100%);
    box-shadow:
      0 0 0 1px #0a0a0e,
      0 3px 5px rgba(0,0,0,0.55) inset,
      2px 0 4px rgba(0,0,0,0.3) inset,
      -2px 0 4px rgba(0,0,0,0.3) inset,
      0 -1px 0 rgba(255,255,255,0.2) inset;
    /* 0s transition = instant snap to pressed state. No cross-fade. */
    transition: none;
  }
  .play-btn::after {
    /* status LED */
    content: '';
    position: absolute;
    top: 8px;
    left: 50%;
    transform: translateX(-50%);
    width: 6px; height: 6px;
    border-radius: 50%;
    background: #2a0608;
    box-shadow: 0 0 0 1px #000, 0 1px 1px rgba(0,0,0,0.5) inset;
    transition: background 0.2s, box-shadow 0.2s;
  }
  .play-btn.playing::after {
    background: var(--accent-hot);
    box-shadow: 0 0 0 1px #000, 0 0 8px var(--accent-hot);
  }
  .play-btn svg { width: 28px; height: 28px; display: block; }
  .play-btn .icon-play { display: block; }
  .play-btn .icon-pause { display: none; }
  .play-btn.playing .icon-play { display: none; }
  .play-btn.playing .icon-pause { display: block; }

  /* ===== LOADING state — spinner + blinking LED + disabled clicks =====
     Set on .deck-unit (the deck root) AND the matching .deck-mount (the
     sibling overlay holding the desktop play/transport controls) by
     setLoading() in DJ.js. Both parents need the rules because the
     fallback play-btn lives inside .deck-unit while the deck-mounted
     play-btn-mounted lives inside .deck-mount.

     Visual changes during loading:
       - Play/pause SVG icons are hidden (visibility, not display, so the
         button keeps its grid-centered layout — display:none on grid
         items would also remove the centering anchor).
       - A CSS-only spinner (::before with rotating border) appears in
         the center of the button face, replacing the icon.
       - The status LED (::after) blinks amber instead of dim red, giving
         a second cue that the deck is busy.
       - cursor: wait + pointer-events: none on play and transport buttons
         so the user can't trigger another load mid-flight. The Songs
         button stays clickable so the user CAN pick a different song.
       - Transport buttons dim slightly (filter brightness) to read as
         disabled. Play button doesn't dim — the spinner is the cue. */
  .deck-unit.loading .play-btn,
  .deck-unit.loading .transport-btn,
  .deck-mount.loading .play-btn,
  .deck-mount.loading .transport-btn {
    cursor: wait;
    pointer-events: none;
  }
  .deck-unit.loading .transport-btn,
  .deck-mount.loading .transport-btn {
    filter: brightness(0.7);
  }
  .deck-unit.loading .play-btn .icon-play,
  .deck-unit.loading .play-btn .icon-pause,
  .deck-mount.loading .play-btn .icon-play,
  .deck-mount.loading .play-btn .icon-pause {
    visibility: hidden;
  }
  .deck-unit.loading .play-btn::before,
  .deck-mount.loading .play-btn::before {
    content: '';
    position: absolute;
    /* Percentage sizing so the spinner scales with the button at every
       responsive breakpoint (72/56/50/42 px). 38% wide, 31% offset
       (= (100-38)/2) gives a centered square that's ~27px on a 72px
       button — same visual weight as the original play SVG (28px). */
    width: 38%;
    height: 38%;
    left: 31%;
    top: 31%;
    border: 3px solid rgba(0, 0, 0, 0.15);
    border-top-color: rgba(0, 0, 0, 0.7);
    border-radius: 50%;
    animation: deck-load-spin 0.7s linear infinite;
    pointer-events: none;
  }
  .deck-unit.loading .play-btn::after,
  .deck-mount.loading .play-btn::after {
    /* LED blinks amber — uses the brand --accent so it pairs visually
       with the Songs button's loading-affordance (the orange pulse).
       Override of the dim-red default and the playing-red rules. */
    background: var(--accent);
    box-shadow: 0 0 0 1px #000, 0 0 6px var(--accent);
    animation: deck-load-led 0.7s ease-in-out infinite alternate;
  }
  @keyframes deck-load-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
  }
  @keyframes deck-load-led {
    from { opacity: 1; }
    to   { opacity: 0.35; }
  }
  @media (prefers-reduced-motion: reduce) {
    .deck-unit.loading .play-btn::before,
    .deck-mount.loading .play-btn::before,
    .deck-unit.loading .play-btn::after,
    .deck-mount.loading .play-btn::after {
      animation: none;
    }
  }

  /* ===== Transport buttons — prev / next, flanking the play button =====
     Sized and styled to sit naturally alongside .play-btn inside a .play-wrap.
     Smaller diameter so the play button stays the hero; same metal bezel look
     minus the status LED. data-action lets you wire behavior later without
     touching markup. */
  .transport-btn {
    position: relative;
    width: 48px; height: 48px;
    border-radius: 10px;
    border: none;
    cursor: pointer;
    /* Same brushed-steel face as .play-btn, shifted slightly darker so the
       play button still reads as the hero. */
    background:
      repeating-linear-gradient(90deg,
        rgba(255,255,255,0.04) 0 1px,
        rgba(0,0,0,0.04) 1px 2px),
      linear-gradient(180deg, #c4c7cc 0%, #9498a0 45%, #60646a 100%);
    /* Flush rest state — same technique as .play-btn: a fine seam around
       the button where it meets the deck cutout, no drop shadow. */
    box-shadow:
      0 0 0 1px #0a0a0e,
      0 1px 0 rgba(255,255,255,0.35) inset,
      0 -1px 2px rgba(0,0,0,0.12) inset;
    display: grid;
    place-items: center;
    /* Quick release, instant press — see .play-btn for full rationale. */
    transition: box-shadow 0.08s ease-out, background 0.08s ease-out;
    touch-action: manipulation;
    color: #1a1a20;
  }
  .transport-btn:hover { color: #000; }
  .transport-btn:active {
    /* Sunk — same inner-shadow recess as .play-btn, lighter touch since
       these buttons are smaller. */
    background:
      repeating-linear-gradient(90deg,
        rgba(255,255,255,0.03) 0 1px,
        rgba(0,0,0,0.05) 1px 2px),
      linear-gradient(180deg, #8b8f95 0%, #72767c 45%, #4e5258 100%);
    box-shadow:
      0 0 0 1px #0a0a0e,
      0 2px 4px rgba(0,0,0,0.55) inset,
      2px 0 3px rgba(0,0,0,0.3) inset,
      -2px 0 3px rgba(0,0,0,0.3) inset,
      0 -1px 0 rgba(255,255,255,0.15) inset;
    transition: none;
  }
  .transport-btn svg { width: 18px; height: 18px; display: block; }

  /* Let .play-wrap hold prev + play + next in a row with bottoms aligned.
     flex-end on the cross-axis lines up the smaller transport buttons to
     the bottom of the taller play button, matching real DJ deck layouts
     where the top edge of smaller buttons sits lower than the main button. */
  .play-wrap { gap: 14px; align-items: flex-end; }

  .hint {
    margin-top: 22px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    color: var(--text-dim);
    letter-spacing: 0.15em;
    text-align: center;
  }
  .hint kbd {
    background: var(--metal-lo);
    border: 1px solid var(--metal-hi);
    border-radius: 3px;
    padding: 1px 5px;
    font-family: inherit;
    font-size: 10px;
    color: var(--text);
  }

  /* On narrow/stacked screens, reveal the .controls strip as a simple
     centered bar holding the fallback play buttons (since .deck-mount
     is hidden below 720px — see earlier media query). */
  @media (max-width: 720px) {
    .controls {
      width: min(58vmin, 648px);
      background: linear-gradient(180deg, var(--metal-hi) 0%, var(--metal) 20%, var(--metal-lo) 100%);
      border: 1px solid #000;
      border-radius: 14px;
      padding: 18px 22px;
      display: flex;
      justify-content: center;
      align-items: center;
      box-shadow:
        0 1px 0 rgba(255,255,255,0.06) inset,
        0 -1px 0 rgba(0,0,0,0.6) inset,
        0 20px 40px rgba(0,0,0,0.6),
        0 6px 14px rgba(0,0,0,0.5);
    }
  }

  /* ========================================================================
     RECORD CRATE — music library that opens over the decks
     ========================================================================
     Design constraint: the decks' vinyl/tonearm drag interactions are on a
     z-index: 1 layer (.deck-unit). The deck-mount overlay for play buttons
     is z-index: 4 and pointer-events: none so clicks pass through — only
     its button children re-enable pointer-events.

     The PICK A SONG button floats ABOVE the play button using position:
     absolute, so it does NOT participate in the play-wrap's flex row and
     doesn't push prev/next buttons inward toward the track counters.
     Its own bounding box (small) is the only area that captures clicks —
     everything around it falls through to the vinyl. */

  /* PICK A SONG button — small SQUARE tile ("SONGS") that sits above each
     deck's play button. Absolutely positioned inside the deck-mount so the
     play-wrap keeps its exact original width and position (critical:
     prevents overlap with the track counters in the crossfader cluster). */
  .crate-open-btn {
    position: absolute;
    /* Sits ABOVE the play button. Bottom offset = deck-mount's 1% bottom
       padding + play-btn height (72px) + 18px gap. The gap has to be
       generous because the play-wrap's padding-bottom (1%) resolves
       against the deck-mount's CONTAINING BLOCK width, while this
       absolute bottom's 1% resolves against the deck-mount's HEIGHT,
       so they're different values. 18px gives clear separation. */
    bottom: calc(1% + 72px + 18px);
    /* Same dimensions as .play-btn so the two buttons read as a paired
       set (play below, songs above) with matching size and radius. */
    width: 72px;
    height: 72px;
    padding: 6px 4px;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    font-family: 'Bricolage Grotesque', sans-serif;
    font-weight: 800;
    font-size: 11px;
    letter-spacing: 0.12em;
    line-height: 1;
    text-align: center;
    text-transform: uppercase;
    color: #2a1505;
    /* Orange chrome face — vertical gradient + faint vertical brush grain
       (same brush technique as .play-btn) for a "polished metal in
       orange" look. The brush stripes use very low contrast so they
       only register as texture, not as visible lines. */
    background:
      repeating-linear-gradient(90deg,
        rgba(255,255,255,0.04) 0 1px,
        rgba(0,0,0,0.04) 1px 2px),
      linear-gradient(180deg, #ffd07a 0%, #ffb347 50%, #e88820 100%);
    border: none;
    border-radius: 14px;
    /* Inlaid-button look matching .play-btn: a crisp dark seam ring, a
       fine top highlight (ambient light catching the seam edge), a soft
       bottom inset (button face's convex curve falling into shadow), and
       a subtle outer glow that conveys "this is the active CTA" without
       the previous version's loud halo. */
    box-shadow:
      0 0 0 1px #5a3a0c,
      0 1px 0 rgba(255, 255, 255, 0.55) inset,
      0 -1px 2px rgba(120, 60, 10, 0.4) inset,
      0 0 8px rgba(255, 150, 50, 0.22);
    cursor: pointer;
    transition: box-shadow 0.15s ease, background 0.08s ease-out;
    touch-action: manipulation;
    /* .deck-mount is pointer-events: none so clicks fall through to the
       vinyl; the button re-enables them for its own bounding box. */
    pointer-events: auto;
    z-index: 1;
  }
  /* Align each deck's songs button to the OUTER edge — same side AND same
     CSS x-position as the play button below it. Both buttons sit at 1%
     from the deck-mount edge.

     IMPORTANT: a small visual offset between the two buttons is intrinsic
     to the 3D perspective tilt and CANNOT be perfectly compensated. Here's
     why: the deck-mount is rendered as a perspective trapezoid (rotateX
     55deg + perspective(1600px)). The play button sits at the BOTTOM of
     the trapezoid where the steel surface is WIDEST in screen pixels,
     and the steel's left edge is at its outermost screen X. The songs
     button sits HIGHER UP where the steel is NARROWER and its left edge
     is more centered. Even though both buttons are at the same CSS
     position (1% from deck-mount edge), perspective projects the play
     button further outward in screen space than the songs button.

     A `translateX` to compensate would align the buttons' projected
     screen positions — but it would push the songs button OUTSIDE the
     steel surface at its own Y level (the steel's left edge AT the
     songs button's height is closer to center than the play button's
     left edge is). So the choice is: small visual offset between the
     buttons (current), or songs button hanging off the steel. We pick
     the former — both buttons stay rooted on the steel surface, and
     the perspective offset reads as natural depth rather than a layout
     bug.

     +3px inward nudge: per user request, both buttons shift 3px toward
     the screen center. For deck-A that means INCREASING `left` (further
     from the left edge); for deck-B that means INCREASING `right`
     (further from the right edge). */
  .deck-mount-a .crate-open-btn { left: calc(1% + 3px); }
  .deck-mount-b .crate-open-btn { right: calc(1% + 3px); }

  .crate-open-btn:hover {
    box-shadow:
      0 0 0 1px #5a3a0c,
      0 1px 0 rgba(255, 255, 255, 0.6) inset,
      0 -1px 2px rgba(120, 60, 10, 0.4) inset,
      0 0 14px rgba(255, 150, 50, 0.42);
  }
  .crate-open-btn:active {
    /* PRESSED = sinks INTO the deck, same technique as .play-btn:active.
       No translate; the illusion is built entirely from inner shadows
       that make the face appear recessed below the surrounding metal,
       plus a slightly darkened gradient. Keeps the visual language
       paired with the play button. */
    background:
      repeating-linear-gradient(90deg,
        rgba(255,255,255,0.03) 0 1px,
        rgba(0,0,0,0.05) 1px 2px),
      linear-gradient(180deg, #cc8a3a 0%, #b87224 50%, #8a5410 100%);
    box-shadow:
      0 0 0 1px #5a3a0c,
      0 3px 5px rgba(40, 20, 5, 0.55) inset,
      2px 0 4px rgba(40, 20, 5, 0.3) inset,
      -2px 0 4px rgba(40, 20, 5, 0.3) inset,
      0 -1px 0 rgba(255, 200, 120, 0.3) inset;
    transition: none;
  }
  .crate-open-btn svg {
    width: 36px;
    height: 36px;
    display: block;
    flex-shrink: 0;
    filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.35));
  }

  /* Gentle pulse while the associated deck has no album loaded — the cue
     that says "click here first". Only the outer glow varies; the inset
     highlights and seam stay constant so the button doesn't visibly
     "breathe" in shape, just brighten and dim. Managed by JS: `.empty`
     is removed when a record loads. */
  @keyframes crate-pulse {
    0%, 100% {
      box-shadow:
        0 0 0 1px #5a3a0c,
        0 1px 0 rgba(255, 255, 255, 0.55) inset,
        0 -1px 2px rgba(120, 60, 10, 0.4) inset,
        0 0 8px rgba(255, 150, 50, 0.22);
    }
    50% {
      box-shadow:
        0 0 0 1px #5a3a0c,
        0 1px 0 rgba(255, 255, 255, 0.65) inset,
        0 -1px 2px rgba(120, 60, 10, 0.4) inset,
        0 0 18px rgba(255, 150, 50, 0.55);
    }
  }
  .crate-open-btn.empty {
    animation: crate-pulse 2.4s ease-in-out infinite;
  }
  @media (prefers-reduced-motion: reduce) {
    .crate-open-btn.empty { animation: none; }
  }

  /* Full-screen overlay that holds the crate UI.
     CRITICAL: `visibility: hidden` is what actually keeps clicks passing
     through to the decks when the crate is closed — NOT `pointer-events: none`.
     Reason: pointer-events: none on the overlay itself blocks the overlay
     from being a target, but its descendants (the .crate box centered right
     over the turntables, record cards, close button) default to auto and
     would still swallow clicks silently. visibility: hidden inherits to
     descendants and makes the whole subtree non-hit-testable. Transition
     delay on visibility keeps the fade-out animation looking smooth. */
  .crate-overlay {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    background: radial-gradient(ellipse at 50% 30%, rgba(20,20,28,0.92) 0%, rgba(4,4,6,0.98) 75%);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
    /* visibility transition: 0s duration, 0.25s delay — snaps to hidden
       AFTER the opacity finishes fading out. */
    transition: opacity 0.25s ease, visibility 0s linear 0.25s;
  }
  .crate-overlay.open {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
    /* When opening, visibility snaps to visible with 0 delay so the
       opacity transition is visible from the start. */
    transition: opacity 0.25s ease, visibility 0s linear 0s;
  }

  .crate-close {
    position: absolute;
    top: 18px;
    right: 18px;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--metal);
    border: 1px solid #0a0a0e;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.08),
      inset 0 -1px 0 rgba(0,0,0,0.6);
    color: var(--text-dim);
    font-size: 18px;
    font-family: 'JetBrains Mono', monospace;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
    z-index: 2;
  }
  .crate-close:hover { color: var(--accent); border-color: var(--accent); }

  /* Wooden milk-crate frame.
     Sized to a generous block that hosts the album grid as its main body.
     Without the old meta/load/hint chrome below the viewport, the viewport
     itself can occupy ~all of the crate's interior. */
  .crate {
    position: relative;
    width: min(94vw, 1200px);
    height: min(88vh, 880px);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    padding: 26px 24px 20px;
    border-radius: 14px;
    background:
      linear-gradient(180deg,
        #2a1a0e 0%, #3a2614 8%, #2e1c10 16%,
        #3a2614 24%, #2a1a0e 32%, #3a2614 40%,
        #2e1c10 48%, #3a2614 56%, #2a1a0e 64%,
        #3a2614 72%, #2e1c10 80%, #3a2614 88%,
        #2a1a0e 100%);
    border: 2px solid #1a0e06;
    box-shadow:
      inset 0 1px 0 rgba(255,210,150,0.12),
      inset 0 -2px 0 rgba(0,0,0,0.5),
      0 30px 80px rgba(0,0,0,0.8);
    transform: scale(0.92);
    transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }
  .crate-overlay.open .crate { transform: scale(1); }
  .crate::before {
    content: '';
    position: absolute;
    inset: 4px;
    border-radius: 10px;
    background:
      repeating-linear-gradient(90deg,
        rgba(20,10,4,0.18) 0px,
        rgba(20,10,4,0.18) 1px,
        rgba(60,40,20,0.06) 1px,
        rgba(60,40,20,0.06) 3px,
        rgba(20,10,4,0.12) 3px,
        rgba(20,10,4,0.12) 4px);
    pointer-events: none;
    z-index: 0;
  }
  .crate::after {
    content: '';
    position: absolute;
    inset: 10px;
    border: 1px solid rgba(220,180,120,0.18);
    border-radius: 8px;
    pointer-events: none;
    z-index: 0;
  }

  .crate-tape {
    position: absolute;
    top: -8px;
    left: 50%;
    transform: translateX(-50%) rotate(-1.5deg);
    padding: 4px 28px;
    background: rgba(230, 220, 190, 0.88);
    color: #2a1a0e;
    font-family: 'Bricolage Grotesque', sans-serif;
    font-weight: 800;
    font-size: 11px;
    letter-spacing: 0.3em;
    text-transform: uppercase;
    box-shadow: 0 2px 4px rgba(0,0,0,0.3);
    z-index: 3;
  }

  /* Vertical-scrolling viewport that holds the responsive album grid.
     Replaces the old horizontal scroll-snap carousel. The grid layout
     itself lives on .crate-track (inside this viewport).
     - overflow-y: auto so long catalogs scroll vertically.
     - overflow-x: hidden because the grid is laid out to fit the viewport
       width; nothing should ever be off the right edge.
     - max-height clamped via min(...) so on tall desktop windows the grid
       takes most of the crate body, while on phones it doesn't push the
       crate taller than the viewport. */
  .crate-viewport {
    position: relative;
    width: 100%;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    z-index: 1;
    border-radius: 6px;
    box-shadow: inset 0 8px 16px rgba(0,0,0,0.45), inset 0 -4px 8px rgba(0,0,0,0.3);
    background: linear-gradient(180deg, #140a04 0%, #0a0604 100%);
    padding: 14px;
    /* Visible scrollbar — uses the brand orange so it pops against the
       dark wood interior of the crate. Wider than a default thin
       scrollbar (14px on WebKit) and "auto" width on Firefox so it gets
       the platform's normal scrollbar size rather than the thin variant.
       Track has a subtle inset shade to define the channel; thumb has
       a rounded edge and brightens on hover. */
    scrollbar-width: auto;
    scrollbar-color: var(--accent) rgba(0, 0, 0, 0.35);
  }
  .crate-viewport::-webkit-scrollbar {
    width: 14px;
  }
  .crate-viewport::-webkit-scrollbar-track {
    background: rgba(0, 0, 0, 0.35);
    border-radius: 7px;
    box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.5);
  }
  .crate-viewport::-webkit-scrollbar-thumb {
    background: linear-gradient(180deg, #ffd07a 0%, var(--accent) 50%, #e88820 100%);
    border-radius: 7px;
    border: 2px solid rgba(0, 0, 0, 0.35);
    background-clip: padding-box;
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.45),
      inset 0 -1px 0 rgba(120, 60, 10, 0.4);
  }
  .crate-viewport::-webkit-scrollbar-thumb:hover {
    background: linear-gradient(180deg, #ffe095 0%, #ffc56a 50%, #f0962a 100%);
    background-clip: padding-box;
  }

  /* The grid itself. `auto-fill` lets the browser pack as many columns as
     fit at the current viewport width, without collapsing empty tracks
     (auto-fit would, which is fine here since the catalog has 14 albums,
     but auto-fill is more predictable as the catalog grows). minmax(...)
     gives each card a floor of 140px so phones land on 2 columns and
     larger screens scale up naturally to 3, 4, 5, 6+ columns.
     The clamp() inside minmax lets the floor itself nudge down on very
     narrow phones (≤340px) so 2 columns still fit. */
  .crate-track {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(clamp(130px, 22vw, 180px), 1fr));
    gap: 14px;
    width: 100%;
    align-content: start;
  }

  .record-card {
    width: 100%;
    aspect-ratio: 1 / 1;
    transition: transform 180ms ease, filter 180ms ease;
    cursor: pointer;
    filter: drop-shadow(0 6px 10px rgba(0,0,0,0.5));
  }
  .record-card:hover {
    transform: translateY(-3px) scale(1.03);
    filter: drop-shadow(0 14px 20px rgba(0,0,0,0.7));
    z-index: 2;
  }
  .record-card:active {
    transform: translateY(-1px) scale(1.01);
    transition-duration: 60ms;
  }
  .record-card-inner {
    position: relative;
    width: 100%;
    height: 100%;
    border-radius: 3px;
    overflow: hidden;
    box-shadow:
      inset 0 0 0 1px rgba(0,0,0,0.6),
      inset 0 0 0 2px rgba(255,255,255,0.04),
      0 2px 6px rgba(0,0,0,0.6);
    background: #0a0a0a;
  }
  .record-card-inner .cover-art-svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
  }
  .record-card-inner::after {
    content: '';
    position: absolute;
    inset: 0;
    background:
      linear-gradient(115deg, transparent 45%, rgba(255,255,255,0.04) 50%, transparent 55%),
      radial-gradient(ellipse at 80% 15%, rgba(255,255,255,0.05) 0%, transparent 40%),
      radial-gradient(ellipse at 15% 85%, rgba(0,0,0,0.18) 0%, transparent 50%);
    pointer-events: none;
  }
  /* Bottom-darkening gradient for label legibility over photo cover art.
     The procedural SVG covers had predictable luminance so a thin shadow
     was enough; real photos vary wildly so we add a localized scrim that
     fades from transparent at ~55% down to ~78% black at the bottom edge.
     z-index 1 puts it above the cover SVG (z-index auto) but below the
     text labels (z-index 2). Lives on ::before so the existing sheen on
     ::after is preserved. */
  .record-card-inner::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(
      to bottom,
      rgba(0,0,0,0) 45%,
      rgba(0,0,0,0.35) 70%,
      rgba(0,0,0,0.78) 100%
    );
    pointer-events: none;
    z-index: 1;
  }
  .cover-title {
    position: absolute;
    left: 10%;
    right: 10%;
    bottom: 22%;
    font-family: 'Bricolage Grotesque', sans-serif;
    font-weight: 800;
    font-size: clamp(14px, 1.6vw, 22px);
    line-height: 0.95;
    color: var(--cover-title, #fff);
    text-transform: uppercase;
    /* Layered shadow: the close-in dark shadow gives a hard outline against
       any background; the wider blurred shadow adds a soft halo so the
       letters detach from busy texture (sky, foliage, etc). */
    text-shadow:
      0 1px 0 rgba(0,0,0,0.9),
      0 2px 6px rgba(0,0,0,0.85),
      0 0 14px rgba(0,0,0,0.55);
    z-index: 2;
    pointer-events: none;
  }
  .cover-artist {
    position: absolute;
    left: 10%;
    bottom: 12%;
    font-family: 'JetBrains Mono', monospace;
    font-size: clamp(8px, 0.85vw, 11px);
    font-weight: 600;
    letter-spacing: 0.18em;
    /* Was var(--cover-artist) — palette colors don't reliably contrast
       against photographic art. White + strong shadow does. */
    color: #fff;
    text-transform: uppercase;
    text-shadow:
      0 1px 0 rgba(0,0,0,0.9),
      0 1px 4px rgba(0,0,0,0.85);
    z-index: 2;
    pointer-events: none;
  }
  .cover-genre {
    position: absolute;
    top: 8%;
    left: 8%;
    /* Pill chip: gives the genre its own dark backdrop so it reads on any
       photo regardless of what's behind it. inline-block lets the chip
       size itself to the text rather than spanning the whole row.
       backdrop-filter on the rare browser that supports it adds a tiny
       blur behind the chip — gracefully ignored elsewhere. */
    display: inline-block;
    padding: 4px 9px;
    background: rgba(0, 0, 0, 0.62);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 999px;
    font-family: 'JetBrains Mono', monospace;
    font-size: clamp(7px, 0.8vw, 10px);
    font-weight: 700;
    letter-spacing: 0.22em;
    color: #fff;
    text-transform: uppercase;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
    z-index: 2;
    pointer-events: none;
  }
  /* Note: the old .crate-meta, .crate-load, and .crate-hint styles were
     removed when the grid layout replaced the carousel. The grid shows all
     metadata on the cards themselves, click-to-load replaced the LOAD
     button, and the keyboard-shortcut hint referenced nav keys that no
     longer apply. The .cover-badge (INSTRUMENTAL / ACAPELLA tag) was
     removed alongside the album.type field — albums no longer carry a
     stem-type since none of the current catalog uses it meaningfully. */

  /* Album art on the record label. Sits inside .rotor so it spins with the
     record. Lives BEHIND .label-text (DOM order) so the arc text reads on
     top. Empty by default — hidden via :empty — and populated by JS on
     load. pointer-events: none throughout so it NEVER blocks scratching. */
  .label-art {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 30%;
    height: 30%;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    overflow: hidden;
    pointer-events: none;
    box-shadow:
      inset 0 0 0 1px rgba(0,0,0,0.55),
      inset 0 0 0 2px rgba(255,255,255,0.08);
  }
  .label-art:empty { display: none; }
  .label-art svg { width: 100%; height: 100%; display: block; }

  /* Transparent click-zone over the record label. Sits ABOVE .label-art
     and .label-text (higher z-index) so clicks on the label open the
     crate. Sized identically to the label so the interactive area
     matches what the user visually perceives as "the label". */
  .label-click {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 30%;
    height: 30%;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    cursor: pointer;
    /* Re-enables pointer events against the vinyl's scratch handler which
       sits below it. pointerdown is stopPropagation'd in JS so clicking
       the label DOESN'T also trigger a scratch. */
    pointer-events: auto;
    /* Above all label visuals so the hit target is always on top. */
    z-index: 6;
    background: transparent;
  }
  /* Subtle hover affordance — a faint brightening of the label area to
     hint the label is clickable. */
  .label-click:hover {
    box-shadow: inset 0 0 0 2px rgba(255, 179, 71, 0.45);
  }
  /* When an album is loaded, arc text flips to white-with-dark-outline so
     it stays readable on top of the colorful album art. */
  .vinyl.has-album .label-text .label-top,
  .vinyl.has-album .label-text .label-bottom {
    fill: #fff;
    paint-order: stroke fill;
    stroke: rgba(0,0,0,0.7);
    stroke-width: 0.45;
  }

  @keyframes record-land {
    0%   { opacity: 0; transform: perspective(1000px) rotateX(55deg) scale(1.4); }
    55%  { opacity: 1; transform: perspective(1000px) rotateX(55deg) scale(0.96); }
    100% { opacity: 1; transform: perspective(1000px) rotateX(55deg) scale(1.0); }
  }
  .vinyl.landing { animation: record-land 550ms cubic-bezier(0.2, 0.8, 0.3, 1.05); }

  /* Narrow-screen tweaks for the crate dialog itself.
     The viewport's height is flex-driven now (flex: 1 on .crate-viewport
     fills the .crate container's interior height), so we don't set an
     explicit height here — that would fight the flex sizing. We just
     trim padding so the crate doesn't waste pixels on a phone. */
  @media (max-width: 720px) {
    .crate { width: 96vw; padding: 18px 14px 14px; }
  }