  :root {
    color-scheme: dark;
    --bg:#0a0612; --panel:rgba(24,13,44,.62); --panel-line:#33215c; --inset:#0c0717;
    --magenta:#ff2d9b; --cyan:#22e0ff; --violet:#7b2dff; --amber:#ffb020;
    --text:#f3eaff; --muted:#9a86c4; --faint:#6a5b90;
    --glow-mag:0 0 18px rgba(255,45,155,.5);
  }
  * { box-sizing: border-box; }

  body {
    font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    color: var(--text); margin: 0; min-height:100vh;
    background:
      radial-gradient(120% 70% at 10% -10%, rgba(123,45,255,.20), transparent 55%),
      radial-gradient(120% 70% at 100% 0%, rgba(255,45,155,.14), transparent 50%),
      radial-gradient(140% 120% at 50% 120%, rgba(34,224,255,.10), transparent 55%),
      var(--bg);
  }
  .card {
    width: 100%; max-width: 560px; position:relative;
    background: var(--panel); border: 1px solid var(--panel-line);
    border-radius: 18px; padding: 24px;
    -webkit-backdrop-filter:blur(16px); backdrop-filter:blur(16px);
    box-shadow: 0 24px 70px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.05);
  }
  h1 {
    font-family:"Chakra Petch",sans-serif; font-weight:800; font-size: 42px; letter-spacing:.14em; margin: 0 0 16px; text-align:center;
    background:linear-gradient(95deg,var(--magenta),var(--violet),var(--cyan),var(--violet),var(--magenta));
    background-size:200% auto;   /* room for the JS-driven colour scroll (tickWordmark) */
    -webkit-background-clip:text; background-clip:text; color:transparent;
    filter:drop-shadow(0 0 18px rgba(255,45,155,.3));
    pointer-events:none;   /* decorative wordmark — its filter layer must not steal clicks from the plug above it */
  }
  .sub { color: var(--muted); font-size: 13px; margin: 0 0 18px; }
  /* brand lockup: the Pulse mark beside the wordmark */
  .brand { display:flex; align-items:center; justify-content:center; gap:14px; margin-bottom:14px; }
  .brand h1 { margin:0; }
  .brand-mark { flex:0 0 auto; width:46px; height:46px; filter:drop-shadow(0 0 9px rgba(255,45,155,.45)); }
  label { display: block; font-family:"JetBrains Mono",monospace; font-size: 11px; color: var(--muted); margin: 16px 0 6px; text-transform: uppercase; letter-spacing: .14em; }
  input[type=text] {
    width: 100%; padding: 10px 12px; border-radius: 9px; border: 1px solid var(--panel-line);
    background: var(--inset); color: var(--text); font-size: 14px; font-family:"JetBrains Mono",monospace;
  }
  input[type=text]:focus { outline:none; border-color:var(--violet); }
  .row { display: flex; gap: 10px; margin-top: 14px; }
  button {
    flex: 1; padding: 12px; border-radius: 11px; border: 1px solid var(--panel-line); cursor: pointer;
    font-family:Inter,sans-serif; font-size: 14px; font-weight: 600; background: var(--inset); color: var(--text); transition: .15s;
  }
  button:hover:not(:disabled) { border-color: var(--violet); }
  button.primary { color:#fff; background: linear-gradient(95deg,var(--magenta),var(--violet)); border:none; box-shadow:var(--glow-mag); }
  button.primary:hover:not(:disabled) { filter:brightness(1.1); }
  button.danger { background: rgba(255,45,120,.16); border:1px solid rgba(255,45,120,.5); color:#ff6f9c; }
  button.danger:hover:not(:disabled) { background: rgba(255,45,120,.26); }
  button:disabled { opacity: .4; cursor: not-allowed; }
  /* segmented mode toggle */
  #modeRow { display:flex; gap:6px; padding:4px; background:var(--inset); border:1px solid var(--panel-line); border-radius:12px; margin-top:4px; }
  button.mode { background:transparent; border:none; color:var(--muted); border-radius:9px; box-shadow:none; }
  button.mode:hover:not(:disabled) { border:none; color:var(--text); }
  button.mode.active { color:#fff; background:linear-gradient(95deg,var(--magenta),var(--violet)); box-shadow:var(--glow-mag); }
  /* four generator modes on one row — tighter than the default button so they fit the drawer */
  #modeRow button { padding:12px 4px; font-size:13px; }
  /* act sub-picker — a choice inside the Acts mode. Deliberately NOT the segmented-box
     treatment of the mode row above: small unboxed pills so the hierarchy reads. */
  #actRow { display:flex; flex-wrap:wrap; justify-content:center; gap:6px; margin-top:10px; }
  /* concrete radius (≈ half height), not 999px: the degenerate radius + a glow shadow
     left a bright crescent past the right edge on scaled/GPU rasterizers */
  #actRow button { flex:0 1 auto; padding:7px 14px; font-size:12px; border-radius:16px;
    border:1px solid var(--panel-line); color:var(--muted); }
  #actRow button:hover:not(:disabled) { border-color:var(--violet); color:var(--text); }
  /* active: flat solid, no glow (explicit none — .mode.active glows otherwise). Solid on
     purpose: a gradient's rightmost pixel column can wrap to the start color on
     fractionally-scaled GPU raster (the bright crescent), and it reads quieter anyway. */
  #actRow button.active { color:#fff; border-color:transparent; box-shadow:none;
    background:var(--violet); }
  .hint { font-size: 12px; color: var(--muted); margin: 10px 0 0; }
  /* themed file picker — the native input is hidden; this label opens it */
  .file-input { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); border:0; }
  .file-btn {
    display:flex; align-items:center; justify-content:center; gap:9px;
    margin-top:8px; padding:16px 14px; cursor:pointer;
    border:1px dashed var(--panel-line); border-radius:11px; background:var(--inset);
    color:var(--cyan); font:600 13px Inter,sans-serif; letter-spacing:.02em; transition:.15s;
  }
  .file-btn svg { width:17px; height:17px; flex:none; }
  .file-btn:hover { border-color:var(--cyan); color:#fff; background:rgba(34,224,255,.06); box-shadow:0 0 18px rgba(34,224,255,.22); }
  .file-input:focus-visible + .file-btn { border-color:var(--cyan); box-shadow:0 0 18px rgba(34,224,255,.22); }
  .file-btn.has-file { border-style:solid; border-color:var(--violet); color:var(--text); background:linear-gradient(95deg,rgba(255,45,155,.12),rgba(123,45,255,.12)); }
  .slider-val { float: right; font-family:"Chakra Petch",sans-serif; color: var(--cyan); font-weight: 700; }
  input[type=range] {
    -webkit-appearance:none; appearance:none; width: 100%; height:8px; border-radius:5px; outline:none; cursor:pointer;
    background:linear-gradient(90deg, var(--magenta), var(--violet) var(--p,50%), #20132f var(--p,50%));
    border:1px solid var(--panel-line);
  }
  input[type=range]::-webkit-slider-thumb { -webkit-appearance:none; width:20px; height:20px; border-radius:50%; border:none;
    background:radial-gradient(circle at 35% 30%, #fff, #ff7ac4 40%, var(--magenta)); box-shadow:0 0 12px rgba(255,45,155,.8); }
  input[type=range]::-moz-range-thumb { width:20px; height:20px; border-radius:50%; border:none;
    background:radial-gradient(circle at 35% 30%, #fff, #ff7ac4 40%, var(--magenta)); box-shadow:0 0 12px rgba(255,45,155,.8); }

  .viz h3 { font-family:"JetBrains Mono",monospace; font-size: 11px; color:var(--cyan); text-transform:uppercase; letter-spacing:.2em; margin: 20px 0 8px; font-weight:600; }
  /* heading row that carries a trailing action (e.g. the heatmap's download button) */
  .viz-head { display:flex; align-items:center; gap:10px; }
  .viz-head .icon-btn { margin-left:auto; }
  .icon-btn { flex:0 0 auto; width:30px; height:30px; padding:0; border-radius:8px; cursor:pointer;
    display:flex; align-items:center; justify-content:center;
    background:var(--inset); border:1px solid var(--panel-line); color:var(--muted); transition:.15s; }
  .icon-btn svg { width:16px; height:16px; pointer-events:none; }
  .icon-btn:hover:not(:disabled) { color:var(--cyan); border-color:var(--cyan); box-shadow:0 0 12px rgba(34,224,255,.2); }
  .icon-btn:disabled { opacity:.4; cursor:not-allowed; }
  canvas { display:block; }
  #curve { width:100%; height: 84px; cursor: pointer; touch-action: none;
    background:var(--inset); border:1px solid var(--panel-line); border-radius:12px; }

  .transport { display:flex; align-items:center; justify-content:center; gap:12px; margin-top:16px; }
  .time { color:var(--muted); font-family:"JetBrains Mono",monospace; font-size:12px; font-variant-numeric: tabular-nums; }

  /* video-style transport: the heatmap is the scrub bar; compact controls sit beneath it */
  #curve.scrub { display:block; width:100%; }
  .transport-bar { display:flex; align-items:center; justify-content:center; gap:10px; margin-top:8px; }
  .transport-btn { flex:0 0 auto; width:36px; height:36px; padding:0; border-radius:50%; cursor:pointer;
    display:flex; align-items:center; justify-content:center; font-size:13px; line-height:1; color:#fff;
    background:linear-gradient(95deg,var(--magenta),var(--violet)); border:none; box-shadow:var(--glow-mag); }
  .transport-btn:hover:not(:disabled) { filter:brightness(1.1); }
  .transport-btn:disabled { opacity:.4; cursor:not-allowed; box-shadow:none; }
  /* prev/next-round jumps — quieter than play, sized a touch smaller */
  .transport-btn.round-btn { width:30px; height:30px; font-size:15px; color:var(--text);
    background:var(--inset); border:1px solid var(--panel-line); box-shadow:none; }
  .transport-btn.round-btn:hover:not(:disabled) { filter:none; border-color:var(--magenta); color:#fff; }

  /* session lifecycle button — Start / Stop / Deny (one morphing control) */
  .session-btn { width:100%; margin-top:16px; padding:14px; border-radius:11px; cursor:pointer; border:none;
    font-family:"Chakra Petch",sans-serif; font-weight:800; font-size:15px; letter-spacing:.04em; color:#fff; transition:.15s; }
  .session-btn:disabled { opacity:.4; cursor:not-allowed; box-shadow:none; }
  .session-btn.start { background:linear-gradient(95deg,var(--magenta),var(--violet)); box-shadow:var(--glow-mag); }
  .session-btn.start:hover { filter:brightness(1.1); }
  /* when a script is loaded and the session hasn't started, pulse to invite the first play */
  .session-btn.start.ready { animation:startReady 1.5s ease-in-out infinite; }
  .session-btn.start.ready:hover { animation:none; }
  @keyframes startReady {
    0%,100% { box-shadow:var(--glow-mag); transform:scale(1); }
    50%     { box-shadow:0 0 30px rgba(255,45,155,.75), 0 0 10px rgba(123,45,255,.5); transform:scale(1.012); }
  }
  @media (prefers-reduced-motion: reduce) { .session-btn.start.ready { animation:none; } }
  .session-btn.stop { background:rgba(255,45,120,.16); border:1px solid rgba(255,45,120,.5); color:#ff6f9c; }
  .session-btn.stop:hover { background:rgba(255,45,120,.26); }
  .session-btn.deny { background:linear-gradient(95deg,var(--magenta),var(--violet)); color:#fff;
    box-shadow:0 0 26px rgba(255,45,155,.55); animation:denyPulse 1.1s ease-in-out infinite; }

  /* abilities — edge / ruin side by side under a header */
  .abilities { margin-top:16px; }
  .abilities .gpanel-h { margin-bottom:8px; }
  /* the edge/ruin labels cycle through very different lengths ("Edge" · "Resume — 12s held")
     — never let that wrap to a second line or wiggle the ticking digits */
  .ability-row > button { white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    font-variant-numeric:tabular-nums; }
  .ability-row { display:flex; gap:10px; }
  .ability-row > button { flex:1 1 0; margin-top:0; }

  .preview-screen { position:relative; border-radius:12px; overflow:hidden;
    background:var(--inset); border:1px solid var(--panel-line); }
  #scroll { width:100%; height:84px; display:block; }   /* match the heatmap height */
  .fx { position:absolute; inset:0; pointer-events:none; overflow:hidden; }

  .divider { border:none; border-top:1px solid var(--panel-line); margin:22px 0 12px; }

  /* connection status dot on the menu button — green when a device is connected */
  .conn-dot { position:absolute; top:-3px; right:-3px; width:10px; height:10px; border-radius:50%;
    background:#5a4a78; border:2px solid #0a0612; pointer-events:none; }
  .conn-dot.on { background:#3ad07a; box-shadow:0 0 8px rgba(58,208,122,.8); }
  /* overclock opt-in */
  .oc-row { display:flex; gap:9px; align-items:flex-start; margin-top:10px; cursor:pointer;
    font-family:Inter,sans-serif; text-transform:none; letter-spacing:normal; color:var(--muted); }
  .oc-row[hidden] { display:none; }
  .oc-row input[type=checkbox] { flex:0 0 auto; width:16px; height:16px; margin-top:1px; accent-color:var(--magenta); cursor:pointer; }
  .oc-text { font-size:11.5px; line-height:1.45; }
  .oc-text b { color:var(--text); }
  .oc-warn { color:var(--amber); }

  .conn-status { font-size:11px; color:var(--muted); margin-top:9px; text-align:center; }
  .conn-status b { color:#3ad07a; }
  .pop-note { margin:9px 0 0; font-size:10.5px; color:var(--faint); line-height:1.4; }
  /* subtle text action inside the popover (e.g. "Forget this device") */
  .pop-link { display:block; margin:8px auto 0; background:none; border:none; padding:2px 4px; cursor:pointer;
    color:var(--muted); font-size:11px; text-decoration:underline; }
  .pop-link:hover { color:#ff6f9c; background:none; border:none; }
  /* dual-handle stroke range slider (two stacked range inputs; only the thumbs catch pointers) */
  .range-dual { position:relative; height:26px; display:flex; align-items:center; margin-top:8px; }
  .range-dual::before { content:""; position:absolute; left:0; right:0; height:5px; top:50%; transform:translateY(-50%);
    background:var(--inset); border:1px solid var(--panel-line); border-radius:3px; }
  .range-dual .range-fill { position:absolute; height:5px; top:50%; transform:translateY(-50%); pointer-events:none;
    background:linear-gradient(90deg,var(--magenta),var(--cyan)); border-radius:3px; box-shadow:0 0 10px rgba(255,45,155,.5); }
  .range-dual input[type=range] { position:absolute; left:0; width:100%; height:5px; margin:0; padding:0; border:none;
    -webkit-appearance:none; appearance:none; background:transparent; pointer-events:none; }
  .range-dual input[type=range]::-webkit-slider-thumb { -webkit-appearance:none; appearance:none; width:16px; height:16px;
    border-radius:50%; border:none; cursor:pointer; pointer-events:all; position:relative; z-index:2;
    background:radial-gradient(circle at 35% 30%,#fff,#ff7ac4 40%,var(--magenta)); box-shadow:0 0 10px rgba(255,45,155,.8); }
  .range-dual input[type=range]::-moz-range-thumb { width:16px; height:16px; border-radius:50%; border:none; cursor:pointer;
    pointer-events:all; background:radial-gradient(circle at 35% 30%,#fff,#ff7ac4 40%,var(--magenta)); box-shadow:0 0 10px rgba(255,45,155,.8); }
  .range-dual input[type=range]::-moz-range-track { background:transparent; border:none; }

  /* debug strip (?debug=1) */
  .xpwrap { margin: 16px 0 0; }
  .xprow { display:flex; align-items:center; gap:10px; }
  .xpreset {
    flex:0 0 auto; background:none; border:none; color:var(--faint); font-size:11px; cursor:pointer;
    padding:2px 4px; text-decoration:underline; font-weight:400;
  }
  .xpreset:hover { color:var(--muted); background:none; border:none; }
  /* debug FPS meter — fixed above every overlay (they top out at z-index 80) */
  #fpsMeter { position:fixed; top:8px; right:8px; z-index:100; pointer-events:none;
    font-family:"JetBrains Mono",monospace; font-size:11px; font-variant-numeric:tabular-nums;
    color:var(--cyan); background:rgba(8,7,16,.65); padding:3px 9px; border-radius:8px; }
  .toast {
    position:fixed; left:50%; top:24px; transform:translateX(-50%) translateY(-12px);
    background:linear-gradient(95deg,var(--magenta),var(--violet)); color:#fff; font-weight:700; padding:10px 18px; border-radius:10px;
    box-shadow:0 8px 30px rgba(0,0,0,.4); opacity:0; pointer-events:none; transition:.3s; z-index:90;   /* above the hold overlays (60) — device errors must show mid-hold */
  }
  .toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
  /* error variant — distinct red so failures don't read as a celebratory toast */
  .toast-err { background:linear-gradient(95deg,#ff3b6b,#b3203f); box-shadow:0 8px 30px rgba(0,0,0,.4), 0 0 22px rgba(255,59,107,.35); max-width:min(92vw,420px); text-align:center; }

  /* ---- HUD: session play-time tile ---- */
  .hud { margin:16px 0 0; }
  .sess-hud { display:flex; gap:8px; margin-bottom:10px; }
  .sess-tile { flex:1 1 0; min-width:0; text-align:center; padding:10px 8px; border-radius:11px;
    background:var(--inset); border:1px solid var(--panel-line); }
  .sess-k { font-family:"JetBrains Mono",monospace; font-size:9.5px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
  .sess-v { font-family:"Chakra Petch",sans-serif; font-weight:800; font-size:22px; color:var(--text); margin-top:3px;
    line-height:1.05; font-variant-numeric:tabular-nums; }

  /* ---- panel headings (abilities) ---- */
  .gpanel-h { font-family:"JetBrains Mono",monospace; font-size:11px; letter-spacing:.18em; text-transform:uppercase; color:var(--cyan); margin-bottom:10px; }

  /* ===== App shell: everything non-gameplay lives in a left drawer behind a hamburger.
         The drawer is always off-canvas (all screen sizes); opening it never pauses play. ===== */
  .sidebar {
    position:fixed; left:0; top:0; bottom:0; width:min(88vw,360px); z-index:40; overflow-y:auto;
    padding:18px 18px 24px;
    /* nearly-opaque bg + a lighter blur: while open it sits over live embeds, and an 18px
       backdrop re-blur of a full-height panel every frame was the cost of 6% translucency */
    background:rgba(20,11,36,.96); border-right:1px solid var(--panel-line);
    -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px);
    transform:translateX(-100%); transition:transform .26s cubic-bezier(.3,.8,.3,1);
    box-shadow:10px 0 60px rgba(0,0,0,.55);
  }
  body.sb-open .sidebar { transform:translateX(0); }
  .sb-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:4px; }
  .sb-title { font-family:"Chakra Petch",sans-serif; font-weight:700; font-size:18px; letter-spacing:.04em;
    background:linear-gradient(95deg,var(--magenta),var(--violet) 50%,var(--cyan)); -webkit-background-clip:text; background-clip:text; color:transparent; }
  .sb-close { flex:0 0 auto; width:34px; height:34px; padding:0; border-radius:9px; font-size:15px;
    background:var(--inset); border:1px solid var(--panel-line); color:var(--muted); cursor:pointer; }
  .sb-close:hover { color:var(--text); border-color:var(--violet); }

  /* accordion sections — collapsed by default */
  .sb-section { border-bottom:1px solid rgba(51,33,92,.5); }
  .sb-sec-head { display:flex; align-items:center; gap:8px; width:100%; padding:15px 2px; cursor:pointer;
    background:none; border:none; text-align:left; }
  .sb-sec-title { font-family:"JetBrains Mono",monospace; font-size:11px; letter-spacing:.18em; text-transform:uppercase; color:var(--cyan); }
  .sb-count { color:var(--muted); font-family:"JetBrains Mono",monospace; font-size:11px; }
  .sb-chevron { margin-left:auto; color:var(--muted); font-size:12px; transition:transform .18s; }
  .sb-section.open .sb-chevron { transform:rotate(90deg); color:var(--cyan); }
  .sb-sec-head:hover .sb-sec-title { color:#fff; }
  .sb-sec-body { display:none; padding:0 2px 18px; }
  .sb-section.open .sb-sec-body { display:block; }

  /* community & support row — pills stay `hidden` in the HTML until their real URL exists */
  /* stacked, centered rows: pills / pride flag */
  .sb-community { display:flex; flex-direction:column; align-items:center; gap:10px; margin-top:18px; }
  .sb-pill-row { display:flex; justify-content:center; gap:8px; }
  .sb-pill[hidden] { display:none; }   /* author display beats the hidden attribute — restore it for the stub pills */
  .sb-pill { display:inline-flex; align-items:center; gap:7px; height:30px; padding:0 12px; border-radius:999px;
    background:var(--inset); border:1px solid var(--panel-line); color:var(--muted);
    font-size:12px; text-decoration:none; transition:color .15s, border-color .15s; }
  .sb-pill svg { width:15px; height:15px; flex:0 0 auto; }
  .sb-pill:hover { color:var(--text); }
  .sb-pill.discord:hover { color:#98a6ff; border-color:#5865f2; }   /* Discord blurple */
  .sb-pill.patreon:hover { color:#ff7d94; border-color:#ff424d; }   /* Patreon coral */
  /* the flag sits in the row like a chip: same height as the pills, square corners */
  .pride-flag { display:inline-flex; }
  .pride-flag svg { height:30px; width:47px; display:block;
    box-shadow:0 0 0 1px rgba(255,255,255,.14); }

  .sb-foot { margin-top:14px; text-align:center; }
  .sb-foot .foot-local { display:block; margin-bottom:8px; }
  .sb-foot nav { display:flex; justify-content:center; gap:14px; }
  .sb-foot a { font-size:11.5px; color:var(--muted); text-decoration:none; }
  .sb-foot a:hover { color:var(--cyan); text-decoration:underline; }
  .foot-local { font-size:11.5px; color:var(--faint); }

  /* main gameplay area — full width; the drawer overlays it */
  .main { padding:72px 16px 28px; display:flex; justify-content:center; align-items:flex-start; min-height:100vh; }
  .main .card { margin:0; }

  /* hamburger (always visible) + scrim */
  .menu-btn { position:fixed; top:16px; left:16px; z-index:50; display:flex; align-items:center; justify-content:center;
    width:46px; height:46px; padding:0; border-radius:12px; cursor:pointer;
    background:var(--inset); border:1px solid var(--panel-line); color:var(--text); }
  .menu-btn:hover { border-color:var(--violet); }
  .menu-btn svg { width:22px; height:22px; pointer-events:none; }
  .menu-btn .conn-dot { position:absolute; top:-3px; right:-3px; }
  body.sb-open .menu-btn { display:none; }
  .scrim { position:fixed; inset:0; z-index:35; background:rgba(6,4,12,.6); opacity:0; pointer-events:none; transition:opacity .25s; }
  body.sb-open .scrim { opacity:1; pointer-events:auto; }

  /* contextual setup hints in the gameplay card (no script / no device) */
  .setup-hints { margin:14px 0 0; }
  .setup-hints:empty { display:none; }
  .hint-card { font-size:13px; line-height:1.5; color:var(--muted); padding:12px 14px; border-radius:11px;
    background:var(--inset); border:1px solid var(--panel-line); }
  .hint-card + .hint-card { margin-top:8px; }
  .hint-card b { color:var(--text); }
  .hint-card.primary { border-color:rgba(255,45,155,.45); background:linear-gradient(95deg, rgba(255,45,155,.10), rgba(123,45,255,.07)); }
  .kbd { font-family:"JetBrains Mono",monospace; font-size:12px; background:rgba(255,255,255,.08);
    border:1px solid var(--panel-line); border-radius:5px; padding:1px 7px; color:var(--cyan); }

  /* phone pass: tighter chrome + bigger tap targets.
     Vertical rhythm is compressed hard here so the whole gameplay surface
     (heatmap → now-playing → transport → XP → HUD → session → abilities)
     fits one phone screen without scrolling. */
  @media (max-width:560px) {
    .main { padding:56px 12px 14px; }
    .card { padding:14px 14px; border-radius:14px; }
    h1 { font-size:28px; }
    .sub { font-size:12px; }
    .stage { gap:10px; }
    .track { width:28px; }
    button { padding:13px; }             /* still ≥44px tall tap targets */
    .row { gap:8px; }
    .xpreset { padding:7px 8px; }

    /* brand */
    .brand { gap:9px; margin-bottom:6px; }
    .brand-mark { width:34px; height:34px; }
    /* setup hints — compact on phones so the no-device hint doesn't push play off-screen */
    .setup-hints { margin-top:8px; }
    .hint-card { padding:9px 12px; font-size:12.5px; line-height:1.45; }
    .hint-card + .hint-card { margin-top:6px; }
    /* section spacing */
    .divider { margin:10px 0 6px; }
    .viz h3 { margin:8px 0 4px; }
    /* the two canvases — shorter on phones, still crisp (render.js refits to DPR) */
    #curve { height:60px; }
    #scroll { height:60px; }
    /* transport */
    .transport { margin-top:8px; }
    .transport-bar { margin-top:6px; }
    /* XP + HUD */
    .xpwrap { margin-top:8px; }
    .hud { margin-top:8px; }
    .sess-hud { gap:6px; margin-bottom:6px; }
    .sess-tile { padding:6px 5px; }
    .sess-v { font-size:16px; }
    /* session + abilities */
    .session-btn { margin-top:8px; padding:12px; }
    .abilities { margin-top:8px; }
    .abilities .gpanel-h { margin-bottom:4px; }

    /* hamburger: smaller + higher so its bottom edge clears the card top (56px) instead
       of overlapping the card's top-left corner */
    .menu-btn { top:9px; left:9px; width:40px; height:40px; border-radius:11px; }
    .menu-btn svg { width:20px; height:20px; }
  }

  /* ===== Native Android shell: fill the device screen =====
     In the web build the card is content-height and centered (fine for a window). In the
     WebView the viewport IS the device screen, so a content-height card leaves dead space
     below it. Here the card stretches to the full viewport height and the now-playing
     preview becomes the elastic element that absorbs the slack — the heatmap stays a thin
     ribbon, the controls (transport · session · abilities) pin to the bottom edge, and no
     blank gap is left at the foot of the card. Gated on `is-android` (set in state.js when
     window.RhythmHost exists) so the web layout is untouched. The preview canvas refits to
     its CSS box via the ResizeObserver in render.js, so it stays crisp at any height. */
  body.is-android { min-height:100dvh; }
  body.is-android .main {
    min-height:100dvh; align-items:stretch;   /* stretch the card to full height, not hug content */
  }
  body.is-android .card {
    display:flex; flex-direction:column; max-width:none;   /* full width + height on the phone */
  }
  /* the gameplay surface grows to fill whatever height is left under the brand/heatmap */
  body.is-android .viz { flex:1 1 auto; display:flex; flex-direction:column; min-height:0; }
  /* The leftover vertical space is split evenly between the now-playing view and the Edge/Ruin
     abilities, so both visibly grow with the screen — not just the preview. */
  body.is-android .preview-screen { flex:1 1 0; min-height:120px; }
  body.is-android #scroll { height:100%; }
  /* Abilities stretch to fill down to the card's bottom edge: the row grows and its buttons
     auto-stretch to the row height (flex row default align-items:stretch). A min-height floor
     keeps the buttons chunky even on shorter screens. The session button between them keeps its
     size. */
  body.is-android .abilities { flex:1 1 0; display:flex; flex-direction:column; min-height:96px; }
  body.is-android .abilities .ability-row { flex:1 1 auto; align-items:stretch; }
  body.is-android .ability-row > button { height:auto; }

  /* ===== Visuals pane (themed clips, visuals.js) =====
     Desktop: the gameplay card docks to the right edge and the pane absorbs the entire
     rest of the viewport (sticky, full height — the clip object-fit:covers the box).
     Narrow screens stack a 16:9 pane above the card; Android turns it into a fixed-height
     band so the full-screen card flex (above) keeps filling the rest. */
  #visualsPane { display:none; }
  body.visuals-on #visualsPane {
    display:block; position:relative; min-width:0; overflow:hidden;
    border-radius:18px; background:#000;   /* letterbox/pillarbox bars */
    border:1px solid var(--panel-line); box-shadow:0 24px 70px rgba(0,0,0,.55);
  }
  #visualsPane iframe { position:absolute; inset:0; width:100%; height:100%; border:0;
    background:#000;
    opacity:0; transition:opacity .15s; }   /* hard cut — a hair of fade masks embed paint */
  #visualsPane iframe.front { opacity:1; }
  /* windowed: the embed is interactive (seek, share, creator click-through) — but only the
     visible buffer; the hidden preload must never swallow input meant for the front */
  #visualsPane iframe:not(.front) { pointer-events:none; }
  .vis-msg { position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    padding:20px; text-align:center; color:var(--muted); font-size:13px; }
  .vis-msg[hidden] { display:none; }
  /* clip actions (skip / block) — overlaid above the front iframe; hover reveals them.
     Touch has no hover and the interactive front embed swallows taps (so tap-to-reveal
     can't work) — keep them faintly visible there instead. */
  #visActions { position:absolute; top:10px; right:10px; z-index:5; display:flex; gap:8px;
    opacity:0; transition:opacity .2s; }
  #visActions[hidden] { display:none; }
  #visActions .icon-btn { background:rgba(12,7,23,.72); }
  #visualsPane:hover #visActions, #visActions:focus-within { opacity:1; }
  @media (hover:none) { #visActions { opacity:.55; } }
  #visBlockBtn:hover:not(:disabled) { color:var(--magenta); border-color:var(--magenta);
    box-shadow:0 0 12px rgba(255,45,155,.2); }
  body.visuals-on .main { gap:20px; }

  /* sidebar picker: pill grids reusing the .mode look */
  .vis-presets { display:grid; grid-template-columns:repeat(3,1fr); gap:6px; padding:4px;
    background:var(--inset); border:1px solid var(--panel-line); border-radius:12px; margin-top:10px; }
  .vis-presets button { flex:none; }
  #visOrient { grid-template-columns:repeat(4,1fr); }   /* the four orientations on one row */
  /* selected tag chips — the act-pill treatment (small unboxed pills, flat solid; see
     #actRow), not the segmented box: they're removals, not a picker, so they read quieter */
  #visTags { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
  #visTags .vis-tag { flex:0 1 auto; padding:6px 12px; font-size:12px; border-radius:15px;
    border:1px solid transparent; color:#fff; background:var(--violet); box-shadow:none; }
  #visTags .vis-tag:hover:not(:disabled) { background:var(--magenta); border-color:transparent; }
  .vis-tag-x { opacity:.75; margin-left:6px; }
  .vis-tags-empty { color:var(--faint); font-size:12px; padding:6px 0; }

  /* custom-tag autocomplete dropdown */
  .vis-custom-wrap { position:relative; }
  .vis-sug { position:absolute; top:calc(100% + 4px); left:0; right:0; z-index:30;
    max-height:260px; overflow-y:auto; padding:4px; border-radius:12px;
    background:var(--panel); border:1px solid var(--panel-line);
    box-shadow:0 14px 40px rgba(0,0,0,.6);
    -webkit-backdrop-filter:blur(16px); backdrop-filter:blur(16px); }
  .vis-sug[hidden] { display:none; }
  .vis-sug-item { display:flex; justify-content:space-between; align-items:center; gap:10px;
    width:100%; padding:8px 10px; border:none; border-radius:8px; background:transparent;
    color:var(--text); font-size:13px; text-align:left; cursor:pointer; }
  .vis-sug-item:hover { background:var(--inset); color:var(--cyan); }
  .vis-sug-n { color:var(--faint); font-size:11.5px; flex:none; }

  /* desktop: card fully right, pane fills everything else edge-to-edge */
  @media (min-width: 921px) {
    body.visuals-on:not(.is-android) .main { justify-content:flex-end; }
    body.visuals-on:not(.is-android) #visualsPane {
      position:sticky; top:72px; flex:1 1 auto;
      height:calc(100vh - 100px);   /* .main pads 72px top + 28px bottom */
    }
  }

  /* narrow: stack a 16:9 pane above the card */
  @media (max-width: 920px) {
    body.visuals-on .main { flex-direction:column; align-items:center; }
    body.visuals-on #visualsPane { width:100%; max-width:560px; flex:0 0 auto; aspect-ratio:16/9; }
  }

  /* Android full-screen flex: pane is a fixed band on top, the card flexes below it */
  body.is-android.visuals-on .main { flex-direction:column; align-items:stretch; }
  body.is-android.visuals-on #visualsPane { width:100%; max-width:none;
    height:30vh; flex:0 0 auto; border-radius:14px; }
  body.is-android.visuals-on .card { flex:1 1 auto; min-height:0; }

  /* ===== Fullscreen mode (fullscreen.js, body.fs-mode) =====
     The view fills the screen; the card collapses into a slim fixed bar along the bottom
     holding only the game controls (transport · time · heatmap-as-seekbar · session ·
     abilities). Multipliers, stats, XP, and headers are hidden. fs-idle (mouse still
     while playing) fades the bar out and hides the cursor. Kept below the overlays
     (z 50-60) and the sidebar (z 40). MUST stay the last block in this file — it
     overrides the desktop/narrow/Android visuals layouts above on equal specificity. */
  body.fs-mode .menu-btn, body.fs-mode .brand, body.fs-mode #setupHints,
  body.fs-mode .divider, body.fs-mode .viz > h3,
  body.fs-mode .hud, body.fs-mode .abilities .gpanel-h,
  body.fs-mode .xpwrap { display:none; }

  /* the maximized view: visuals pane, or the stroke preview when visuals are off */
  body.fs-mode.visuals-on #visualsPane {
    position:fixed; inset:0; width:auto; height:auto; max-width:none; aspect-ratio:auto;
    border:none; border-radius:0; box-shadow:none; z-index:1;
  }
  /* fs: while the bar is up the front embed stays interactive (seek/share/creator links,
     same as windowed) — only once fs-idle hides the bar does the pane go inert, so the
     next input falls through to the wake-the-bar listeners (fullscreen.js fsPoke), which
     never see events swallowed by an iframe (standard video-player wake tap). Clicks INTO
     the interactive embed can't poke either — fullscreen.js catches the window blur they
     cause and restarts the countdown. */
  body.fs-mode.fs-idle #visualsPane iframe { pointer-events:none; }
  body.fs-mode.visuals-on .preview-screen { display:none; }
  body.fs-mode:not(.visuals-on) .preview-screen {
    position:fixed; inset:0; z-index:1; border:none; border-radius:0; margin:0; max-width:none;
  }
  body.fs-mode:not(.visuals-on) #scroll { width:100%; height:100%; }

  /* the slim bar */
  body.fs-mode .card {
    position:fixed; left:0; right:0; bottom:0; top:auto; z-index:20;
    display:flex; max-width:none; width:auto; min-height:0;
    padding:8px 14px; border:none; border-top:1px solid var(--panel-line); border-radius:0;
    background:rgba(12,7,23,.85); box-shadow:0 -12px 40px rgba(0,0,0,.45);
    transition:opacity .3s;
  }
  body.fs-mode .viz { display:flex; flex:1 1 auto; flex-direction:row; align-items:center; gap:14px; min-height:0; }
  body.fs-mode .transport-bar { order:1; margin:0; }
  body.fs-mode .transport { order:2; margin:0; }
  body.fs-mode #curve { order:3; flex:1 1 auto; min-width:100px; width:auto; height:34px; }
  body.fs-mode .session-btn { order:4; width:auto; margin:0; padding:9px 20px; font-size:13px; flex:0 0 auto; }
  body.fs-mode .abilities { order:5; margin:0; flex:0 0 auto; display:block; min-height:0; }
  body.fs-mode .abilities .ability-row { margin:0; gap:8px; }
  /* stable width — content-sized buttons would resize the whole bar as the label text
     changes state ("Edge" ↔ "Resume — 12s held") */
  body.fs-mode .abilities .ability-row > button { padding:8px 12px; font-size:13px; height:auto; min-height:0;
    width:160px; flex:0 0 auto; }

  /* idle: fade the bar, hide the cursor */
  body.fs-mode.fs-idle .card { opacity:0; pointer-events:none; }
  body.fs-mode.fs-idle { cursor:none; }
  /* clip actions ride the same wake cycle (their clicks bubble to the fsPoke listeners);
     the iframe pointer-events:none rule above doesn't touch them — they're a sibling */
  body.fs-mode #visActions { opacity:1; }
  body.fs-mode.fs-idle #visActions { opacity:0; pointer-events:none; }

  /* the moment a session ends (End session or Deny), its overlay exists and owns the screen —
     the mini-bar vanishes instantly (no fade) so the goo splat lands on ONLY the video, and
     mouse pokes can't resurrect it until the overlay is closed */
  body.fs-mode:has(.sum-overlay:not([hidden])) .card,
  body.fs-mode:has(.deny-overlay:not([hidden])) .card { opacity:0; pointer-events:none; transition:none; }

  /* fullscreen toggle glyphs */
  #fsBtn { display:flex; align-items:center; justify-content:center; }
  #fsBtn svg { width:16px; height:16px; pointer-events:none; }
  #fsBtn .fs-compress { display:none; }
  body.fs-mode #fsBtn .fs-expand { display:none; }
  body.fs-mode #fsBtn .fs-compress { display:block; }

  /* phone-landscape widths: tighten the bar so everything still fits on one row */
  @media (max-width: 900px) {
    body.fs-mode .viz { gap:8px; }
    body.fs-mode .abilities .ability-row > button { width:112px; }
    body.fs-mode .session-btn { padding:9px 12px; }
    body.fs-mode #curve { min-width:60px; }
  }

