  :root {
    --orb-hue: 200;
    --aurora-a: 200; --aurora-b: 280; --aurora-c: 120;
    --aurora-d: 50;  --aurora-e: 320;
    --aurora-f: 170; --aurora-g: 30;
    --bg: #06060a;
    --ink: #ece6d5;
    --ink-dim: rgba(236, 230, 213, 0.42);
    --ink-faint: rgba(236, 230, 213, 0.16);
    --glass: rgba(236, 230, 213, 0.06);
    --glass-border: rgba(236, 230, 213, 0.14);
  }

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

  html, body {
    height: 100%;
    width: 100%;
    overflow: hidden;
    background: var(--bg);
    color: var(--ink);
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    user-select: none;
    -webkit-user-select: none;
    -webkit-tap-highlight-color: transparent;
    touch-action: none;
    overscroll-behavior: none;
  }

  /* ── deepest layer: two counter-rotating conic gradients ────────────── */
  .conic-rays {
    position: fixed;
    inset: -50%;
    z-index: 0;
    pointer-events: none;
    background: conic-gradient(from 0deg at 50% 50%,
      hsla(0,   80%, 55%, 0.12) 0%,
      hsla(40,  80%, 55%, 0.04) 8%,
      hsla(80,  80%, 55%, 0.12) 16%,
      hsla(120, 80%, 55%, 0.04) 24%,
      hsla(170, 80%, 55%, 0.12) 32%,
      hsla(210, 80%, 55%, 0.04) 40%,
      hsla(260, 80%, 55%, 0.12) 48%,
      hsla(300, 80%, 55%, 0.04) 56%,
      hsla(330, 80%, 55%, 0.12) 64%,
      hsla(0,   80%, 55%, 0.04) 72%,
      hsla(40,  80%, 55%, 0.12) 80%,
      hsla(120, 80%, 55%, 0.04) 88%,
      hsla(260, 80%, 55%, 0.12) 100%);
    filter: blur(60px);
    mix-blend-mode: screen;
    opacity: 0.55;
    animation: spin-cw 140s linear infinite;
    will-change: transform;
  }
  .conic-rays-2 {
    position: fixed;
    inset: -50%;
    z-index: 0;
    pointer-events: none;
    background: conic-gradient(from 180deg at 50% 50%,
      hsla(20,  70%, 55%, 0.06) 0%,
      hsla(180, 70%, 55%, 0.10) 20%,
      hsla(60,  70%, 55%, 0.05) 40%,
      hsla(280, 70%, 55%, 0.10) 60%,
      hsla(140, 70%, 55%, 0.06) 80%,
      hsla(20,  70%, 55%, 0.10) 100%);
    filter: blur(80px);
    mix-blend-mode: screen;
    opacity: 0.45;
    animation: spin-ccw 200s linear infinite;
    will-change: transform;
  }
  @keyframes spin-cw  { to { transform: rotate(360deg); } }
  @keyframes spin-ccw { to { transform: rotate(-360deg); } }

  /* ── aurora — 7 colored fog blobs ───────────────────────────────────── */
  .aurora {
    position: fixed;
    inset: -15%;
    z-index: 1;
    pointer-events: none;
    filter: blur(90px) saturate(1.35);
    opacity: 0.7;
    transition: opacity 800ms;
  }
  .aurora.dim { opacity: 0.34; }

  .aurora-blob {
    position: absolute;
    border-radius: 50%;
    mix-blend-mode: screen;
    will-change: transform, background;
    transition: background 1200ms ease;
  }
  .blob-a {
    width: 70vmax; height: 70vmax;
    top: -20%; left: -25%;
    background: radial-gradient(circle, hsl(var(--aurora-a), 75%, 55%) 0%, transparent 55%);
    animation: drift-a 38s ease-in-out infinite;
  }
  .blob-b {
    width: 60vmax; height: 60vmax;
    bottom: -25%; right: -20%;
    background: radial-gradient(circle, hsl(var(--aurora-b), 75%, 50%) 0%, transparent 55%);
    animation: drift-b 46s ease-in-out infinite;
  }
  .blob-c {
    width: 80vmax; height: 80vmax;
    top: 30%; left: 15%;
    background: radial-gradient(circle, hsl(var(--aurora-c), 70%, 45%) 0%, transparent 55%);
    animation: drift-c 58s ease-in-out infinite;
  }
  .blob-d {
    width: 55vmax; height: 55vmax;
    top: -10%; right: 5%;
    background: radial-gradient(circle, hsl(var(--aurora-d), 80%, 52%) 0%, transparent 55%);
    animation: drift-d 52s ease-in-out infinite;
  }
  .blob-e {
    width: 65vmax; height: 65vmax;
    bottom: -5%; left: 25%;
    background: radial-gradient(circle, hsl(var(--aurora-e), 72%, 48%) 0%, transparent 55%);
    animation: drift-e 64s ease-in-out infinite;
  }
  .blob-f {
    width: 50vmax; height: 50vmax;
    top: 40%; right: 20%;
    background: radial-gradient(circle, hsl(var(--aurora-f), 78%, 50%) 0%, transparent 55%);
    animation: drift-f 70s ease-in-out infinite;
  }
  .blob-g {
    width: 60vmax; height: 60vmax;
    bottom: 20%; left: -10%;
    background: radial-gradient(circle, hsl(var(--aurora-g), 75%, 50%) 0%, transparent 55%);
    animation: drift-g 56s ease-in-out infinite;
  }
  @keyframes drift-a {
    0%, 100% { transform: translate(0, 0) scale(1); }
    33%      { transform: translate(20vw, 18vh) scale(1.15); }
    66%      { transform: translate(-12vw, 28vh) scale(0.95); }
  }
  @keyframes drift-b {
    0%, 100% { transform: translate(0, 0) scale(1); }
    50%      { transform: translate(-25vw, -22vh) scale(1.2); }
  }
  @keyframes drift-c {
    0%, 100% { transform: translate(0, 0) scale(1); }
    25%      { transform: translate(15vw, -18vh) scale(0.9); }
    75%      { transform: translate(-18vw, 14vh) scale(1.1); }
  }
  @keyframes drift-d {
    0%, 100% { transform: translate(0, 0) scale(1); }
    40%      { transform: translate(-22vw, 24vh) scale(1.18); }
    80%      { transform: translate(8vw, -16vh) scale(0.92); }
  }
  @keyframes drift-e {
    0%, 100% { transform: translate(0, 0) scale(1); }
    50%      { transform: translate(18vw, -28vh) scale(1.1); }
  }
  @keyframes drift-f {
    0%, 100% { transform: translate(0, 0) scale(1); }
    35%      { transform: translate(-16vw, 16vh) scale(1.12); }
    70%      { transform: translate(20vw, -12vh) scale(0.94); }
  }
  @keyframes drift-g {
    0%, 100% { transform: translate(0, 0) scale(1); }
    45%      { transform: translate(22vw, 18vh) scale(1.16); }
    85%      { transform: translate(-8vw, -22vh) scale(0.92); }
  }

  /* ── bokeh layer — drifting soft colored circles ────────────────────── */
  .bokeh {
    position: fixed;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    overflow: hidden;
  }
  .bokeh-dot {
    position: absolute;
    border-radius: 50%;
    background: radial-gradient(circle, hsla(var(--bokeh-hue, 200), 80%, 60%, 0.32) 0%, hsla(var(--bokeh-hue, 200), 80%, 60%, 0.04) 60%, transparent 80%);
    mix-blend-mode: screen;
    filter: blur(14px) saturate(1.2);
    transform: translate(-50%, -50%);
    will-change: transform, background;
    transition: background 1200ms ease;
    animation: bokeh-drift var(--bokeh-dur, 60s) ease-in-out var(--bokeh-delay, 0s) infinite alternate;
  }
  @keyframes bokeh-drift {
    0%   { transform: translate(calc(-50% + var(--bokeh-x0, 0px)), calc(-50% + var(--bokeh-y0, 0px))) scale(0.85); opacity: 0.45; }
    50%  { opacity: 0.85; }
    100% { transform: translate(calc(-50% + var(--bokeh-x1, 80px)), calc(-50% + var(--bokeh-y1, -60px))) scale(1.2); opacity: 0.55; }
  }

  /* ── multi-orb spotlights (set via JS) ──────────────────────────────── */
  .spotlight {
    position: fixed;
    inset: 0;
    z-index: 2;
    pointer-events: none;
    mix-blend-mode: screen;
    background: transparent;
    transition: background 80ms linear;
  }

  /* ── film grain ──────────────────────────────────────────────────────── */
  #grain {
    position: fixed;
    inset: -50%;
    pointer-events: none;
    z-index: 50;
    opacity: 0.05;
    mix-blend-mode: overlay;
    background-image: url("data:image/svg+xml;utf8,<svg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.92' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
    animation: grain 8s steps(8) infinite;
  }
  @keyframes grain {
    0%   { transform: translate(0, 0); }
    25%  { transform: translate(-3%, 4%); }
    50%  { transform: translate(2%, -3%); }
    75%  { transform: translate(-2%, 2%); }
    100% { transform: translate(0, 0); }
  }

  /* ── horizon hairlines ──────────────────────────────────────────────── */
  .horizon {
    position: fixed;
    left: 0; right: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--ink-faint) 30%, var(--ink-faint) 70%, transparent);
    z-index: 3;
    pointer-events: none;
  }
  .horizon.top { top: 24%; }
  .horizon.bot { bottom: 24%; }

  /* ── chrome ──────────────────────────────────────────────────────────── */
  header {
    position: fixed;
    top: 28px;
    left: 32px;
    z-index: 10;
    pointer-events: none;
  }
  h1 {
    font-family: 'Fraunces', serif;
    font-weight: 300;
    font-style: italic;
    font-size: 42px;
    font-variation-settings: "opsz" 144, "SOFT" 100;
    letter-spacing: -0.035em;
    line-height: 0.95;
  }

  /* Slowly-shifting gradient on the start-screen "flowstate" wordmark.
     6-stop spectrum loops every 18s. background-clip: text confines the
     gradient to the glyph fill. drop-shadow gives the moving color a
     soft glow without affecting the rest of the start screen. */
  .start-title {
    background: linear-gradient(
      90deg,
      #c9a962 0%,    /* gold */
      #ff8aa6 16%,   /* coral */
      #b074ff 32%,   /* violet */
      #6ec5ff 48%,   /* sapphire */
      #66e6c4 64%,   /* mint */
      #ffd27b 80%,   /* amber */
      #c9a962 100%   /* loop */
    );
    background-size: 300% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    animation: start-title-shift 18s linear infinite;
    filter: drop-shadow(0 0 14px hsla(40, 70%, 60%, 0.22));
  }
  @keyframes start-title-shift {
    0%   { background-position:   0% 50%; }
    100% { background-position: 300% 50%; }
  }
  h1 .dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: hsl(var(--orb-hue), 80%, 65%);
    box-shadow: 0 0 12px hsla(var(--orb-hue), 90%, 65%, 0.6);
    margin-left: 4px;
    transform: translateY(-22px);
    transition: background 100ms, box-shadow 100ms;
  }
  .byline {
    font-size: 9px;
    letter-spacing: 0.32em;
    text-transform: uppercase;
    margin-top: 14px;
    color: var(--ink-dim);
  }

  .legend {
    position: fixed;
    top: 32px;
    right: 32px;
    z-index: 10;
    text-align: right;
    pointer-events: none;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
  }
  /* The help button — re-enable pointer events just on this child since
     the .legend wrapper has pointer-events:none to keep the orb canvas
     fully interactive behind the chrome. */
  #btn-help {
    pointer-events: auto;
    appearance: none;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.18);
    color: rgba(255, 255, 255, 0.85);
    width: 28px;
    height: 28px;
    border-radius: 50%;
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-weight: 400;
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    transition: background 150ms, border-color 150ms, color 150ms, transform 150ms;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
  }
  #btn-help:hover {
    background: rgba(255, 215, 0, 0.10);
    border-color: rgba(255, 215, 0, 0.45);
    color: hsl(40, 75%, 78%);
    transform: scale(1.05);
  }
  #btn-help:active { transform: scale(0.95); }

  /* Zen-mode toggle — sits next to the help button in the legend.
     Wider than ?-button since it's a 3-letter word; same dim-pill look. */
  #btn-zen {
    pointer-events: auto;
    appearance: none;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.18);
    color: rgba(255, 255, 255, 0.78);
    height: 26px;
    padding: 0 12px;
    border-radius: 999px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 150ms, border-color 150ms, color 150ms, transform 150ms;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  #btn-zen:hover {
    background: rgba(110, 197, 255, 0.10);
    border-color: rgba(110, 197, 255, 0.42);
    color: hsl(205, 80%, 78%);
    transform: scale(1.04);
  }
  #btn-zen:active { transform: scale(0.96); }

  /* Reese bass toggle — same pill shape as #btn-zen. Glows hue-tinted
     when the drone is active so the user can tell at a glance whether
     bass is running underneath the orbs. */
  #btn-bass {
    pointer-events: auto;
    appearance: none;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.18);
    color: rgba(255, 255, 255, 0.78);
    height: 26px;
    padding: 0 12px;
    border-radius: 999px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 150ms, border-color 150ms, color 150ms,
                transform 150ms, box-shadow 200ms;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  #btn-bass:hover {
    background: rgba(180, 130, 255, 0.10);
    border-color: rgba(180, 130, 255, 0.42);
    color: hsl(270, 70%, 80%);
    transform: scale(1.04);
  }
  #btn-bass:active { transform: scale(0.96); }
  #btn-bass.active {
    background: hsla(270, 75%, 50%, 0.22);
    border-color: hsla(270, 80%, 65%, 0.65);
    color: hsl(270, 80%, 85%);
    box-shadow: 0 0 14px hsla(270, 80%, 60%, 0.45);
  }

  /* Bass volume slider — sits directly below the bass pill in the
     legend column. Stays visible even when bass is off (just dimmer)
     so the user discovers it; brightens when bass is active. The
     hue echoes #btn-bass's purple. Slim horizontal range so it fits
     the legend's right-aligned narrow column. */
  #bass-vol {
    pointer-events: auto;
    -webkit-appearance: none;
    appearance: none;
    width: 76px;
    height: 4px;
    border-radius: 999px;
    background: linear-gradient(to right,
      hsla(270, 70%, 60%, 0.55) 0%,
      hsla(270, 70%, 60%, 0.55) calc(var(--bass-vol, 0.45) * 100%),
      rgba(255, 255, 255, 0.10) calc(var(--bass-vol, 0.45) * 100%),
      rgba(255, 255, 255, 0.10) 100%);
    outline: none;
    cursor: pointer;
    opacity: 0.45;
    transition: opacity 200ms;
    margin-right: 4px;
  }
  #btn-bass.active ~ #bass-vol,
  #bass-vol:hover,
  #bass-vol:focus { opacity: 1; }
  #bass-vol::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: hsl(270, 80%, 75%);
    border: 1px solid hsla(270, 80%, 90%, 0.6);
    box-shadow: 0 0 6px hsla(270, 80%, 60%, 0.55);
    cursor: pointer;
  }
  #bass-vol::-moz-range-thumb {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: hsl(270, 80%, 75%);
    border: 1px solid hsla(270, 80%, 90%, 0.6);
    box-shadow: 0 0 6px hsla(270, 80%, 60%, 0.55);
    cursor: pointer;
  }

  /* Zen-mode exit pill — fixed top-right, only shown while .zen-mode
     is active on body. Subtle by default so it doesn't break immersion;
     more prominent on hover. */
  #zen-exit {
    position: fixed;
    top: 24px;
    right: 24px;
    z-index: 200;
    appearance: none;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.14);
    color: rgba(255, 255, 255, 0.55);
    padding: 8px 16px;
    border-radius: 999px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    cursor: pointer;
    transition: opacity 400ms ease, background 200ms, color 200ms;
    pointer-events: auto;
    display: none;
  }
  #zen-exit:hover {
    background: rgba(255, 255, 255, 0.10);
    color: rgba(255, 255, 255, 0.92);
  }

  /* Zen-mode chrome hiding — all UI elements fade out except the canvas
     + orbs + the exit pill. Pointer events disabled so taps fall through
     to orbs even before the opacity transition finishes. */
  body.zen-mode header,
  body.zen-mode .legend,
  body.zen-mode .scale-toggle,
  body.zen-mode #readout,
  body.zen-mode .transport,
  body.zen-mode .horizon,
  body.zen-mode #orb-panel,
  body.zen-mode .encoding-toast {
    opacity: 0;
    pointer-events: none;
    transition: opacity 600ms ease;
  }
  body.zen-mode #zen-exit { display: block; }

  /* ── Secret 5-point star orb (glass / crystal) ──────────────────────
     Shape: clip-path polygon — 10 points walking the outer/inner radii
     of a 5-point star (apex at 12 o'clock). Look: translucent crystal,
     mostly clear interior with a bright highlight at upper-left and
     soft cool-white halos extending outward. clip-path strips both
     box-shadow and border, so the rim/glow are layered as drop-shadow
     filters (which DO follow the clipped shape). The voice picker is
     hidden on stars so the bell timbre stays locked — visual + sonic
     identity together. */
  .orb-wrap.star .orb-body {
    background: radial-gradient(circle at 38% 30%,
      rgba(255, 255, 255, 0.85) 0%,
      rgba(230, 240, 255, 0.32) 26%,
      rgba(190, 215, 245, 0.14) 58%,
      rgba(150, 185, 225, 0.05) 100%);
    box-shadow: none;
    clip-path: polygon(
      50% 2%,
      61% 35%,
      96% 35%,
      68% 57%,
      79% 92%,
      50% 71%,
      21% 92%,
      32% 57%,
      4% 35%,
      39% 35%
    );
    filter:
      drop-shadow(0 0 12px rgba(255, 255, 255, 0.70))
      drop-shadow(0 0 30px rgba(220, 235, 255, 0.40))
      drop-shadow(0 0 64px rgba(190, 220, 255, 0.22));
    animation: star-pulse 3.2s ease-in-out infinite;
  }
  /* Hide the soft outer-bloom ::after on stars (clip-path would chop
     it anyway, but explicit removal keeps the GPU cleaner). */
  .orb-wrap.star .orb-body::after { display: none; }
  /* Voice picker is locked on stars — the bell timbre is part of the
     star's identity. Other controls (speed/brightness) still work. */
  /* Hide the voice-pick on stars in BOTH locations: while the controls
     are still nested inside .orb-wrap.star (desktop / pre-selection),
     and after they've been reparented into #orb-panel on mobile. The
     .is-star class is set by spawnSecretStar() on the .orb-controls
     element itself, so it travels with it across reparents. */
  .orb-wrap.star .voice-pick,
  .orb-controls.is-star .voice-pick { display: none; }
  /* Slow crystal breathing while idle — pure white / cool-blue halos. */
  @keyframes star-pulse {
    0%, 100% {
      filter:
        drop-shadow(0 0 12px rgba(255, 255, 255, 0.70))
        drop-shadow(0 0 30px rgba(220, 235, 255, 0.40))
        drop-shadow(0 0 64px rgba(190, 220, 255, 0.22));
    }
    50% {
      filter:
        drop-shadow(0 0 20px rgba(255, 255, 255, 0.92))
        drop-shadow(0 0 44px rgba(220, 235, 255, 0.55))
        drop-shadow(0 0 90px rgba(190, 220, 255, 0.32));
    }
  }

  .legend .row {
    font-size: 9px;
    letter-spacing: 0.28em;
    text-transform: uppercase;
    color: var(--ink-dim);
    line-height: 1.8;
  }
  .legend .row span { color: var(--ink); }
  .legend .row span#orb-count {
    color: hsl(var(--orb-hue), 60%, 78%);
    transition: color 200ms;
  }
  .legend .row.tip {
    margin-top: 8px;
    color: var(--ink-faint);
    font-size: 8px;
    letter-spacing: 0.22em;
  }

  #readout {
    position: fixed;
    bottom: 32px;
    left: 32px;
    display: flex;
    gap: 30px;
    z-index: 10;
    pointer-events: none;
  }
  #readout > div {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 64px;
  }
  #readout .label {
    font-size: 8px;
    letter-spacing: 0.32em;
    color: var(--ink-dim);
    text-transform: uppercase;
  }
  #readout .value {
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-size: 26px;
    font-weight: 300;
    color: hsl(var(--orb-hue), 65%, 78%);
    line-height: 1;
    transition: color 200ms;
    font-variation-settings: "opsz" 144, "SOFT" 100;
  }
  #readout .unit {
    font-family: 'JetBrains Mono', monospace;
    font-size: 9px;
    letter-spacing: 0.2em;
    color: var(--ink-dim);
    margin-left: 4px;
    font-style: normal;
  }

  /* Key picker affordance — applies on desktop AND mobile so the
     "tap to change key" gesture is discoverable everywhere. The friend
     who beta-tested noted the existing key-lock "wasn't intuitive";
     adding the chevron + pointer cursor + a brief on-load pulse makes
     it read as a button rather than a passive label. */
  #readout > div.key-tappable {
    pointer-events: auto;
    cursor: pointer;
    transition: opacity 150ms;
  }
  #readout > div.key-tappable:hover .value { color: hsl(40, 80%, 78%); }
  #readout > div.key-tappable:active { opacity: 0.7; }
  #readout > div.key-tappable .label::after {
    content: ' ▾';
    opacity: 0.5;
    margin-left: 2px;
    font-size: 9px;
  }
  /* Brief 4-second highlight pulse on first start so newcomers notice
     the key cell is interactive. Removed via JS once the user has
     tapped it OR after 8 seconds. */
  #readout > div.key-hint .label {
    color: hsl(40, 75%, 75%);
    animation: key-hint-pulse 1.6s ease-in-out infinite;
  }
  #readout > div.key-hint .label::after {
    opacity: 0.95;
    color: hsl(40, 90%, 78%);
  }
  @keyframes key-hint-pulse {
    0%, 100% { opacity: 0.65; }
    50%      { opacity: 1.0; }
  }

  /* ── scale toggle (bottom-center) ───────────────────────────────────── */
  .scale-toggle {
    position: fixed;
    bottom: 36px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 10;
    display: flex;
    gap: 4px;
    padding: 4px;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    border-radius: 999px;
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
  }
  .pill {
    appearance: none;
    background: transparent;
    border: none;
    padding: 8px 18px;
    border-radius: 999px;
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-weight: 300;
    font-size: 14px;
    color: var(--ink-dim);
    cursor: pointer;
    transition: color 200ms, background 250ms;
    line-height: 1;
  }
  .pill:hover { color: var(--ink); }
  .pill.active {
    background: hsla(var(--orb-hue), 70%, 55%, 0.25);
    color: hsl(var(--orb-hue), 60%, 88%);
    box-shadow: inset 0 0 0 1px hsla(var(--orb-hue), 70%, 65%, 0.35);
  }

  /* ── transport ───────────────────────────────────────────────────────── */
  .transport {
    position: fixed;
    bottom: 32px;
    right: 32px;
    z-index: 10;
    display: flex;
    gap: 12px;
    align-items: center;
  }
  .ctrl {
    appearance: none;
    width: 52px;
    height: 52px;
    border-radius: 50%;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    color: var(--ink);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
                background 250ms, box-shadow 250ms, border-color 250ms,
                opacity 250ms;
    position: relative;
  }
  .ctrl:hover {
    transform: scale(1.06);
    background: rgba(236, 230, 213, 0.10);
    border-color: rgba(236, 230, 213, 0.25);
  }
  .ctrl:active { transform: scale(0.96); }
  .ctrl svg { width: 18px; height: 18px; }
  .ctrl[disabled] { opacity: 0.3; pointer-events: none; }
  .ctrl.add {
    color: hsl(var(--orb-hue), 60%, 80%);
    border-color: hsla(var(--orb-hue), 70%, 65%, 0.3);
  }
  .ctrl.add:hover {
    background: hsla(var(--orb-hue), 70%, 50%, 0.18);
    border-color: hsla(var(--orb-hue), 70%, 65%, 0.6);
    box-shadow: 0 0 18px hsla(var(--orb-hue), 70%, 60%, 0.3);
  }
  .ctrl.recording {
    background: hsla(0, 80%, 50%, 0.22);
    border-color: hsla(0, 90%, 60%, 0.5);
    box-shadow:
      0 0 18px hsla(0, 90%, 55%, 0.4),
      inset 0 0 12px hsla(0, 80%, 60%, 0.2);
    animation: recPulse 1.4s ease-in-out infinite;
  }
  @keyframes recPulse {
    0%, 100% { box-shadow: 0 0 14px hsla(0, 90%, 55%, 0.35), inset 0 0 10px hsla(0, 80%, 60%, 0.15); }
    50%      { box-shadow: 0 0 28px hsla(0, 95%, 60%, 0.6),  inset 0 0 14px hsla(0, 80%, 60%, 0.3); }
  }
  .ctrl.recording .rec-dot { fill: hsl(0, 90%, 62%); }
  .rec-time {
    position: absolute;
    bottom: -22px;
    left: 50%;
    transform: translateX(-50%);
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px;
    letter-spacing: 0.12em;
    color: hsl(0, 70%, 70%);
    white-space: nowrap;
    opacity: 0;
    transition: opacity 250ms;
  }
  .ctrl.recording .rec-time { opacity: 1; }

  .encoding-toast {
    position: fixed;
    bottom: 96px;
    right: 32px;
    z-index: 20;
    padding: 10px 16px;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border-radius: 10px;
    font-size: 10px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink-dim);
    opacity: 0;
    transition: opacity 300ms;
    pointer-events: none;
  }
  .encoding-toast.show { opacity: 1; }
  .encoding-toast.success { color: hsl(140, 60%, 75%); }

  #trails {
    position: fixed;
    inset: 0;
    z-index: 4;
    pointer-events: none;
  }

  /* ── orb wrapper / body / controls ──────────────────────────────────── */
  .orb-wrap {
    position: fixed;
    z-index: 6;
    --hue: 200;
    --size: 150px;
    --bright: 0.85;
    /* left/top set inline in JS */
  }
  .orb-wrap.satellite { --size: 92px; }

  /*
   * Orb body — uses the GPT-generated photoreal sphere image, pre-processed
   * to have a real alpha channel via luminance-keyed transparency.
   *
   * Strategy:
   *   1. background-image is the orb WebP (800×800, RGBA with smooth alpha).
   *      Real alpha means no mix-blend-mode hack and no stacking-context
   *      gotchas — it composites cleanly on top of the aurora layers below.
   *   2. background-size: 108% so the orb's outer bloom slightly extends
   *      past the div's circular clip, hiding any visible edge.
   *   3. filter chain drives color (hue-rotate), brightness, saturation.
   *      Source image is centered around hue 200 (sapphire), so the
   *      hue-rotate offset = (target_hue - 200) deg.
   *   4. State classes (shift, paused, muted) override filter — each must
   *      re-declare the full chain since CSS `filter` is a single property.
   *   5. box-shadow halos extend the orb's color into the surrounding
   *      aurora layers — these create the room's ambient color wash.
   */
  .orb-body {
    position: absolute;
    top: 0; left: 0;
    width: var(--size);
    height: var(--size);
    border-radius: 50%;
    cursor: grab;
    transform: translate(-50%, -50%) scale(var(--orb-scale, 1));
    background-image: url("lib/orb-master.webp");
    background-size: 108% 108%;
    background-position: center;
    background-repeat: no-repeat;
    filter:
      hue-rotate(calc((var(--hue) - 200) * 1deg))
      saturate(calc(0.85 + var(--bright) * 0.55))
      brightness(calc(0.55 + var(--bright) * 0.6))
      contrast(1.05);
    /* Tight inner glow only — a small box-shadow that extends the
       orb's color just past its edge. The wider, softer atmospheric
       bloom is now on the ::after pseudo-element below, where
       mix-blend-mode: screen makes overlapping orbs additively
       combine (real bioluminescence physics) instead of muddying
       each other's halos. */
    box-shadow:
      0 0 calc(var(--size) * 0.18 * var(--bright)) hsla(var(--hue), 85%, 60%, calc(0.22 * var(--bright)));
    transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
                filter 280ms ease,
                box-shadow 250ms;
    will-change: transform, filter;
    isolation: isolate; /* keep ::after's screen-blend scoped to this orb */
  }
  /* Inner specular comes from the orb image itself — no ::before. */
  .orb-body::before { display: none; }
  /* Outer atmospheric bloom — mix-blend-mode: screen + heavy blur =
     soft additive light that integrates with neighbors instead of
     stacking opaque halos. Wide enough (40% inset) to read as ambient
     wash, faded enough (radial gradient → transparent at 60%) that
     the orb's own edge stays the focal point. */
  .orb-body::after {
    content: '';
    position: absolute;
    inset: -40%;
    border-radius: 50%;
    background: radial-gradient(
      circle,
      hsla(var(--hue), 78%, 62%, calc(0.55 * var(--bright))) 0%,
      hsla(var(--hue), 78%, 62%, calc(0.20 * var(--bright))) 35%,
      transparent 60%
    );
    mix-blend-mode: screen;
    filter: blur(14px);
    pointer-events: none;
    z-index: -1;
    transition: opacity 200ms;
  }
  /* When orbs are attached as a pair, their bloom blends should
     integrate even tighter — increase the screen-blend amplitude so
     the two glows visibly merge rather than just overlap. */
  .orb-wrap.attached .orb-body::after {
    background: radial-gradient(
      circle,
      hsla(var(--hue), 80%, 65%, calc(0.75 * var(--bright))) 0%,
      hsla(var(--hue), 80%, 65%, calc(0.30 * var(--bright)))  35%,
      transparent 65%
    );
  }
  .orb-wrap.dragging .orb-body {
    --orb-scale: 0.94;
    cursor: grabbing;
    transition: transform 80ms ease-out;
  }
  .orb-wrap.dragging { z-index: 7; }

  /* note-boundary tactile flash — preserves hue-rotate */
  .orb-wrap.shift .orb-body {
    filter:
      hue-rotate(calc((var(--hue) - 200) * 1deg))
      saturate(calc(1.0 + var(--bright) * 0.55))
      brightness(calc(0.75 + var(--bright) * 0.6))
      contrast(1.15);
  }

  /* paused (global) — only applies if not muted; dim the baseline */
  .orb-wrap.paused:not(.muted) .orb-body {
    filter:
      hue-rotate(calc((var(--hue) - 200) * 1deg))
      saturate(calc(0.5 + var(--bright) * 0.3))
      brightness(calc(0.32 + var(--bright) * 0.28))
      contrast(1.0);
    cursor: not-allowed;
  }
  /* muted (per-orb) — desaturate to dim grey ember; skip hue-rotate
     since grayscale dominates anyway */
  .orb-wrap.muted .orb-body {
    filter:
      grayscale(0.85)
      brightness(0.32)
      saturate(0.3)
      contrast(0.95);
  }

  .orb-wrap:not(.dragging):not(.paused):not(.spawning):not(.muted) .orb-body {
    animation: breathe 5.2s ease-in-out infinite;
  }
  /* Each keyframe must re-declare the full filter chain including
     hue-rotate — `filter` is a single property in CSS. */
  @keyframes breathe {
    0%, 100% {
      filter:
        hue-rotate(calc((var(--hue) - 200) * 1deg))
        saturate(calc(0.85 + var(--bright) * 0.55))
        brightness(calc(0.55 + var(--bright) * 0.6))
        contrast(1.05);
    }
    50% {
      filter:
        hue-rotate(calc((var(--hue) - 200) * 1deg))
        saturate(calc(0.92 + var(--bright) * 0.6))
        brightness(calc(0.6 + var(--bright) * 0.65))
        contrast(1.1);
    }
  }
  .orb-wrap.spawning .orb-body {
    animation: spawn 600ms cubic-bezier(0.2, 1.4, 0.4, 1);
  }
  @keyframes spawn {
    0%   { transform: translate(-50%, -50%) scale(0); opacity: 0; }
    60%  { transform: translate(-50%, -50%) scale(1.15); opacity: 1; }
    100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  }
  .orb-wrap.removing .orb-body {
    animation: remove 350ms cubic-bezier(0.4, 0, 0.6, 1) forwards;
    pointer-events: none;
  }
  .orb-wrap.removing { pointer-events: none; }
  @keyframes remove {
    0%   { transform: translate(-50%, -50%) scale(1); opacity: 1; }
    100% { transform: translate(-50%, -50%) scale(0.4); opacity: 0; }
  }

  /* mute pulse — a subtle "off" indicator ring */
  .orb-wrap.muted .orb-body::after {
    background: radial-gradient(ellipse, rgba(255,255,255,0.15) 0%, transparent 75%);
  }

  /* the × delete button — inside orb-body, top-right */
  .orb-remove {
    position: absolute;
    top: 0;
    right: 0;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55);
    border: 1px solid rgba(255, 255, 255, 0.25);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    color: rgba(255, 255, 255, 0.9);
    font-size: 16px;
    font-weight: 300;
    line-height: 1;
    font-family: 'JetBrains Mono', monospace;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 8;
    opacity: 0;
    transform: translate(20%, -20%) scale(0.85);
    transition: opacity 200ms, background 200ms, transform 100ms, border-color 200ms;
    appearance: none;
    padding: 0;
  }
  .orb-wrap.satellite:hover .orb-remove { opacity: 0.85; }
  .orb-remove:hover {
    background: hsla(0, 80%, 50%, 0.75);
    border-color: hsla(0, 80%, 65%, 0.7);
    transform: translate(20%, -20%) scale(1);
    opacity: 1 !important;
  }
  /* Touch devices: hide the × delete button until the orb is tapped/held.
     Was 0.55 always-visible — the user wanted a clean default with the
     menu only appearing during interaction. .touching is added on
     touchstart and removed 800ms after release (see flowstate.js
     onUp handler). */
  @media (hover: none) {
    .orb-wrap.satellite .orb-remove { opacity: 0; pointer-events: none; transition: opacity 200ms ease; }
    .orb-wrap.satellite.touching .orb-remove,
    .orb-wrap.satellite.dragging .orb-remove {
      opacity: 0.85;
      pointer-events: auto;
    }
  }

  /* ── per-orb control rail (speed dots + brightness slider) ──────────── */
  .orb-controls {
    position: absolute;
    top: calc(var(--size) / 2 + 16px);
    left: 0;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 6px 12px;
    background: rgba(8, 8, 12, 0.7);
    border: 1px solid var(--glass-border);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border-radius: 999px;
    opacity: 0;
    transition: opacity 220ms, transform 220ms;
    z-index: 8;
    white-space: nowrap;
    pointer-events: auto;
  }
  .orb-wrap.controls-above .orb-controls {
    top: auto;
    bottom: calc(var(--size) / 2 + 16px);
  }
  .orb-wrap:hover .orb-controls,
  .orb-wrap.touching .orb-controls {
    opacity: 0.95;
  }
  /* Touch devices: hide ALL inline orb-controls. They get relocated
     into the global #orb-panel via JS when an orb is selected — see
     selectOrb() in flowstate.js. Using display:none (not opacity:0)
     so there's no transition flash on deselect when the element gets
     reparented back into the orb-wrap. */
  @media (hover: none) {
    .orb-controls {
      display: none !important;
    }
  }

  /* ── Detached mobile orb-panel ─────────────────────────────────────────
     Fixed-position panel above the readout. Empty by default; JS moves
     the selected orb's .orb-controls into it on tap. Populating it
     with `:not(:empty)` flips display from none to flex so the panel
     only renders when there's something to show. */
  /* Left-edge drawer — thin content-hugging strip that slides in from
     the left when an orb is selected. Vertically centered with auto
     height so the header (logo + zen + ?) stays visible above the
     drawer and the transport/readout stay visible below. Rounded only
     on the right side (left side hugs the screen edge). */
  #orb-panel {
    position: fixed;
    top: 50%;
    left: 0;
    width: 84px;
    z-index: 30;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    padding: 10px 0;
    background: rgba(14, 16, 22, 0.45);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-left: none;
    border-radius: 0 22px 22px 0;
    backdrop-filter: blur(26px) saturate(150%);
    -webkit-backdrop-filter: blur(26px) saturate(150%);
    box-shadow: 6px 0 28px rgba(0, 0, 0, 0.40);
    transform: translate(-100%, -50%);
    opacity: 0;
    pointer-events: none;
    transition: transform 380ms cubic-bezier(0.2, 0.8, 0.25, 1),
                opacity 260ms ease;
  }
  #orb-panel:not(:empty) {
    transform: translate(0, -50%);
    opacity: 1;
    pointer-events: auto;
    /* Cap the drawer at viewport height with internal scroll — protects
       small phones (iPhone SE ≈ 667px) from overflow when voices +
       speeds + arps + slider stack up tall. 24px keeps a hair of
       breathing room top + bottom. */
    max-height: calc(100vh - 24px);
    overflow-y: auto;
  }
  /* When the left-edge drawer is open, fade the header so the
     "flowstate" title + byline don't read as visually overlapped by
     the glass drawer. The drawer is only ever populated on mobile
     (see selectOrb's IS_MOBILE_VIEWPORT guard), so this :has() rule
     is effectively mobile-only without needing a media query. */
  body:has(#orb-panel:not(:empty)) header {
    opacity: 0.18;
    transition: opacity 220ms ease;
  }

  /* When .orb-controls is moved INTO the panel, neutralize its
     orb-relative absolute positioning AND force display:flex
     (overrides the mobile-default display:none above). */
  #orb-panel > .orb-controls {
    display: flex !important;
    position: static !important;
    top: auto !important;
    left: auto !important;
    transform: none !important;
    opacity: 1 !important;
    pointer-events: auto !important;
    padding: 0 !important;
    background: none !important;
    border: none !important;
    box-shadow: none !important;
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }

  /* Bigger speed-dots tap targets in the panel — 36×36 minimum for
     finger-friendly hits (Apple HIG: 44pt; we go a little smaller
     because the bullet is centered in the button). */
  #orb-panel .speed-dots {
    gap: 4px;
  }
  #orb-panel .speed-dots button {
    width: 36px;
    height: 36px;
  }
  #orb-panel .speed-dots button::before {
    inset: 11px;            /* keeps the dot visually centered */
  }
  /* Per-division dot sizing lives in the styled-pill block below
     (around line 1180) — keeping a single source of truth for these
     four overrides. */

  /* Vertical brightness slider — slim column to fit the thin drawer.
     `appearance: slider-vertical` is the Webkit/Safari path; modern
     Firefox + Chromium honor `writing-mode: vertical-lr` with
     `direction: rtl` to flip the value axis so up = louder. Sized for
     comfortable thumb-drag on phones. */
  #orb-panel input.brightness {
    width: 8px;
    height: 124px;
    writing-mode: vertical-lr;
    direction: rtl;
    -webkit-appearance: slider-vertical;
    appearance: slider-vertical;
    margin: 4px 0;
  }

  /* Glass-pill style for every toggle inside the drawer — matches the
     transport `.ctrl` aesthetic (semi-transparent fill, blurred backdrop,
     subtle border). Active state picks up the orb's --hue so the user
     gets visible feedback on which voice/speed is selected.

     When .orb-controls is reparented into #orb-panel we override its
     existing styles to stack as a single vertical column (voices,
     speeds, slider). Dividers are hidden — gaps handle separation. */
  #orb-panel > .orb-controls {
    flex-direction: column !important;
    align-items: center !important;
    flex-wrap: nowrap !important;
    gap: 14px !important;
  }
  #orb-panel > .orb-controls > .ctrl-divider { display: none !important; }

  /* Voice picker — six glass pills stacked vertically. */
  #orb-panel .voice-pick {
    flex-direction: column;
    gap: 6px;
  }
  #orb-panel .voice-pick button {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
    transition: background 180ms, border-color 180ms, color 180ms,
                transform 140ms cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  #orb-panel .voice-pick button:hover {
    background: rgba(236, 230, 213, 0.10);
    border-color: rgba(236, 230, 213, 0.25);
  }
  #orb-panel .voice-pick button:active { transform: scale(0.94); }
  #orb-panel .voice-pick button.active {
    background: hsla(var(--hue, 200), 70%, 50%, 0.18);
    border-color: hsla(var(--hue, 200), 80%, 65%, 0.55);
    color: hsl(var(--hue, 200), 85%, 82%);
    text-shadow: 0 0 10px hsla(var(--hue, 200), 90%, 70%, 0.55);
  }

  /* Speed dots — four glass pills stacked vertically (slowest at top,
     fastest at bottom, matching small→large dot growth). */
  #orb-panel .speed-dots {
    flex-direction: column;
    gap: 6px;
  }
  #orb-panel .speed-dots button {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    transition: background 180ms, border-color 180ms, transform 140ms,
                box-shadow 200ms;
  }
  #orb-panel .speed-dots button:hover {
    background: rgba(236, 230, 213, 0.10);
    border-color: rgba(236, 230, 213, 0.25);
  }
  #orb-panel .speed-dots button:active { transform: scale(0.94); }
  #orb-panel .speed-dots button.active {
    background: hsla(var(--hue, 200), 70%, 50%, 0.18);
    border-color: hsla(var(--hue, 200), 80%, 65%, 0.55);
    box-shadow: 0 0 14px hsla(var(--hue, 200), 90%, 60%, 0.40);
  }
  /* Inner dot tweaks — neutral white fill on glass, hue-tinted on active. */
  #orb-panel .speed-dots button::before {
    background: rgba(255, 255, 255, 0.55);
  }
  #orb-panel .speed-dots button.active::before {
    background: hsl(var(--hue, 200), 90%, 88%);
    box-shadow: 0 0 10px hsla(var(--hue, 200), 95%, 75%, 0.7);
  }
  #orb-panel .speed-dots button[data-div="4n"]::before  { inset: 13px; }
  #orb-panel .speed-dots button[data-div="8n"]::before  { inset: 12px; }
  #orb-panel .speed-dots button[data-div="16n"]::before { inset: 11px; }
  #orb-panel .speed-dots button[data-div="32n"]::before { inset: 10px; }

  /* Arp picker in the drawer — glass pills stacked vertically, same
     visual language as voice-pick / speed-dots above. */
  #orb-panel .arp-pick {
    flex-direction: column;
    gap: 4px;
  }
  #orb-panel .arp-pick button {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 17px;
    transition: background 180ms, border-color 180ms, color 180ms,
                transform 140ms cubic-bezier(0.2, 0.8, 0.2, 1);
  }
  #orb-panel .arp-pick button:hover {
    background: rgba(236, 230, 213, 0.10);
    border-color: rgba(236, 230, 213, 0.25);
  }
  #orb-panel .arp-pick button:active { transform: scale(0.94); }
  #orb-panel .arp-pick button.active {
    background: hsla(var(--hue, 200), 70%, 50%, 0.18);
    border-color: hsla(var(--hue, 200), 80%, 65%, 0.55);
    color: hsl(var(--hue, 200), 85%, 82%);
    text-shadow: 0 0 10px hsla(var(--hue, 200), 90%, 70%, 0.55);
  }

  /* Mobile: shrink the strip a touch on small phones so it leaves more
     canvas exposed. Below 380px (iPhone SE-class) we go even thinner. */
  @media (max-width: 760px) {
    #orb-panel {
      width: 78px;
      padding: 8px 0;
      gap: 10px;
    }
    #orb-panel .voice-pick button,
    #orb-panel .speed-dots button,
    #orb-panel .arp-pick button {
      width: 34px;
      height: 34px;
    }
    #orb-panel input.brightness {
      height: 100px;
    }
  }
  @media (max-width: 380px) {
    #orb-panel { width: 70px; }
    #orb-panel .voice-pick button,
    #orb-panel .speed-dots button,
    #orb-panel .arp-pick button {
      width: 32px;
      height: 32px;
    }
  }

  /* Hide panel entirely on desktop (hover model works fine there). */
  @media (hover: hover) and (pointer: fine) {
    #orb-panel { display: none !important; }
  }

  .speed-dots {
    display: flex;
    align-items: center;
    gap: 1px;
  }
  .speed-dots button {
    appearance: none;
    background: transparent;
    border: none;
    width: 18px;
    height: 18px;
    cursor: pointer;
    position: relative;
    padding: 0;
  }
  .speed-dots button::before {
    content: '';
    position: absolute;
    inset: 5px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.3);
    transition: background 150ms, inset 150ms;
  }
  .speed-dots button[data-div="4n"]::before  { inset: 7px; }
  .speed-dots button[data-div="8n"]::before  { inset: 6px; }
  .speed-dots button[data-div="16n"]::before { inset: 5px; }
  .speed-dots button[data-div="32n"]::before { inset: 4px; }
  .speed-dots button:hover::before { background: rgba(255, 255, 255, 0.6); }
  .speed-dots button.active::before {
    background: hsl(var(--hue), 80%, 72%);
    box-shadow: 0 0 8px hsla(var(--hue), 90%, 70%, 0.6);
  }

  .ctrl-divider {
    width: 1px;
    height: 14px;
    background: var(--glass-border);
  }

  /* Arp pattern picker — 4 glyphs (↕ ↑ ↓ ?) for up-down / up / down /
     random. Sits between speed-dots and the brightness slider in both
     the inline desktop rail and the mobile slide-out drawer. Uses the
     same tap-target sizing as voice-pick so the layouts feel uniform. */
  .arp-pick {
    display: flex;
    align-items: center;
    gap: 2px;
  }
  .arp-pick button {
    appearance: none;
    background: transparent;
    border: none;
    width: 20px;
    height: 18px;
    padding: 0;
    margin: 0;
    cursor: pointer;
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-weight: 400;
    font-size: 14px;
    line-height: 1;
    color: rgba(255, 255, 255, 0.45);
    transition: color 150ms, text-shadow 150ms;
  }
  .arp-pick button:hover {
    color: rgba(255, 255, 255, 0.85);
  }
  .arp-pick button.active {
    color: hsl(var(--hue, 200), 80%, 78%);
    text-shadow: 0 0 8px hsla(var(--hue, 200), 90%, 70%, 0.6);
  }

  /* ── Voice picker (5 tiny letters: p l b m s) ──────────────────────────
     Sits at the front of the .orb-controls rail. Single lowercase
     italic letters in serif so the typography matches the wordmark +
     start-screen + readout values. Active voice gets the orb's hue
     color (same treatment as active speed-dot). */
  .voice-pick {
    display: flex;
    align-items: center;
    gap: 2px;
  }
  .voice-pick button {
    appearance: none;
    background: transparent;
    border: none;
    width: 18px;
    height: 18px;
    padding: 0;
    margin: 0;
    cursor: pointer;
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-weight: 400;
    font-size: 13px;
    line-height: 1;
    color: rgba(255, 255, 255, 0.45);
    transition: color 150ms, text-shadow 150ms;
  }
  .voice-pick button:hover {
    color: rgba(255, 255, 255, 0.85);
  }
  .voice-pick button.active {
    color: hsl(var(--hue, 200), 80%, 78%);
    text-shadow: 0 0 8px hsla(var(--hue, 200), 90%, 70%, 0.6);
  }

  /* In the mobile detached panel, scale up to finger-friendly size. */
  /* (Older voice-pick sizing rules removed — superseded by the
     glass-pill block under the #orb-panel section above.) */

  /* ── Voice recorder modal (the 'v' button opens this) ─────────────── */
  .voice-modal-overlay {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    padding: 24px;
    animation: voice-modal-fade 180ms ease;
  }
  @keyframes voice-modal-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  .voice-modal {
    width: 100%;
    max-width: 360px;
    background: var(--mg-surface, #14171f);
    background: linear-gradient(180deg, rgba(28, 32, 42, 0.97), rgba(14, 17, 24, 0.97));
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 28px;
    padding: 28px 24px 22px;
    text-align: center;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
  }
  .voice-modal-title {
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-weight: 400;
    font-size: 26px;
    color: rgba(255, 255, 255, 0.95);
    margin-bottom: 6px;
  }
  .voice-modal-sub {
    font-size: 12px;
    line-height: 1.45;
    color: rgba(255, 255, 255, 0.55);
    margin-bottom: 22px;
  }
  .voice-modal-stage {
    display: flex;
    justify-content: center;
    margin: 8px 0 24px;
  }
  .voice-rec-btn {
    appearance: none;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 50%;
    width: 132px;
    height: 132px;
    position: relative;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    gap: 4px;
    transition: background 200ms, border-color 200ms;
  }
  .voice-rec-btn:hover { background: rgba(255, 255, 255, 0.06); }
  .voice-rec-dot {
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background: hsl(355, 78%, 60%);
    box-shadow: 0 0 16px hsla(355, 80%, 60%, 0.45);
    transition: transform 200ms, background 200ms;
  }
  .voice-rec-btn.recording .voice-rec-dot {
    transform: scale(0.55);
    border-radius: 6px;
    background: hsl(355, 90%, 65%);
    box-shadow: 0 0 28px hsla(355, 95%, 65%, 0.7);
    animation: voice-rec-pulse 1s ease-in-out infinite;
  }
  @keyframes voice-rec-pulse {
    0%, 100% { box-shadow: 0 0 28px hsla(355, 95%, 65%, 0.7); }
    50%      { box-shadow: 0 0 40px hsla(355, 95%, 65%, 0.95); }
  }
  .voice-rec-btn.has-clip .voice-rec-dot {
    background: hsl(140, 60%, 58%);
    box-shadow: 0 0 16px hsla(140, 70%, 58%, 0.55);
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 18px 0 18px 28px;
    border-color: transparent transparent transparent hsl(140, 60%, 58%);
    border-radius: 0;
    /* Override the round-circle defaults — this becomes a play triangle. */
  }
  .voice-rec-time {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    letter-spacing: 0.04em;
    color: rgba(255, 255, 255, 0.55);
    text-transform: uppercase;
  }
  .voice-rec-ring {
    position: absolute;
    inset: -4px;
    border-radius: 50%;
    border: 2px solid transparent;
    pointer-events: none;
  }
  .voice-rec-btn.recording .voice-rec-ring {
    border-color: hsl(355, 90%, 65%);
    animation: voice-rec-ring-spin 2s linear forwards;
  }
  @keyframes voice-rec-ring-spin {
    from { transform: rotate(0deg); border-top-color: hsl(355, 90%, 65%); border-right-color: transparent; border-bottom-color: transparent; border-left-color: transparent; }
    to   { transform: rotate(360deg); border-color: hsl(355, 90%, 65%); }
  }

  .voice-modal-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    flex-wrap: wrap;
  }
  .voice-modal-actions button {
    appearance: none;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 999px;
    padding: 9px 18px;
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-size: 14px;
    color: rgba(255, 255, 255, 0.85);
    cursor: pointer;
    transition: background 150ms, border-color 150ms;
  }
  .voice-modal-actions button:hover {
    background: rgba(255, 255, 255, 0.12);
    border-color: rgba(255, 255, 255, 0.18);
  }
  .voice-modal-save {
    background: hsla(40, 70%, 60%, 0.18) !important;
    border-color: hsla(40, 70%, 60%, 0.35) !important;
    color: hsl(40, 75%, 75%) !important;
  }
  .voice-modal-save:hover {
    background: hsla(40, 70%, 60%, 0.28) !important;
  }

  /* Empty 'v' state — rendered as a hairline outline circle until a
     sample exists. Distinguishes it from the other 5 letter buttons
     which are all "ready to play." Long-press tip is in the title
     attribute and aria-label. */
  .voice-pick button[data-voice="voice"]:not(.has-sample)::after {
    content: '';
    position: absolute;
    inset: 25%;
    border: 1px dashed rgba(255, 255, 255, 0.35);
    border-radius: 50%;
    pointer-events: none;
  }
  .voice-pick button[data-voice="voice"] {
    position: relative;
  }

  input.brightness {
    appearance: none;
    -webkit-appearance: none;
    width: 70px;
    height: 4px;
    background: linear-gradient(90deg,
      rgba(255,255,255,0.1) 0%,
      hsla(var(--hue), 70%, 60%, 0.4) 100%);
    border-radius: 999px;
    outline: none;
    cursor: pointer;
    margin: 0;
  }
  input.brightness::-webkit-slider-thumb {
    appearance: none;
    -webkit-appearance: none;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: hsl(var(--hue), 80%, 78%);
    box-shadow: 0 0 8px hsla(var(--hue), 90%, 70%, 0.6);
    cursor: pointer;
    border: none;
  }
  input.brightness::-moz-range-thumb {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: hsl(var(--hue), 80%, 78%);
    box-shadow: 0 0 8px hsla(var(--hue), 90%, 70%, 0.6);
    cursor: pointer;
    border: none;
  }

  /* ── start gate ──────────────────────────────────────────────────────── */
  #start {
    position: fixed;
    inset: 0;
    z-index: 1000;
    background: rgba(7, 7, 10, 0.86);
    backdrop-filter: blur(28px);
    -webkit-backdrop-filter: blur(28px);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 56px;
    cursor: pointer;
    transition: opacity 1.4s ease, visibility 1.4s;
  }
  #start.hidden {
    opacity: 0;
    pointer-events: none;
    visibility: hidden;
  }
  .start-orb {
    width: 110px;
    height: 110px;
    border-radius: 50%;
    /* Same orb image as the live app — gives the splash a teaser of
       what's inside. Hue-rotated warm so the splash reads "sunset/amber"
       rather than the cool blue base. */
    background-image: url("lib/orb-master.webp");
    background-size: 108% 108%;
    background-position: center;
    background-repeat: no-repeat;
    filter: hue-rotate(180deg) saturate(1.1) brightness(1.1) contrast(1.05);
    box-shadow:
      0 0 70px hsla(30, 85%, 60%, 0.45),
      0 0 180px hsla(310, 70%, 55%, 0.2);
    animation: idleFloat 5.6s ease-in-out infinite;
    position: relative;
  }
  .start-orb::after { display: none; }
  @keyframes idleFloat {
    0%, 100% { transform: translateY(0) scale(1); }
    50%      { transform: translateY(-10px) scale(1.05); }
  }
  .start-text-wrap { text-align: center; }
  .start-title {
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-weight: 300;
    font-size: 64px;
    letter-spacing: -0.04em;
    line-height: 0.95;
    font-variation-settings: "opsz" 144, "SOFT" 100;
    margin-bottom: 14px;
  }
  .start-sub {
    font-size: 10px;
    letter-spacing: 0.4em;
    text-transform: uppercase;
    color: var(--ink-dim);
  }
  .start-cta {
    margin-top: 8px;
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-weight: 300;
    font-size: 16px;
    color: var(--ink-dim);
    letter-spacing: 0.04em;
  }
  .start-cta::before { content: '— '; font-family: 'JetBrains Mono', monospace; font-style: normal; }
  .start-cta::after  { content: ' —'; font-family: 'JetBrains Mono', monospace; font-style: normal; }

  /* ── responsive ──────────────────────────────────────────────────────── */
  @media (max-width: 760px) {
    h1 { font-size: 26px; }
    header { top: 14px; left: 16px; }
    .byline { font-size: 9px; letter-spacing: 0.18em; }
    .legend { top: 16px; right: 16px; }
    .legend .row { font-size: 8px; }

    /* Vertical stack from the bottom up — no overlap, clear hierarchy.
       Bands: transport b24 → scale-toggle b78 → readout b126.

       Readout shows ALL 5 cells on mobile (Key, Octave, Filter, Mode,
       Color hex). Two-row grid keeps each cell readable without
       shrinking text below readability. Color cell shows the orb's
       current hex code that matches its visible color. */
    #readout {
      left: 50%;
      transform: translateX(-50%);
      bottom: 122px;
      display: grid;
      grid-template-columns: repeat(3, auto);
      grid-auto-rows: auto;
      gap: 6px 18px;
      flex-wrap: unset;
      max-width: calc(100vw - 28px);
      justify-content: center;
      justify-items: center;
      text-align: center;
    }
    /* All 5 cells visible — none hidden. */
    #readout > div { display: flex; flex-direction: column; align-items: center; }
    #readout .label { font-size: 8px; opacity: 0.7; }
    #readout .value { font-size: 16px; }
    #readout .value .unit { font-size: 9px; margin-left: 2px; opacity: 0.7; }
    /* Hex value — small monospace so it doesn't crowd the row. */
    #rd-hex {
      font-family: 'JetBrains Mono', monospace;
      font-style: normal;
      font-size: 12px;
      letter-spacing: 0.04em;
    }

    /* Key picker (mobile) — tap the KEY readout cell to cycle 12 keys. */
    #readout > div.key-tappable {
      position: relative;
      transition: opacity 150ms;
    }
    #readout > div.key-tappable:active { opacity: 0.7; }
    #readout > div.key-tappable .label::after {
      content: ' ▾';
      opacity: 0.4;
      font-size: 8px;
      margin-left: 1px;
    }
    #rd-key.key-bump { animation: key-bump 220ms ease; }
    #rd-key { display: inline-block; transform-origin: center; }
    @keyframes key-bump {
      0%   { transform: scale(1.0);  color: hsl(40, 75%, 75%); }
      50%  { transform: scale(1.18); color: hsl(40, 90%, 80%); }
      100% { transform: scale(1.0);  color: inherit; }
    }

    /* "Play C ref" button on the voice recorder — secondary style. */
    .voice-modal-tone {
      background: rgba(110, 197, 255, 0.10) !important;
      border-color: rgba(110, 197, 255, 0.28) !important;
      color: hsl(205, 80%, 78%) !important;
    }
    .voice-modal-tone:hover { background: rgba(110, 197, 255, 0.18) !important; }

    .scale-toggle {
      bottom: 78px;
      padding: 3px;
    }
    .pill { padding: 5px 11px; font-size: 11px; }

    .transport {
      bottom: 22px; left: 50%; right: auto;
      transform: translateX(-50%);
      gap: 14px;
    }
    .ctrl { width: 46px; height: 46px; }

    .orb-wrap.main { --size: 110px; }
    .orb-wrap.satellite { --size: 70px; }
    input.brightness { width: 56px; }
    .start-title { font-size: 40px; }
    .horizon.top { top: 22%; }
    .horizon.bot { bottom: 22%; }
  }

  /* Extra-narrow phones (iPhone SE, sub-380px) — pull legend off-screen
     pieces tighter and shrink orb a touch more. */
  @media (max-width: 400px) {
    .legend { font-size: 9px; }
    .legend .row span { display: block; }
    #readout { gap: 16px; bottom: 122px; }
    #readout .value { font-size: 16px; }
    .orb-wrap.main { --size: 96px; }
  }

  /* ── 2026-04-30: clean-bg override ───────────────────────────────────────
     Hide every "atmospheric" decorative layer so the canvas reads as
     pure dark space with just the orbs. The Playwright audit caught
     bokeh + conic-rays bleeding color through `mix-blend-mode: screen`
     even after .aurora was hidden — extending the hide list catches
     all 5 colored backdrops. JS still toggles classes on #aurora
     (e.g. .dim) and `.bokeh` (which gets `.bokeh-dot` children injected
     dynamically); both queries succeed against display:none elements
     without throwing. Revert by deleting this block. */
  .aurora,
  .aurora-blob,
  .bokeh,
  .bokeh-dot,
  .conic-rays,
  .conic-rays-2,
  .horizon,
  #grain {
    display: none !important;
  }
  body, .bg, .horizon {
    background: #000 !important;
  }

  /* ── Mobile perf override (2026-04-30) ─────────────────────────────────
     backdrop-filter: blur() is the single most expensive paint primitive
     on mobile GPUs — it forces re-blurring the entire region behind the
     element on every frame. Replace with opaque/translucent fills on
     mobile so the layout looks nearly identical without the per-frame
     blur cost. Desktop keeps the glassy backdrop-filter look. */
  @media (max-width: 760px) {
    #start {
      backdrop-filter: none !important;
      -webkit-backdrop-filter: none !important;
      background: rgba(7, 7, 10, 0.94) !important;
    }
    .scale-toggle {
      backdrop-filter: none !important;
      -webkit-backdrop-filter: none !important;
      background: rgba(20, 22, 28, 0.85) !important;
    }
    .transport,
    .ctrl {
      backdrop-filter: none !important;
      -webkit-backdrop-filter: none !important;
    }
    .ctrl {
      background: rgba(20, 22, 28, 0.85) !important;
    }
    /* Wordmark gradient still cycles, but drop the drop-shadow filter on
       mobile — shadow on animated gradient text means the GPU re-applies
       the shadow on every position-shift frame. ~60fps savings. */
    .start-title {
      filter: none !important;
    }
  }

  /* ── Help modal ────────────────────────────────────────────────────────
     Full-screen overlay with a centered scrollable card. Matches the
     voice-recorder modal pattern so the visual language is consistent
     across every modal in the app. */
  .help-modal-overlay {
    position: fixed;
    inset: 0;
    z-index: 2100;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    background: rgba(0, 0, 0, 0.7);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    padding: 24px;
    overflow-y: auto;
    animation: voice-modal-fade 200ms ease;
  }
  .help-modal-overlay[hidden] { display: none; }

  .help-modal-card {
    width: 100%;
    max-width: 540px;
    background: linear-gradient(180deg, rgba(28, 32, 42, 0.97), rgba(14, 17, 24, 0.97));
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 28px;
    padding: 36px 32px 28px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
    margin: auto;
    position: relative;
    color: rgba(255, 255, 255, 0.9);
  }

  .help-modal-close {
    appearance: none;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.12);
    color: rgba(255, 255, 255, 0.7);
    width: 32px;
    height: 32px;
    border-radius: 50%;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    position: absolute;
    top: 16px;
    right: 16px;
    transition: all 150ms;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
  }
  .help-modal-close:hover {
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
  }

  .help-modal-title {
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-weight: 300;
    font-size: 32px;
    line-height: 1;
    background: linear-gradient(90deg, #c9a962, #ff8aa6, #b074ff, #6ec5ff, #66e6c4, #ffd27b, #c9a962);
    background-size: 300% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    animation: start-title-shift 18s linear infinite;
    margin-bottom: 4px;
  }
  .help-modal-sub {
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-size: 13px;
    color: rgba(255, 255, 255, 0.55);
    margin-bottom: 24px;
  }

  .help-section {
    margin-bottom: 22px;
  }
  .help-section h3 {
    font-family: 'JetBrains Mono', monospace;
    font-size: 10px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: hsl(40, 75%, 75%);
    margin: 0 0 10px;
    font-weight: 500;
  }
  .help-section ul {
    list-style: none;
    padding: 0;
    margin: 0 0 8px;
    font-size: 13px;
    line-height: 1.6;
    color: rgba(255, 255, 255, 0.78);
  }
  .help-section li {
    padding: 3px 0;
    position: relative;
    padding-left: 14px;
  }
  .help-section li::before {
    content: '·';
    color: rgba(255, 255, 255, 0.35);
    position: absolute;
    left: 4px;
    top: 0;
    font-size: 16px;
    line-height: 1.3;
  }
  .help-section b {
    color: rgba(255, 255, 255, 0.95);
    font-weight: 500;
  }
  .help-row-label {
    margin: 8px 0 4px;
    font-size: 11px;
    color: rgba(255, 255, 255, 0.6);
    text-transform: lowercase;
    letter-spacing: 0.04em;
  }
  .help-voices li {
    padding-left: 36px;
  }
  .help-voices li::before { content: ''; }
  .help-voices .vk {
    position: absolute;
    left: 6px;
    top: 3px;
    font-family: 'Fraunces', serif;
    font-style: italic;
    color: hsl(200, 70%, 75%);
    font-size: 15px;
    width: 22px;
    text-align: center;
  }
  .help-section kbd {
    display: inline-block;
    padding: 1px 7px;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 4px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    color: rgba(255, 255, 255, 0.85);
    margin: 0 1px;
  }

  .help-modal-footer {
    margin-top: 8px;
    padding-top: 16px;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    font-family: 'Fraunces', serif;
    font-style: italic;
    font-size: 11px;
    color: rgba(255, 255, 255, 0.4);
    text-align: center;
    letter-spacing: 0.04em;
  }

  @media (max-width: 760px) {
    .help-modal-card {
      padding: 30px 22px 22px;
      border-radius: 24px;
    }
    .help-modal-title { font-size: 26px; }
    .help-section { margin-bottom: 18px; }
    .help-section ul { font-size: 12.5px; }
  }
