/*
 * ==========================================================
 * dingbat — design tokens
 * "handheld console": deep indigo-black + warm amber phosphor
 * ==========================================================
 */

:root {
  /* ----------------------------------------------------------
   * The color tokens below are grouped so a theme can override
   * :root wholesale (e.g. body[data-theme="…"]). Every chrome
   * color in this file derives from one of these — search the
   * file and you'll find no bare palette literal outside a
   * pure-black/white shadow. Keep each `*-rgb` triplet in sync
   * with the solid token it mirrors.
   * -------------------------------------------------------- */

  /* ---- Base surfaces (opaque) ---- */
  --bg: #0c0e16;
  --stage: #05060a;
  --surface-1: #141826;
  --surface-2: #1c2233;
  --surface-3: #262e44;
  --surface-hover: #2f3852;

  /* ---- Raw RGB triplets, for rgba() alpha compositing. A theme
     overrides the triplet and every rgba() built on it follows. ---- */
  --hi-rgb: 255, 255, 255;        /* white highlights / hairline insets */
  --accent-rgb: 255, 176, 77;     /* == --accent */
  --accent-hi-rgb: 255, 214, 150; /* warm highlight (active-button inset) */
  --danger-rgb: 255, 107, 107;    /* == --danger */
  --text-rgb: 234, 238, 246;      /* == --text */
  --stage-rgb: 5, 6, 10;          /* == --stage (dark scrims) */
  --surface-1-rgb: 20, 24, 38;    /* == --surface-1 */

  /* ---- Hairline borders & insets ---- */
  --border: rgba(var(--hi-rgb), 0.08);
  --border-2: rgba(var(--hi-rgb), 0.14);
  --inset-hi: rgba(var(--hi-rgb), 0.09);
  --inset-lo: rgba(0, 0, 0, 0.45);

  /* ---- Text ---- */
  --text: #eaeef6;
  --text-dim: #99a3ba;
  --text-faint: #616b83;

  /* ---- Accent (amber phosphor) ---- */
  --accent: #ffb04d;
  --accent-2: #ffc880;
  --accent-hi: #ffd699;         /* button-primary hover gradient top */
  --accent-ink: #2a1800;        /* ink on accent fills */
  --accent-glow: rgba(var(--accent-rgb), 0.32);

  /* ---- Status ---- */
  --live: #4fd583;
  --danger: #ff6b6b;
  --danger-hi: #ff9b9b;         /* "armed" destructive-button gradient top */

  /* ---- Screen (LCD off-state) ---- */
  --screen-off: #000;

  /* ---- Chrome gradient endpoints ---- */
  --topbar-top: #161b2b;
  --topbar-bottom: #10131e;
  --surface-2-lo: #171d2c;       /* darker foot of surface-2 button gradients */
  --stage-top: #0b0d15;          /* stage radial-gradient centre */
  --control-strip-top: #0e1119;  /* touch-control strip gradient top */

  /* ---- Amber-tinted pressed / active fills ---- */
  --accent-tint-top: #2a2410;    /* shared top of active/pressed amber fills */
  --btn-active-bottom: #211c0c;  /* icon-btn.active / segmented-bar active */
  --pad-pressed-bottom: #1d1809; /* on-screen pad pressed */

  /* ---- Dark translucent scrims (feature-specific) ---- */
  --drop-overlay-bg: rgba(9, 11, 18, 0.82);
  --paused-close-bg: rgba(10, 10, 12, 0.62);
  --paused-close-bg-hover: rgba(20, 12, 12, 0.8);
  --net-stall-bg: rgba(20, 16, 8, 0.72);

  /* ---- Developer log ---- */
  --log-text: #d7dbe6;
  --log-error: #ff7b72;

  /* ---- Cartridge icons (home tiles) ---- */
  --cart-gba-top: #6b7488;
  --cart-gba-bottom: #464e5e;
  --cart-gba-label-lo: #d98724;  /* label gradient foot (top = --accent) */
  --cart-gb-top: #9a9ea8;
  --cart-gb-bottom: #6e727c;
  --cart-gbc-top: #8f6fe6;
  --cart-gbc-bottom: #5b3fc0;

  /* ---- System badges ---- */
  --badge-gba-fg: #b3a7ff;
  --badge-gba-bg: rgba(138, 125, 255, 0.16);
  --badge-gb-fg: #74d47f;
  --badge-gb-bg: rgba(95, 191, 106, 0.16);
  --badge-gbc-fg: #5fd6de;
  --badge-gbc-bg: rgba(79, 208, 216, 0.16);

  /* ---- Touch controls (on-screen d-pad / buttons). Default to the neutral
     surface look; device themes override these to match the real handheld's
     buttons. --pad-* covers every pad; --btn-ab-* overrides just A/B; the
     --frame-line brackets the screen (top bar + control strip edges). ---- */
  --pad-top: var(--surface-3);
  --pad-bottom: var(--surface-1);
  --pad-border: var(--border-2);
  --pad-label: var(--text-dim);
  --btn-ab-top: var(--pad-top);
  --btn-ab-bottom: var(--pad-bottom);
  --btn-ab-border: var(--pad-border);
  --btn-ab-border-w: 1px;
  --btn-ab-label: var(--pad-label);
  --pill-top: var(--pad-top);
  --pill-bottom: var(--pad-bottom);
  --pill-label: var(--pad-label);
  --frame-line: var(--border);
  /* Decorative double pinstripe across the control panel (Famicom); the
     two lines sit just through the bottom of the d-pad / A-B cluster. */
  --panel-stripe: transparent;

  /* Chrome (top bar + screen surround + control panel). Device themes paint
     these the shell colour with legible dark/light ink; defaults keep the
     dark UI look so amber/black/light are unchanged. */
  --chrome-ink: var(--text);
  --chrome-ink-dim: var(--text-dim);
  --chrome-btn-top: var(--surface-2);
  --chrome-btn-bottom: var(--surface-2-lo);
  --chrome-btn-border: var(--border);
  --chrome-btn-hover-top: var(--surface-3);
  --chrome-btn-hover-bottom: var(--surface-2);
  --control-strip-bottom: var(--bg);
  --home-bg: transparent;

  --radius: 11px;
  --radius-sm: 8px;
  --radius-lg: 18px;

  --font-ui: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, Roboto,
    Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo,
    Consolas, monospace;

  --bar-h: 52px;

  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
}

/*
 * ==========================================================
 * Chrome themes
 * Selected in Settings → Video → App theme; persisted to
 * localStorage ("dingbat_theme") and applied as
 * <html data-theme="…"> by an inline script in <head> before
 * first paint. Amber (above) is the default — no attribute.
 * Each block overrides only the tokens that differ.
 * ==========================================================
 */

/* ---- Pure Black: true-#000 OLED chrome, neutral gray surfaces,
       amber accent kept (classic amber-on-black phosphor) ---- */
:root[data-theme="black"] {
  --bg: #000000;
  --stage: #000000;
  --surface-1: #0e0e0f;
  --surface-2: #161617;
  --surface-3: #202021;
  --surface-hover: #29292b;

  --stage-rgb: 0, 0, 0;
  --surface-1-rgb: 14, 14, 15;
  --text-rgb: 236, 236, 238;

  /* True-black surfaces need slightly stronger hairlines to separate */
  --border: rgba(var(--hi-rgb), 0.1);
  --border-2: rgba(var(--hi-rgb), 0.17);

  --text: #ececee;
  --text-dim: #9c9ca4;
  --text-faint: #63636b;

  --topbar-top: #101011;
  --topbar-bottom: #060607;
  --surface-2-lo: #101011;
  --stage-top: #000000;
  --control-strip-top: #050505;

  --accent-tint-top: #221d0c;
  --btn-active-bottom: #191507;
  --pad-pressed-bottom: #141104;

  --drop-overlay-bg: rgba(0, 0, 0, 0.85);
  --net-stall-bg: rgba(10, 8, 4, 0.75);
}

/* OLED black: let the ambient glow bleed out of the stage and fade behind the
   top bar and control strip. Their fills (and the stage's own) go transparent
   so the true #000 body background is the sole backdrop and the halo reads as
   one continuous wash. The glow drops to a negative layer so it sits behind the
   chrome and the game screen, and the stage stops clipping it at its edges.
   Scoped to this theme — the other themes keep their tinted chrome gradients. */
:root[data-theme="black"] #stage {
  overflow: visible;
  background: transparent;
}
:root[data-theme="black"] #glow-canvas {
  z-index: -1;
}
:root[data-theme="black"] #topbar,
:root[data-theme="black"] #controls {
  background: transparent;
}

/* ---- Light: dark-on-light equivalent of the amber chrome.
       Hairline "highlights" become dark borders; insets stay a soft
       white bevel (correct on light surfaces); accent deepens to a
       burnt amber that holds ≥4.5:1 as text on the light surfaces. ---- */
:root[data-theme="light"] {
  --bg: #e2e4ea;
  --stage: #cdd0d9;
  --surface-1: #eef0f5;
  --surface-2: #f3f4f8;
  --surface-3: #fbfcfe;
  --surface-hover: #ffffff;

  /* --hi-rgb stays white: knobs/thumbs and top-edge bevels remain
     correct on light surfaces. Borders are overridden to dark. */
  --accent-rgb: 156, 84, 0;
  --accent-hi-rgb: 216, 138, 31;
  --danger-rgb: 207, 63, 63;
  --text-rgb: 29, 36, 51;
  --stage-rgb: 205, 208, 217;
  --surface-1-rgb: 238, 240, 245;

  --border: rgba(24, 32, 52, 0.16);
  --border-2: rgba(24, 32, 52, 0.28);
  --inset-hi: rgba(255, 255, 255, 0.85);
  --inset-lo: rgba(24, 32, 52, 0.16);

  --text: #1d2433;
  --text-dim: #4d576d;
  --text-faint: #6d7689;

  --accent: #9c5400;
  --accent-2: #b76a0a;
  --accent-hi: #d88a1f;
  --accent-ink: #ffffff;
  --accent-glow: rgba(var(--accent-rgb), 0.28);

  --live: #22a55b;
  --danger: #cf3f3f;
  --danger-hi: #e06060;

  --topbar-top: #f7f8fb;
  --topbar-bottom: #e9ebf1;
  --surface-2-lo: #e8eaf1;
  --stage-top: #d8dbe2;
  --control-strip-top: #dfe2ea;

  /* Pressed/active fills invert to light amber tints; the accent
     (#9c5400) stays the icon/label color on top of them. */
  --accent-tint-top: #ffe7c4;
  --btn-active-bottom: #ffdca6;
  --pad-pressed-bottom: #f8d193;

  --drop-overlay-bg: rgba(233, 236, 243, 0.85);
  --paused-close-bg: rgba(245, 246, 250, 0.82);
  --paused-close-bg-hover: rgba(255, 236, 236, 0.92);
  --net-stall-bg: rgba(248, 249, 252, 0.88);

  --log-text: #2a3040;
  --log-error: #c62828;

  --cart-gba-label-lo: #7a4200;

  --badge-gba-fg: #4f46b8;
  --badge-gba-bg: rgba(99, 91, 255, 0.14);
  --badge-gb-fg: #1e7a2e;
  --badge-gb-bg: rgba(46, 150, 60, 0.15);
  --badge-gbc-fg: #086f77;
  --badge-gbc-bg: rgba(20, 160, 170, 0.14);
}

/* The two white-tint hover fills are invisible on light surfaces */
:root[data-theme="light"] .button-ghost:hover,
:root[data-theme="light"] .modal-close:hover {
  background: rgba(24, 32, 52, 0.07);
}

/* ---- Indigo: translucent purple AGB-001 shell, blue-violet accent.
       Hue/accent sampled from a real purple GBA (H~250). ---- */
:root[data-theme="indigo"] {
  --bg: #0d0b17;
  --stage: #050408;
  --surface-1: #161328;
  --surface-2: #1d1834;
  --surface-3: #262143;
  --surface-hover: #312a53;

  --accent-rgb: 127, 106, 231;
  --accent-hi-rgb: 205, 199, 240;
  --text-rgb: 236, 235, 239;
  --stage-rgb: 5, 4, 8;
  --surface-1-rgb: 22, 19, 40;

  --text: #ecebef;
  --text-dim: #a19fac;
  --text-faint: #696774;

  --accent: #7f6ae7;
  --accent-2: #ab9eeb;
  --accent-hi: #cdc7f0;
  --accent-ink: #0e0637;
  --accent-glow: rgba(var(--accent-rgb), 0.35);

  --topbar-top: #19152d;
  --topbar-bottom: #110f1f;
  --surface-2-lo: #1a162f;
  --stage-top: #0b0913;
  --control-strip-top: #0e0b18;

  --accent-tint-top: #1c1738;
  --btn-active-bottom: #15112a;
  --pad-pressed-bottom: #110e22;

  --drop-overlay-bg: rgba(11, 9, 19, 0.82);
  --net-stall-bg: rgba(17, 13, 33, 0.75);

  --cart-gba-label-lo: #705cd6;

  /* Chrome = shell colour (#675ba0) around the screen: top bar,
     screen surround and control panel. Menus/home stay dark. */
  --topbar-top: #6c60a5;
  --topbar-bottom: #5a508c;
  --stage: #675ba0;
  --stage-top: #756aaa;
  --control-strip-top: #695da3;
  --control-strip-bottom: #615596;
  --home-bg: var(--bg);
  --frame-line: rgba(0, 0, 0, 0.42);
  --chrome-ink: #fbf7fd;
  --chrome-ink-dim: #d9d2de;
  --chrome-btn-top: rgba(0, 0, 0, 0.24);
  --chrome-btn-bottom: rgba(0, 0, 0, 0.32);
  --chrome-btn-border: rgba(255, 255, 255, 0.22);
  --chrome-btn-hover-top: rgba(0, 0, 0, 0.34);
  --chrome-btn-hover-bottom: rgba(0, 0, 0, 0.44);

  /* GBA buttons: pale off-white plastic (#d5d4d9) */
  --pad-top: #e2e2e5;
  --pad-bottom: #c5c4cb;
  --pad-border: rgba(20, 16, 12, 0.32);
  --pad-label: #3a3a40;
}

/* ---- Fuchsia: milky translucent fuchsia-pink GBA shell, dusty-rose accent.
       Hue/accent sampled from a real fuchsia GBA (H~336) — softer and warmer
       than a neon hot pink. ---- */
:root[data-theme="fuchsia"] {
  --bg: #170a0f;
  --stage: #090406;
  --surface-1: #28121b;
  --surface-2: #351823;
  --surface-3: #44202e;
  --surface-hover: #54293a;

  --accent-rgb: 232, 115, 154;
  --accent-hi-rgb: 240, 204, 216;
  --text-rgb: 239, 235, 237;
  --stage-rgb: 9, 4, 6;
  --surface-1-rgb: 40, 18, 27;

  --text: #efebed;
  --text-dim: #ad9fa4;
  --text-faint: #74676c;

  --accent: #e8739a;
  --accent-2: #eca2bb;
  --accent-hi: #f0ccd8;
  --accent-ink: #340414;
  --accent-glow: rgba(var(--accent-rgb), 0.32);

  --topbar-top: #2e151f;
  --topbar-bottom: #200e15;
  --surface-2-lo: #301520;
  --stage-top: #13090d;
  --control-strip-top: #190b10;

  --accent-tint-top: #381724;
  --btn-active-bottom: #2a111b;
  --pad-pressed-bottom: #220e16;

  --drop-overlay-bg: rgba(19, 9, 13, 0.82);
  --net-stall-bg: rgba(33, 13, 21, 0.75);

  --cart-gba-label-lo: #d1618e;

  /* Chrome = shell colour (#af748c) around the screen: top bar,
     screen surround and control panel. Menus/home stay dark. */
  --topbar-top: #b37a91;
  --topbar-bottom: #a4617c;
  --stage: #af748c;
  --stage-top: #b88499;
  --control-strip-top: #b1778f;
  --control-strip-bottom: #a96a84;
  --home-bg: var(--bg);
  --frame-line: rgba(0, 0, 0, 0.34);
  --chrome-ink: #211d1a;
  --chrome-ink-dim: #524b45;
  --chrome-btn-top: rgba(255, 255, 255, 0.30);
  --chrome-btn-bottom: rgba(255, 255, 255, 0.12);
  --chrome-btn-border: rgba(0, 0, 0, 0.16);
  --chrome-btn-hover-top: rgba(255, 255, 255, 0.44);
  --chrome-btn-hover-bottom: rgba(255, 255, 255, 0.24);

  /* GBA buttons: pale off-white plastic (#d5d4d9) */
  --pad-top: #e2e2e5;
  --pad-bottom: #c5c4cb;
  --pad-border: rgba(20, 16, 12, 0.32);
  --pad-label: #3a3a40;
}

/* ---- Glacier: milky translucent clear-blue GBA shell, icy periwinkle accent.
       Hue/accent sampled from a real glacier GBA (H~222) — periwinkle-blue,
       not the cyan it used to be. ---- */
:root[data-theme="glacier"] {
  --bg: #0b0e16;
  --stage: #040608;
  --surface-1: #131927;
  --surface-2: #192133;
  --surface-3: #222b42;
  --surface-hover: #2b3752;

  --accent-rgb: 118, 155, 229;
  --accent-hi-rgb: 204, 217, 240;
  --text-rgb: 235, 236, 239;
  --stage-rgb: 4, 6, 8;
  --surface-1-rgb: 19, 25, 39;

  --text: #ebecef;
  --text-dim: #a0a3ac;
  --text-faint: #686b74;

  --accent: #769be5;
  --accent-2: #a4bbea;
  --accent-hi: #ccd9f0;
  --accent-ink: #061537;
  --accent-glow: rgba(var(--accent-rgb), 0.3);

  --topbar-top: #161d2c;
  --topbar-bottom: #0f141f;
  --surface-2-lo: #171e2e;
  --stage-top: #090c13;
  --control-strip-top: #0c0f18;

  --accent-tint-top: #182137;
  --btn-active-bottom: #121929;
  --pad-pressed-bottom: #0f1422;

  --drop-overlay-bg: rgba(10, 12, 18, 0.82);
  --net-stall-bg: rgba(14, 19, 32, 0.75);

  --cart-gba-label-lo: #5e85d4;

  /* Chrome = shell colour (#999db6) around the screen: top bar,
     screen surround and control panel. Menus/home stay dark. */
  --topbar-top: #9fa3ba;
  --topbar-bottom: #878ca9;
  --stage: #999db6;
  --stage-top: #a8abc1;
  --control-strip-top: #9ca0b8;
  --control-strip-bottom: #9094b0;
  --home-bg: var(--bg);
  --frame-line: rgba(0, 0, 0, 0.34);
  --chrome-ink: #211d1a;
  --chrome-ink-dim: #524b45;
  --chrome-btn-top: rgba(255, 255, 255, 0.30);
  --chrome-btn-bottom: rgba(255, 255, 255, 0.12);
  --chrome-btn-border: rgba(0, 0, 0, 0.16);
  --chrome-btn-hover-top: rgba(255, 255, 255, 0.44);
  --chrome-btn-hover-bottom: rgba(255, 255, 255, 0.24);

  /* GBA buttons: pale off-white plastic (#d5d4d9) */
  --pad-top: #e2e2e5;
  --pad-bottom: #c5c4cb;
  --pad-border: rgba(20, 16, 12, 0.32);
  --pad-label: #3a3a40;
}

/* ---- Kiwi: translucent green Game Boy Color (Nintendo's "Kiwi"),
       grass-green accent sampled from the real shell (H~110). ---- */
:root[data-theme="kiwi"] {
  --bg: #0c170b;
  --stage: #050804;
  --surface-1: #162813;
  --surface-2: #1c3418;
  --surface-3: #254321;
  --surface-hover: #2f532a;

  --accent-rgb: 101, 219, 77;
  --accent-hi-rgb: 198, 237, 192;
  --text-rgb: 236, 239, 235;
  --stage-rgb: 5, 8, 4;
  --surface-1-rgb: 22, 40, 19;

  --text: #ecefeb;
  --text-dim: #a1ac9f;
  --text-faint: #697467;

  --accent: #65db4d;
  --accent-2: #97e189;
  --accent-hi: #c6edc0;
  --accent-ink: #0c3905;
  --accent-glow: rgba(var(--accent-rgb), 0.28);

  --topbar-top: #182d15;
  --topbar-bottom: #111f0f;
  --surface-2-lo: #192f16;
  --stage-top: #0b1309;
  --control-strip-top: #0d180b;

  --accent-tint-top: #1b3916;
  --btn-active-bottom: #142a10;
  --pad-pressed-bottom: #10230e;

  --drop-overlay-bg: rgba(11, 19, 9, 0.82);
  --net-stall-bg: rgba(16, 33, 13, 0.75);

  --cart-gba-label-lo: #4dcc33;

  /* Chrome = shell colour (#6ee126) around the screen: top bar,
     screen surround and control panel. Menus/home stay dark. */
  --topbar-top: #74e22f;
  --topbar-bottom: #60cc1c;
  --stage: #6ee126;
  --stage-top: #7de43c;
  --control-strip-top: #71e22a;
  --control-strip-bottom: #66da1e;
  --home-bg: var(--bg);
  --frame-line: rgba(0, 0, 0, 0.34);
  --chrome-ink: #211d1a;
  --chrome-ink-dim: #524b45;
  --chrome-btn-top: rgba(255, 255, 255, 0.30);
  --chrome-btn-bottom: rgba(255, 255, 255, 0.12);
  --chrome-btn-border: rgba(0, 0, 0, 0.16);
  --chrome-btn-hover-top: rgba(255, 255, 255, 0.44);
  --chrome-btn-hover-bottom: rgba(255, 255, 255, 0.24);

  /* GBC buttons: black plastic (#282a30) */
  --pad-top: #34363e;
  --pad-bottom: #1a1b1f;
  --pad-border: rgba(0, 0, 0, 0.55);
  --pad-label: #cfced4;
}

/* ---- DMG: the original Game Boy — warm-grey brick chrome with the
       iconic pea-green LCD as the accent (screen green H~82). ---- */
:root[data-theme="dmg"] {
  --bg: #111110;
  --stage: #070706;
  --surface-1: #1f1e1c;
  --surface-2: #282824;
  --surface-3: #34332f;
  --surface-hover: #41413c;

  --accent-rgb: 156, 201, 84;
  --accent-hi-rgb: 205, 225, 183;
  --text-rgb: 237, 237, 237;
  --stage-rgb: 7, 7, 6;
  --surface-1-rgb: 31, 30, 28;

  --text: #ededed;
  --text-dim: #a7a6a5;
  --text-faint: #6f6e6d;

  --accent: #9cc954;
  --accent-2: #b1d283;
  --accent-hi: #cde1b7;
  --accent-ink: #273c07;
  --accent-glow: rgba(var(--accent-rgb), 0.28);

  --topbar-top: #23221f;
  --topbar-bottom: #181816;
  --surface-2-lo: #242421;
  --stage-top: #0f0f0d;
  --control-strip-top: #131311;

  --accent-tint-top: #2b331c;
  --btn-active-bottom: #202615;
  --pad-pressed-bottom: #1b1f11;

  --drop-overlay-bg: rgba(15, 14, 13, 0.82);
  --net-stall-bg: rgba(25, 30, 16, 0.75);

  --cart-gba-label-lo: #7da63a;

  /* Chrome = shell colour (#c2bcb9) around the screen: top bar,
     screen surround and control panel. Menus/home stay dark. */
  --topbar-top: #c7c1be;
  --topbar-bottom: #b4aca9;
  --stage: #c2bcb9;
  --stage-top: #cec9c7;
  --control-strip-top: #c4bfbc;
  --control-strip-bottom: #bbb4b1;
  --home-bg: var(--bg);
  --frame-line: rgba(0, 0, 0, 0.34);
  --chrome-ink: #211d1a;
  --chrome-ink-dim: #524b45;
  --chrome-btn-top: rgba(255, 255, 255, 0.30);
  --chrome-btn-bottom: rgba(255, 255, 255, 0.12);
  --chrome-btn-border: rgba(0, 0, 0, 0.16);
  --chrome-btn-hover-top: rgba(255, 255, 255, 0.44);
  --chrome-btn-hover-bottom: rgba(255, 255, 255, 0.24);

  /* DMG: black d-pad (#262828), magenta A/B (#9f295c),
     grey Select/Start (#7f797c), blue-violet labels */
  --pad-top: #323535;
  --pad-bottom: #171818;
  --pad-border: rgba(0, 0, 0, 0.55);
  --pad-label: #9d98db;
  --btn-ab-top: #b32e68;
  --btn-ab-bottom: #87234e;
  --btn-ab-border: rgba(0, 0, 0, 0.4);
  --btn-ab-label: #f3dbe7;
  --pill-top: #8c8689;
  --pill-bottom: #6f6a6d;
  /* Dark violet on the grey pills — the mid-violet pad label washed out */
  --pill-label: #211d33;
}

/* ---- Atomic Purple: translucent Atomic Purple Game Boy Color — an
       orchid violet, more magenta than Indigo (H~282). ---- */
:root[data-theme="atomic-purple"] {
  --bg: #120b16;
  --stage: #070408;
  --surface-1: #211327;
  --surface-2: #2b1933;
  --surface-3: #372242;
  --surface-hover: #452b52;

  --accent-rgb: 195, 110, 231;
  --accent-hi-rgb: 231, 203, 240;
  --text-rgb: 238, 235, 239;
  --stage-rgb: 7, 4, 8;
  --surface-1-rgb: 33, 19, 39;

  --text: #eeebef;
  --text-dim: #a8a0ac;
  --text-faint: #706874;

  --accent: #c36ee7;
  --accent-2: #d5a3eb;
  --accent-hi: #e7cbf0;
  --accent-ink: #290638;
  --accent-glow: rgba(var(--accent-rgb), 0.32);

  --topbar-top: #25162c;
  --topbar-bottom: #1a0f1f;
  --surface-2-lo: #26172e;
  --stage-top: #100913;
  --control-strip-top: #140c18;

  --accent-tint-top: #2d1738;
  --btn-active-bottom: #21112a;
  --pad-pressed-bottom: #1c0e22;

  --drop-overlay-bg: rgba(15, 10, 18, 0.82);
  --net-stall-bg: rgba(26, 13, 33, 0.75);

  --cart-gba-label-lo: #ae54d4;

  /* Chrome = shell colour (#9769a9) around the screen: top bar,
     screen surround and control panel. Menus/home stay dark. */
  --topbar-top: #9b6fad;
  --topbar-bottom: #88599b;
  --stage: #9769a9;
  --stage-top: #a279b2;
  --control-strip-top: #996cab;
  --control-strip-bottom: #905fa3;
  --home-bg: var(--bg);
  --frame-line: rgba(0, 0, 0, 0.42);
  --chrome-ink: #fbf7fd;
  --chrome-ink-dim: #d9d2de;
  --chrome-btn-top: rgba(0, 0, 0, 0.24);
  --chrome-btn-bottom: rgba(0, 0, 0, 0.32);
  --chrome-btn-border: rgba(255, 255, 255, 0.22);
  --chrome-btn-hover-top: rgba(0, 0, 0, 0.34);
  --chrome-btn-hover-bottom: rgba(0, 0, 0, 0.44);

  /* GBC buttons: black plastic (#282a30) */
  --pad-top: #34363e;
  --pad-bottom: #1a1b1f;
  --pad-border: rgba(0, 0, 0, 0.55);
  --pad-label: #cfced4;
}

/* ---- Daiei: the Daiei Hawks orange GBA — a burnt orange, redder and
       deeper than the default amber (H~22). ---- */
:root[data-theme="daiei"] {
  --bg: #160f0b;
  --stage: #080605;
  --surface-1: #271b14;
  --surface-2: #32231a;
  --surface-3: #412e23;
  --surface-hover: #513a2c;

  --accent-rgb: 235, 124, 51;
  --accent-hi-rgb: 242, 210, 176;
  --text-rgb: 239, 237, 236;
  --stage-rgb: 8, 6, 5;
  --surface-1-rgb: 39, 27, 20;

  --text: #efedec;
  --text-dim: #aba4a0;
  --text-faint: #736c68;

  --accent: #eb7c33;
  --accent-2: #efa76c;
  --accent-hi: #f2d2b0;
  --accent-ink: #441904;
  --accent-glow: rgba(var(--accent-rgb), 0.3);

  --topbar-top: #2c1e17;
  --topbar-bottom: #1e1510;
  --surface-2-lo: #2d1f17;
  --stage-top: #120d0a;
  --control-strip-top: #18100c;

  --accent-tint-top: #3b2214;
  --btn-active-bottom: #2c190f;
  --pad-pressed-bottom: #24150c;

  --drop-overlay-bg: rgba(18, 13, 10, 0.82);
  --net-stall-bg: rgba(34, 20, 11, 0.75);

  --cart-gba-label-lo: #cf6a30;

  /* Chrome = shell colour (#c76637) around the screen: top bar,
     screen surround and control panel. Menus/home stay dark. */
  --topbar-top: #ca6c3e;
  --topbar-bottom: #af5a30;
  --stage: #c76637;
  --stage-top: #cd754a;
  --control-strip-top: #c9693a;
  --control-strip-bottom: #bb6034;
  --home-bg: var(--bg);
  --frame-line: rgba(0, 0, 0, 0.34);
  --chrome-ink: #211d1a;
  --chrome-ink-dim: #524b45;
  --chrome-btn-top: rgba(255, 255, 255, 0.30);
  --chrome-btn-bottom: rgba(255, 255, 255, 0.12);
  --chrome-btn-border: rgba(0, 0, 0, 0.16);
  --chrome-btn-hover-top: rgba(255, 255, 255, 0.44);
  --chrome-btn-hover-bottom: rgba(255, 255, 255, 0.24);

  /* GBA buttons: pale off-white plastic (#d5d4d9) */
  --pad-top: #e2e2e5;
  --pad-bottom: #c5c4cb;
  --pad-border: rgba(20, 16, 12, 0.32);
  --pad-label: #3a3a40;
}

/* ---- Famicom: the Game Boy Micro Famicom edition — gold-warm chrome
       with the Family Computer's garnet red as the accent (H~2). ---- */
:root[data-theme="famicom"] {
  --bg: #12110f;
  --stage: #070706;
  --surface-1: #211e1a;
  --surface-2: #2a2722;
  --surface-3: #37332d;
  --surface-hover: #454038;

  --accent-rgb: 224, 99, 92;
  --accent-hi-rgb: 237, 194, 187;
  --text-rgb: 238, 237, 237;
  --stage-rgb: 7, 7, 6;
  --surface-1-rgb: 33, 30, 26;

  --text: #eeeded;
  --text-dim: #a8a6a4;
  --text-faint: #706e6c;

  --accent: #e0635c;
  --accent-2: #e38f87;
  --accent-hi: #edc2bb;
  --accent-ink: #340704;
  --accent-glow: rgba(var(--accent-rgb), 0.3);

  --topbar-top: #25221e;
  --topbar-bottom: #191714;
  --surface-2-lo: #26231f;
  --stage-top: #0f0e0d;
  --control-strip-top: #141210;

  --accent-tint-top: #3c1613;
  --btn-active-bottom: #2d100e;
  --pad-pressed-bottom: #250d0c;

  --drop-overlay-bg: rgba(15, 14, 13, 0.82);
  --net-stall-bg: rgba(35, 13, 11, 0.75);

  --cart-gba-label-lo: #c74343;

  /* Chrome = Famicom gold (#b99c68) around the screen: top bar,
     screen surround and control panel. Menus/home stay dark. */
  --topbar-top: #bca16f;
  --topbar-bottom: #af8e53;
  --stage: #b99c68;
  --stage-top: #b99c68;
  --control-strip-top: #bb9e6b;
  --control-strip-bottom: #b4955e;
  --home-bg: var(--bg);
  --frame-line: rgba(0, 0, 0, 0.34);
  --chrome-ink: #211d1a;
  --chrome-ink-dim: #524b45;
  --chrome-btn-top: rgba(255, 255, 255, 0.30);
  --chrome-btn-bottom: rgba(255, 255, 255, 0.12);
  --chrome-btn-border: rgba(0, 0, 0, 0.16);
  --chrome-btn-hover-top: rgba(255, 255, 255, 0.44);
  --chrome-btn-hover-bottom: rgba(255, 255, 255, 0.24);

  /* Famicom: dark buttons (#33363b), bold red A/B rings (#b44148), and the
     faceplate's double pinstripe across the control panel */
  --pad-top: #3f4349;
  --pad-bottom: #25272b;
  --pad-border: rgba(0, 0, 0, 0.5);
  --pad-label: #e6d9bf;
  --btn-ab-top: #3f4349;
  --btn-ab-bottom: #25272b;
  --btn-ab-border: #b44148;
  --btn-ab-border-w: 3px;
  --btn-ab-label: #e6d9bf;
  --dpad-border: #b44148;
  --dpad-border-w: 3px;
  --dpad-bar-line: transparent;
  --panel-stripe: rgba(24, 18, 10, 0.62);
}

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

.no-select {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
}

/*
 * ==========================================================
 * Base
 * ==========================================================
 */

body {
  margin: 0;
  /* Fill the layout viewport (the fixed-positioning containing block).
     Correct in browser tabs, where the layout viewport rightly excludes
     collapsible browser chrome. See the standalone override below. */
  inset: 0;
  overflow: hidden;
  position: fixed;
  touch-action: none;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* iOS installed-to-home-screen (standalone): size to 100vh (the actual viewport
   height), NOT 100% / 100dvh. Per the r/PWA "chin gap" findings, 100% measures
   the containing block and can fall short of the real viewport, leaving a bottom
   gap; 100vh is the stable full height. We no longer send black-translucent (it
   offset the viewport up and WAS the chin gap) — with the default status-bar
   style iOS reserves the status bar and fills both safe areas from the
   theme-color meta. Browser tabs keep the inset sizing above. */
@media (display-mode: standalone) {
  body {
    height: 100vh;
  }
  /* Legacy status-bar iPhones (home-button class, e.g. SE 1st gen on iOS 15):
     the standalone webview sits BELOW the opaque 20pt status bar instead of
     under an overlaying one, so 100vh (the full screen) overhangs the screen
     bottom by exactly those 20pt — clipping Select/Start. Detect the case by
     its signature, safe-area-inset-top = 0 (notch devices report >= 44px and
     the max() zeroes the correction). iOS-only via -webkit-touch-callout so
     Android/desktop PWAs (where inset-top is also 0 but 100vh is already the
     window height) keep the plain sizing; ditto legacy landscape, where iOS
     hides the status bar. */
  @supports (-webkit-touch-callout: none) {
    @media (orientation: portrait) {
      body {
        height: calc(100vh - max(0px, 20px - env(safe-area-inset-top, 0px)));
      }
    }
  }
}

button {
  font-family: inherit;
}

/*
 * ==========================================================
 * Top bar
 * ==========================================================
 */

#topbar {
  flex: 0 0 auto;
  height: calc(var(--bar-h) + var(--safe-t));
  padding: var(--safe-t) calc(10px + var(--safe-r)) 0 calc(10px + var(--safe-l));
  display: flex;
  align-items: center;
  gap: 8px;
  background: linear-gradient(180deg, var(--topbar-top), var(--topbar-bottom));
  border-bottom: 1px solid var(--frame-line);
  /* No drop shadow: it cast a dark gradient band onto the stage below */
  box-shadow: 0 1px 0 rgba(var(--hi-rgb), 0.03);
  position: relative;
  z-index: 50;
}

#topbar-spacer {
  flex: 1 1 auto;
}

/* --- Generic tactile icon button --- */
.icon-btn {
  --sz: 36px;
  width: var(--sz);
  height: var(--sz);
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--chrome-ink-dim);
  background: linear-gradient(180deg, var(--chrome-btn-top), var(--chrome-btn-bottom));
  border: 1px solid var(--chrome-btn-border);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 1px 0 var(--inset-hi), 0 1px 2px rgba(0, 0, 0, 0.35);
  cursor: pointer;
  transition: color 0.12s, background 0.12s, box-shadow 0.12s, transform 0.06s;
}

.icon-btn svg {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (hover: hover) {
  .icon-btn:hover {
    color: var(--chrome-ink);
    background: linear-gradient(180deg, var(--chrome-btn-hover-top), var(--chrome-btn-hover-bottom));
    border-color: var(--chrome-btn-border);
  }
}

.icon-btn:active {
  transform: translateY(1px);
  box-shadow: inset 0 2px 5px var(--inset-lo);
}

.icon-btn[hidden] {
  display: none;
}

.icon-btn.active {
  color: var(--accent);
  border-color: rgba(var(--accent-rgb), 0.5);
  background: linear-gradient(180deg, var(--accent-tint-top), var(--btn-active-bottom));
  box-shadow: inset 0 1px 0 rgba(var(--accent-hi-rgb), 0.15),
    0 0 0 1px rgba(var(--accent-rgb), 0.12), 0 0 12px -2px var(--accent-glow);
}

/* --- Menu --- */
#menu-container {
  position: relative;
  flex: 0 0 auto;
}

#menu-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 100;
  min-width: 210px;
  padding: 6px;
  /* Scroll when the item list is taller than the space below the top bar
     (short windows, phone landscape) — otherwise the tail is unreachable */
  max-height: calc(100dvh - var(--bar-h) - var(--safe-t) - 20px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface-1);
  border: 1px solid var(--border-2);
  border-radius: var(--radius);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.55), inset 0 1px 0 var(--inset-hi);
  animation: menu-in 0.13s ease-out;
}

/* Bottom scrim while the dropdown scrolls (phone landscape): a sticky fade
   pinned to the scrollport's bottom edge, so the clipped item visibly
   dissolves instead of reading as the end of the menu. Sticky keeps it
   anchored while scrolling. It overlays the final item, so it only paints
   (opacity 1) while more items actually sit below the fold — the
   .can-scroll-down class is toggled from JS on open/scroll/resize, exactly
   like the settings tab-bar scrims. Kept in layout at opacity 0 the rest of
   the time (not display:none) so its presence can't shift the overflow
   measurement that decides whether to show it. */
#menu-dropdown::after {
  content: "";
  display: block;
  position: sticky;
  bottom: 0;
  height: 26px;
  /* Overlay the last item with a net-zero footprint: the -26px top margin
     cancels the 26px height so the scrim adds nothing to scrollHeight, and
     bottom:0 (not a negative offset) keeps it from extending the scroll
     region. A non-zero bottom offset here made the menu perpetually 6px
     scrollable, which pinned the fade over the last item forever. */
  margin-top: -26px;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--surface-1) 85%);
  border-radius: 0 0 var(--radius) var(--radius);
  opacity: 0;
  transition: opacity 0.12s;
}

#menu-dropdown.can-scroll-down::after {
  opacity: 1;
}

@keyframes menu-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  padding: 9px 12px;
  background: none;
  color: var(--text);
  border: none;
  border-radius: var(--radius-sm);
  text-align: left;
  font-size: 14px;
  cursor: pointer;
}

.menu-item svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  fill: none;
  stroke: var(--text-faint);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.menu-item:hover {
  background: var(--surface-3);
}

.menu-item:hover svg {
  stroke: var(--accent);
}

.menu-item-muted {
  color: var(--text-dim);
  font-size: 13px;
  padding: 7px 12px;
}

.menu-heading {
  padding: 8px 12px 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.menu-sep {
  border: none;
  border-top: 1px solid var(--border);
  margin: 6px 4px;
}

/* --- Playback controls (only while actively playing) --- */
#playback-controls {
  display: none;
  gap: 6px;
  flex: 0 0 auto;
}

body.running #playback-controls {
  display: flex;
}

/* Main Menu item only appears while a game is being played */
#main-menu,
#main-menu-sep {
  display: none;
}

body.running #main-menu {
  display: flex;
}

body.running #main-menu-sep {
  display: block;
}

/* Save actions + screenshot only make sense once a game is loaded */
#save-state,
#load-state,
#manage-saves,
#screenshot,
#save-sep {
  display: none;
}

body.has-game #save-state,
body.has-game #load-state,
body.has-game #manage-saves,
body.has-game #screenshot {
  display: flex;
}

body.has-game #save-sep {
  display: block;
}

/* On phones the in-menu volume row sits just above this sep and is always
   present, so the sep shows even without a game */
@media (max-width: 560px) {
  #save-sep {
    display: block;
  }
}

/* Save State | Load State share one half-width row */
.menu-split {
  display: flex;
  gap: 4px;
}

.menu-split .menu-item {
  flex: 1 1 0;
  min-width: 0;
  width: auto;
}

#pause .ico-play,
#pause.paused .ico-pause {
  display: none;
}

#pause.paused .ico-play {
  display: block;
}

/* --- Status readout --- */
#status {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-dim);
  min-width: 0;
}

#fps:empty {
  display: none;
}

/* The status readout is only meaningful once a game is running */
#status {
  display: none;
}

body.running #status {
  display: inline-flex;
}

/* Declutter the top bar on phones: the top-bar volume slider gives way to the
   mute toggle; a full slider lives in the menu instead. (Home-screen mobile
   compaction lives in its own block after the home rules — see end of file.) */
@media (max-width: 560px) {
  #vol-slider {
    display: none;
  }
}

/* ...and the in-menu volume slider is only for phones (top bar has one on
   wider screens), so it doesn't duplicate the top-bar slider on desktop. */
@media (min-width: 561px) {
  #menu-volume {
    display: none;
  }
}

/* --- Enhanced-audio (sound-engine HLE) indicator --- */
#hle-indicator {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 36px;
  color: var(--accent);
  /* Fade/scale in when engaged so it never pops abruptly */
  opacity: 1;
  transition: opacity 0.35s ease, transform 0.35s ease;
}

#hle-indicator[hidden] {
  display: none;
}

/* Staged reveal: JS unhides first, then adds .on for the transition */
#hle-indicator:not(.on) {
  opacity: 0;
  transform: scale(0.7);
}

#hle-indicator svg {
  width: 18px;
  height: 18px;
}

#hle-indicator svg path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

#hle-indicator svg circle {
  fill: currentColor;
  stroke: none;
}

/* Gentle glow so it reads as "lit", echoing the running-status LED */
#hle-indicator.on svg {
  filter: drop-shadow(0 0 3px var(--accent-glow));
}

/* --- Volume --- */
#volume-control {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}

#mute-btn .ico-mute {
  display: none;
}

#mute-btn.muted .ico-vol {
  display: none;
}

#mute-btn.muted .ico-mute {
  display: block;
  color: var(--danger);
}

.vol-range {
  -webkit-appearance: none;
  appearance: none;
  height: 5px;
  border-radius: 3px;
  background: linear-gradient(
    90deg,
    var(--accent) 0%,
    var(--accent) var(--vol, 100%),
    var(--surface-3) var(--vol, 100%),
    var(--surface-3) 100%
  );
  cursor: pointer;
  outline: none;
}

#vol-slider {
  width: 92px;
}

.vol-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: rgb(var(--hi-rgb));
  border: none;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  cursor: pointer;
}

.vol-range::-moz-range-thumb {
  width: 15px;
  height: 15px;
  border: none;
  border-radius: 50%;
  background: rgb(var(--hi-rgb));
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  cursor: pointer;
}

.vol-range.muted {
  filter: grayscale(1) brightness(0.7);
}

/* Volume row inside the menu dropdown (primary volume control on phones) */
.menu-volume {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 10px 12px 12px;
}

.menu-volume svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  fill: none;
  stroke: var(--text-faint);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.menu-volume .vol-range {
  flex: 1 1 auto;
}

/* --- Update badge --- */
#update-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: 0 0 auto;
  padding: 6px 12px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--accent-2);
  background: rgba(var(--accent-rgb), 0.08);
  border: 1px solid rgba(var(--accent-rgb), 0.32);
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

#update-btn[hidden] {
  display: none;
}

#update-btn:hover {
  background: rgba(var(--accent-rgb), 0.16);
  border-color: rgba(var(--accent-rgb), 0.55);
}

#update-btn .update-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent);
  animation: pulse 1.8s ease-in-out infinite;
}

@keyframes pulse {
  50% {
    opacity: 0.35;
  }
}

/* Fullscreen icon toggle */
#fullscreen-btn .ico-collapse {
  display: none;
}

body.fs #fullscreen-btn .ico-expand {
  display: none;
}

body.fs #fullscreen-btn .ico-collapse {
  display: block;
}

/*
 * ==========================================================
 * Stage
 * ==========================================================
 */

#stage {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Flat: the old static radial gradient read as a permanent "glow" behind
     the game frame even with Ambient glow off. The glow feature paints its
     own canvas when enabled, so the surround stays flat here. */
  background: var(--stage);
  overflow: hidden;
}

/* Subtle scanline atmosphere behind the screen */
#stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    0deg,
    rgba(var(--hi-rgb), 0.015) 0 1px,
    transparent 1px 3px
  );
  pointer-events: none;
  z-index: 0;
}

/* The CSS box must always be the same shape as the canvas backing store or
   the picture distorts (GB is 10:9, GBA is 3:2, and the wasm resizes the
   backing per system on ROM load). JS pins --game-ar to the LIVE backing
   ratio (updateCanvasScaling), so the two can never disagree — even if a
   cached frontend meets a newer wasm build, the worst case is letterboxing
   inside the canvas, never a stretch. */
#canvas {
  position: relative;
  z-index: 1;
  width: min(100%, calc((100dvh - var(--bar-h)) * var(--game-ar, 1.5)));
  max-width: 100%;
  max-height: 100%;
  aspect-ratio: var(--game-ar, 1.5);
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  background: var(--screen-off);
  display: none;
}

body.running #canvas {
  display: block;
  border-radius: 4px;
  /* Just a crisp 1px edge: the old 18px/50px drop shadow + accent glow read
     as a permanent gradient behind the frame even with Ambient glow off. The
     glow feature paints its own canvas, so the frame sits flat on the
     background here. */
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6);
}

/* Ambient glow backdrop: a coarse sample of the frame, inflated past the
   canvas edges and heavily blurred. JS pins it to the canvas rect and feeds
   it ~10 samples/second with temporal smoothing. */
#glow-canvas {
  position: absolute;
  z-index: 0;
  pointer-events: none;
  transform: scale(1.45);
  /* Blur only — saturation is baked into the sampled bitmap (updateGlow), so
     the compositor runs a single filter pass. The source is a 24x16 image blown
     up ~60x per cell, so a smaller radius still blends the cells smoothly at a
     fraction of the large-radius blur's GPU cost. */
  filter: blur(32px);
  opacity: 0.8;
  border-radius: 28px;
  /* The sampled bitmap is opaque edge-to-edge, so blur() alone yields a blurred
     rectangle. A radial mask feathers the halo to nothing before its outer edge
     so it reads as a soft elliptical glow rather than a hard box. The scale is
     bumped a touch to compensate for the halo the mask trims away. */
  -webkit-mask-image: radial-gradient(ellipse at center, black 40%, transparent 78%);
  mask-image: radial-gradient(ellipse at center, black 40%, transparent 78%);
}

/* Scanlines are now drawn by the WebGL2 fragment shader (uniform-gated), not a
   DOM overlay — the old #scanline-overlay element and its sizing are gone. */

/* MBC5 rumble: a small transform-only jitter on the game screen while the
   cart's motor runs (body.rumbling, set by the RAF loop). Width/height are
   deliberately untouched — integer scaling pins those. */
body.rumbling #canvas {
  animation: rumble-shake 60ms linear infinite;
}

@keyframes rumble-shake {
  0%   { transform: translate(1.2px, -0.8px); }
  25%  { transform: translate(-1.2px, 0.8px); }
  50%  { transform: translate(0.8px, 1.2px); }
  75%  { transform: translate(-0.8px, -1.2px); }
  100% { transform: translate(1.2px, -0.8px); }
}

/*
 * ==========================================================
 * Home / empty state
 * ==========================================================
 */

#home {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  padding: 24px;
  overflow-y: auto;
  /* Home stays dark even when a device theme paints the stage the shell
     colour (--home-bg defaults to transparent for non-device themes). */
  background: var(--home-bg);
}

body.running #home {
  display: none;
}

/* margin:auto (not align/justify center) centers the block yet still lets the
   top scroll into view when the content is taller than a short viewport, so
   the logo/wordmark can never be clipped. */
#home-inner {
  width: 100%;
  max-width: 480px;
  margin: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 26px;
  animation: home-in 0.4s ease-out both;
}

@keyframes home-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
}

/* Resume button — only when a game is loaded (returning from Main Menu) */
#home-resume {
  display: none;
  align-items: center;
  gap: 9px;
  padding: 11px 24px;
  font-size: 15px;
  font-weight: 600;
  color: var(--accent-ink);
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
  border: 1px solid rgba(var(--accent-rgb), 0.6);
  border-radius: 999px;
  box-shadow: inset 0 1px 0 rgba(var(--hi-rgb), 0.35),
    0 8px 24px -8px var(--accent-glow);
  cursor: pointer;
  transition: transform 0.06s, filter 0.13s;
}

#home-resume svg {
  width: 18px;
  height: 18px;
}

#home-resume:hover {
  filter: brightness(1.06);
}

#home-resume:active {
  transform: translateY(1px);
}

body.has-game #home-resume {
  display: inline-flex;
}

/* Wrapper around Resume + the mobile-only "Load a game" button. On desktop it
   is transparent to layout (display: contents) and the mobile button doesn't
   render, so the home column is pixel-identical to before. The touch layout
   lives in the (pointer: coarse) home block near the mobile compaction rules. */
/* Load a game / Resume row. On every viewport the home screen shows a simple
   "Load a game" button rather than a big drop target — drag-and-drop still
   works document-wide (see #drop-overlay), and this keeps the recents grid on
   screen instead of pushing it below the fold. */
#home-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  width: 100%;
}

#home-load {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 11px 24px;
  font-size: 15px;
  border-radius: 999px;
}

/* When a game is paused, Resume is the primary action, so Load drops to the
   neutral (secondary) button look. */
body.has-game #home-load {
  color: var(--text);
  background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
  border-color: var(--border-2);
  box-shadow: inset 0 1px 0 var(--inset-hi);
}
body.has-game #home-load:hover {
  background: linear-gradient(180deg, var(--surface-hover), var(--surface-3));
}

/* Paused-game card — frozen frame of the game Resume returns to. Shown only
   while a game is paused at the home screen (JS toggles [hidden]). */
#home-paused {
  position: relative;
}

#home-paused-shot {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px;
  background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color 0.14s, background 0.14s, transform 0.06s;
}

#home-paused-shot:hover {
  border-color: var(--border-2);
  background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
}

#home-paused-shot:active {
  transform: translateY(1px);
}

#home-paused-canvas {
  display: block;
  width: 192px;
  height: auto;
  border-radius: 5px;
  background: var(--screen-off);
  image-rendering: pixelated;
}

.home-paused-caption {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 192px;
}

#home-paused-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
  text-align: left;
  color: var(--text);
}

.home-paused-hint {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.03em;
  padding: 2px 5px;
  border-radius: 4px;
  color: var(--accent);
  background: rgba(var(--accent-rgb), 0.14);
}

/* Close ("unload") — ends the paused session; all stored data is kept */
#home-paused-close {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-size: 16px;
  line-height: 1;
  color: var(--text-dim);
  background: var(--paused-close-bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  transition: color 0.12s, background 0.12s, border-color 0.12s;
}

#home-paused-close:hover {
  color: var(--danger);
  border-color: rgba(var(--danger-rgb), 0.4);
  background: var(--paused-close-bg-hover);
}

#home-brand {
  text-align: center;
}

#home-logo {
  width: 76px;
  height: auto;
  image-rendering: pixelated;
  filter: drop-shadow(0 4px 14px rgba(0, 0, 0, 0.6));
  margin-bottom: 14px;
}

#home-brand h1 {
  margin: 0;
  font-size: 30px;
  font-weight: 700;
  letter-spacing: -0.01em;
}

#home-tagline {
  margin: 6px 0 0;
  font-size: 13.5px;
  color: var(--text-faint);
  letter-spacing: 0.01em;
}

#home-recent-wrap {
  width: 100%;
}

#home-recent-wrap[hidden] {
  display: none;
}

.home-recent-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 10px;
}

/* display:flex above would otherwise defeat the [hidden] attribute the empty
   library state sets on the header. */
.home-recent-head[hidden] {
  display: none;
}

.home-recent-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.home-recent-meta {
  display: flex;
  align-items: baseline;
  gap: 12px;
}

#storage-info {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-faint);
}

.home-manage-link {
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 11px;
  color: var(--text-faint);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.home-manage-link:hover {
  color: var(--text);
}

#home-recent {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 8px;
}

/* Empty-library placeholder (no games yet) — hosts the Drive sign-in entry.
   #home-recent is an auto-fill grid, so span every column and center the box
   with margin auto rather than letting it sit in the first 210px track. */
.home-empty {
  grid-column: 1 / -1;
  justify-self: center;
  width: 100%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 24px 16px;
  text-align: center;
  border: 1px dashed var(--border-2);
  border-radius: var(--radius-sm);
}
.home-empty-msg {
  margin: 0;
  font-size: 13px;
  color: var(--text-dim);
}
.home-empty-sub {
  margin: 6px 0 2px;
  font-size: 11.5px;
  color: var(--text-faint);
}

.home-tile {
  position: relative;
  border-radius: var(--radius-sm);
  /* Grid items default to min-width auto, so a long nowrap ROM name would
     inflate the 1fr track past the viewport (seen on phones); this lets the
     track stay put and the name's ellipsis do its job */
  min-width: 0;
}

.home-tile-launch {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 10px 34px 10px 12px;
  text-align: left;
  color: var(--text);
  background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color 0.14s, background 0.14s, transform 0.06s;
}

.home-tile-launch:hover {
  border-color: var(--border-2);
  background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
}

.home-tile-launch:active {
  transform: translateY(1px);
}

.home-tile-art {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  object-fit: cover;
  border-radius: 4px;
  image-rendering: auto;
  box-shadow: inset 0 0 0 1px rgba(var(--hi-rgb), 0.12);
}

.home-tile-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
}

.badge-gba {
  color: var(--badge-gba-fg);
  background: var(--badge-gba-bg);
}
.badge-gb {
  color: var(--badge-gb-fg);
  background: var(--badge-gb-bg);
}
.badge-gbc {
  color: var(--badge-gbc-fg);
  background: var(--badge-gbc-bg);
}

.home-tile-link {
  display: none;
}
body.debug-2p .home-tile-link {
  position: absolute;
  top: 50%;
  right: 32px;
  transform: translateY(-50%);
  height: 22px;
  padding: 0 7px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--text-faint);
  background: none;
  border: 1px solid transparent;
  border-radius: 5px;
  cursor: pointer;
  opacity: 0.5;
  transition: opacity 0.12s, color 0.12s, background 0.12s, border-color 0.12s;
}
.home-tile:hover .home-tile-link {
  opacity: 1;
}
.home-tile-link:hover {
  color: var(--accent);
  /* rgba lines: pre-Safari-16.2 fallback, overridden where color-mix parses */
  background: rgba(var(--accent-rgb), 0.14);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-color: rgba(var(--accent-rgb), 0.4);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}

/*
 * ==========================================================
 * Drop overlay
 * ==========================================================
 */

#drop-overlay {
  position: absolute;
  inset: 0;
  z-index: 300;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--drop-overlay-bg);
  backdrop-filter: blur(2px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
}

#drop-overlay.visible {
  opacity: 1;
}

#drop-overlay-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 46px 64px;
  font-size: 19px;
  font-weight: 600;
  color: var(--accent-2);
  border: 2px dashed var(--accent);
  border-radius: var(--radius-lg);
}

#drop-overlay-inner svg {
  width: 40px;
  height: 40px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/*
 * ==========================================================
 * Log (developer)
 * ==========================================================
 */

#log {
  position: absolute;
  /* Below the topbar (50) so the menu dropdown and toasts (300+) stay usable
     while the log is open; above stage internals and net indicators (<=41) */
  z-index: 45;
  height: 250px;
  width: 100%;
  top: 0;
  left: 0;
  overflow: auto;
  padding: 8px 12px;
  margin: 0;
  line-height: 1.4;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--log-text);
  background: rgba(var(--stage-rgb), 0.92);
  border-bottom: 1px solid var(--border);
}

#log p {
  margin: 0 0 2px;
  overflow-wrap: anywhere;
}

#log-toolbar {
  position: sticky;
  top: -8px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  margin: -8px 0 4px;
  background: rgba(var(--stage-rgb), 0.96);
  border-bottom: 1px solid var(--border);
}

#log-context {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 10px;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}

#log-toolbar button {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 11px;
  padding: 3px 10px;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.log-error {
  color: var(--log-error);
}

.log-warn {
  color: var(--accent);
}

/* --- Toast (brief feedback for save-state actions) --- */

#toast {
  position: absolute;
  /* Above .modal-overlay (500): toasts fired while a modal is open (Quick
     Save's confirmation, sync notices) must not render dimmed behind the
     overlay's backdrop. */
  z-index: 600;
  left: 50%;
  bottom: 20px;
  transform: translate(-50%, 8px);
  max-width: min(80%, 420px);
  padding: 8px 16px;
  border-radius: 999px;
  background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
  border: 1px solid var(--border-2);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 1px 0 var(--inset-hi) inset;
  color: var(--text);
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
}

#toast.show {
  opacity: 1;
  transform: translate(-50%, 0);
}

/*
 * ==========================================================
 * Buttons (shared)
 * ==========================================================
 */

.button {
  padding: 8px 16px;
  font-size: 13.5px;
  color: var(--text);
  background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
  border: 1px solid var(--border-2);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 1px 0 var(--inset-hi);
  cursor: pointer;
  transition: background 0.13s, border-color 0.13s, transform 0.06s;
}

.button:hover {
  background: linear-gradient(180deg, var(--surface-hover), var(--surface-3));
}

.button:active {
  transform: translateY(1px);
}

.button-sm {
  padding: 5px 11px;
  font-size: 12.5px;
}

.button-primary {
  color: var(--accent-ink);
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
  border-color: rgba(var(--accent-rgb), 0.6);
  font-weight: 600;
  box-shadow: inset 0 1px 0 rgba(var(--hi-rgb), 0.35),
    0 2px 12px -4px var(--accent-glow);
}

.button-primary:hover {
  background: linear-gradient(180deg, var(--accent-hi), var(--accent-2));
}

.button-ghost {
  background: transparent;
  border-color: var(--border);
  color: var(--text-dim);
}

.button-ghost:hover {
  background: rgba(var(--hi-rgb), 0.04);
  color: var(--text);
}

/*
 * ==========================================================
 * Modals
 * ==========================================================
 */

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 500;
  background: rgba(var(--stage-rgb), 0.66);
  backdrop-filter: blur(3px);
  display: none;
  align-items: center;
  justify-content: center;
  /* Keep modals out of the notch / home-indicator regions (viewport-fit=cover
     extends the layout viewport under them, esp. installed-to-home-screen). */
  padding: calc(20px + var(--safe-t)) calc(20px + var(--safe-r))
           calc(20px + var(--safe-b)) calc(20px + var(--safe-l));
}

.modal-overlay.open {
  display: flex;
  animation: overlay-in 0.15s ease-out;
}

@keyframes overlay-in {
  from {
    opacity: 0;
  }
}

.modal {
  position: relative;
  width: 100%;
  max-width: 400px;
  /* Subtract the safe-area padding the overlay adds, so a full-height modal
     still fits between the notch and the home indicator. */
  max-height: calc(100dvh - 40px - var(--safe-t) - var(--safe-b));
  overflow-y: auto;
  padding: 22px 24px;
  background: var(--surface-1);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6), inset 0 1px 0 var(--inset-hi);
  animation: modal-in 0.16s cubic-bezier(0.2, 0.8, 0.3, 1);
}

@keyframes modal-in {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.98);
  }
}

.modal h3 {
  margin: 0 0 16px;
  font-size: 17px;
  font-weight: 700;
}

.modal p {
  margin: 0 0 8px;
  color: var(--text-dim);
  line-height: 1.5;
}

.modal-hint {
  font-size: 12.5px;
  margin: -6px 0 14px;
}

/* Toggle row + subhead (used by the Controls modal) */
.modal-toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 18px;
}

.modal-toggle-sub {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--text-faint);
}

/* ---- Save States modal ---- */
.modal-states {
  max-width: 460px;
}

.states-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin: 14px 0 18px;
}

.state-slot {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px;
  background: var(--surface-2);
  border: 2px solid var(--border-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-align: left;
  color: inherit;
  transition: border-color 0.12s, background 0.12s;
}

.state-slot:hover {
  border-color: rgba(var(--accent-rgb), 0.55);
}

.state-slot.selected {
  border-color: var(--accent);
  background: rgba(var(--accent-rgb), 0.14);
}

.state-slot .slot-thumb {
  width: 100%;
  aspect-ratio: 3 / 2;
  border-radius: 3px;
  background: var(--surface-3);
  object-fit: cover;
  image-rendering: pixelated;
  display: block;
}

/* Empty slots show a dashed placeholder in place of a screenshot. */
.state-slot.empty .slot-thumb {
  border: 1px dashed var(--border-2);
  background: transparent;
}

.state-slot .slot-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 6px;
  font-size: 11px;
  color: var(--text-dim);
}

/* One line, always: "1 · Quick" must never wrap under a long timestamp —
   the slot number stays whole and the time ellipsizes instead. */
.state-slot .slot-label > span {
  white-space: nowrap;
}

.state-slot .slot-label > span:last-child {
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

.state-slot .slot-num {
  font-weight: 700;
  color: var(--text);
  flex-shrink: 0;
}

.states-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

/* Delete on the left, Load/Save grouped on the right. */
.states-actions-split {
  justify-content: space-between;
}

.states-actions-right {
  display: flex;
  gap: 10px;
}

.button-danger {
  color: #ff6b6b;
  border-color: rgba(255, 107, 107, 0.4);
}

.button-danger:hover:not(:disabled) {
  color: #fff;
  background: rgba(255, 80, 80, 0.85);
  border-color: rgba(255, 80, 80, 0.9);
}

/* ---- Report a Bug modal ---- */
.modal-report {
  max-width: 440px;
}

.report-label {
  display: block;
  margin: 12px 0 5px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-dim);
}

.report-textarea {
  width: 100%;
  resize: vertical;
  min-height: 68px;
  font: inherit;
  padding: 8px 10px;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-sm);
}

.report-scrub {
  margin-top: 16px;
  padding: 12px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-sm);
}

.report-scrub.disabled {
  opacity: 0.55;
}

.report-scrub-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}

.report-when {
  font-size: 12px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.report-preview {
  display: block;
  width: 240px;
  max-width: 100%;
  aspect-ratio: 3 / 2;
  margin: 8px auto;
  border-radius: 4px;
  background: var(--surface-3);
  image-rendering: pixelated;
}

.report-slider {
  width: 100%;
  margin: 4px 0 2px;
  accent-color: var(--accent);
}

.report-scrub-hint {
  margin: 6px 0 0;
}

/* A setting suspended by another (e.g. integer scaling / scanlines while an
   upscale filter is active): visibly inert, state preserved. */
.modal-toggle-row.suspended {
  opacity: 0.45;
}
.modal-toggle-row.suspended .switch {
  cursor: default;
}

/* display:block above would otherwise defeat the hidden attribute */
.modal-toggle-sub[hidden] {
  display: none;
}

.modal-subhead {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 12px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

/* Toggle switch */
.switch {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
  cursor: pointer;
}

.switch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.switch-track {
  width: 42px;
  height: 24px;
  border-radius: 999px;
  background: var(--surface-3);
  border: 1px solid var(--border-2);
  position: relative;
  transition: background 0.15s, border-color 0.15s;
}

.switch-track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: rgb(var(--hi-rgb));
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
  transition: transform 0.16s cubic-bezier(0.2, 0.8, 0.3, 1);
}

.switch input:checked + .switch-track {
  background: linear-gradient(180deg, var(--accent-2), var(--accent));
  border-color: rgba(var(--accent-rgb), 0.6);
}

.switch input:checked + .switch-track::after {
  transform: translateX(18px);
}

.switch input:focus-visible + .switch-track {
  box-shadow: 0 0 0 3px var(--accent-glow);
}

/* Keyboard rebinding is meaningless on touch devices — show only the toggle */
@media (pointer: coarse) {
  #kb-section {
    display: none;
  }
}

/* --- Settings modal (tabbed) --- */
.modal-settings {
  max-width: 460px;
}

/* Wrapper exists to host the non-scrolling edge scrims — the bar itself
   scrolls, so gradients on it would scroll along with the content */
#settings-tabs-wrap {
  position: relative;
  margin-bottom: 18px;
}

/* Edge scrims: shown (via JS class) only while the bar can scroll that way.
   Two layers each: an opaque fade to the strip surface (clipped tab text
   visibly dissolves — the shadow alone had near-zero contrast on the dark
   themes and read as a hard clip), over the --inset-lo shadow that keeps the
   edge marked even when the cutoff lands exactly on the gap between tabs. */
#settings-tabs-wrap::before,
#settings-tabs-wrap::after {
  content: "";
  position: absolute;
  top: 1px;
  bottom: 1px;
  width: 28px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
  z-index: 1;
}

#settings-tabs-wrap::before {
  left: 1px;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  background:
    linear-gradient(to right, var(--surface-2) 20%, rgba(0, 0, 0, 0)),
    linear-gradient(to right, var(--inset-lo), rgba(0, 0, 0, 0));
}

#settings-tabs-wrap::after {
  right: 1px;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background:
    linear-gradient(to left, var(--surface-2) 20%, rgba(0, 0, 0, 0)),
    linear-gradient(to left, var(--inset-lo), rgba(0, 0, 0, 0));
}

#settings-tabs-wrap.can-scroll-left::before,
#settings-tabs-wrap.can-scroll-right::after {
  opacity: 1;
}

#settings-tabs {
  display: flex;
  gap: 4px;
  padding: 3px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  /* More tabs than fit on a narrow phone: scroll horizontally, no scrollbar
     (a tab clipped at the edge is the scroll affordance) */
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* When scrolling a tab into view, stop short of the edge so a sliver of
     the neighboring tab stays visible as a "there's more" hint */
  scroll-padding: 0 28px;
}

#settings-tabs::-webkit-scrollbar {
  display: none;
}

.settings-tab {
  /* Grow to fill the bar when there's room, but never shrink below the
     label — overflow scrolls instead */
  flex: 1 0 auto;
  padding: 7px 4px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-dim);
  background: none;
  border: none;
  border-radius: calc(var(--radius-sm) - 2px);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.13s, color 0.13s;
}

.settings-tab:hover {
  color: var(--text);
}

.settings-tab.active {
  color: var(--accent);
  background: var(--surface-1);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35), inset 0 1px 0 var(--inset-hi);
}

/* A stable height keeps the dialog from jumping between tabs */
.settings-pane {
  min-height: 264px;
}

/* The first section label in a pane needs no separator — the tab bar is one */
.settings-pane .modal-subhead:first-child,
.modal-subhead.no-rule {
  border-top: none;
  padding-top: 0;
}

.settings-note {
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text-faint);
}

/* "Reset all settings" row at the bottom of the General tab */
.settings-reset-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.settings-reset-row .modal-toggle-sub {
  display: block;
  margin-top: 2px;
}

.reset-settings-btn {
  flex: 0 0 auto;
  white-space: nowrap;
}

/* Second tap arms the button before it wipes settings (matches the delete
   lists' two-step confirm styling). */
.reset-settings-btn.armed {
  color: var(--accent-ink);
  background: linear-gradient(180deg, var(--danger-hi), var(--danger));
  border-color: rgba(var(--danger-rgb), 0.6);
  font-weight: 600;
}

/* Build identity at the bottom of the settings modal ("dingbat <commit>") */
.settings-version {
  margin: 14px 0 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-faint);
  text-align: right;
}

/* Radio option rows (renderer / BIOS mode) */
.radio-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 14px;
}

.radio-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 13.5px;
}

.radio-row:hover {
  background: var(--surface-2);
}

.radio-row:has(input:checked) {
  border-color: rgba(var(--accent-rgb), 0.35);
  background: rgba(var(--accent-rgb), 0.05);
}

.radio-row input {
  margin: 3px 0 0;
  accent-color: var(--accent);
  flex: 0 0 auto;
}

.radio-sub {
  display: block;
  margin-top: 1px;
  font-size: 12px;
  color: var(--text-faint);
}

/* App theme picker (Video tab): one chip per chrome theme, with a
   swatch showing that theme's background + accent. Swatch colors are
   fixed per chip (they preview a theme, they don't follow the active
   one), so they're set below rather than derived from tokens. */
#theme-picker {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 6px;
}

.theme-chip,
.choice-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-dim);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color 0.13s, background 0.13s, color 0.13s;
}

.theme-chip:hover,
.choice-chip:hover {
  color: var(--text);
  background: var(--surface-2);
}

.theme-chip.selected,
.choice-chip.selected {
  color: var(--text);
  border-color: rgba(var(--accent-rgb), 0.55);
  background: rgba(var(--accent-rgb), 0.07);
}

/* Small chip radiogroups (touch input style / joystick behavior) */
.chip-row {
  display: flex;
  gap: 6px;
  flex: 0 0 auto;
}

.chip-row .choice-chip {
  justify-content: center;
  min-width: 74px;
}

/* display:flex above would otherwise defeat hiding the joystick-mode row */
.modal-toggle-row.hidden {
  display: none;
}

.theme-swatch {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  position: relative;
  background: var(--swatch-bg);
  border: 1px solid var(--border-2);
}

.theme-swatch::after {
  content: "";
  position: absolute;
  right: 3px;
  bottom: 3px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--swatch-accent);
}

.theme-chip[data-theme-name="amber"] .theme-swatch {
  --swatch-bg: #0c0e16;
  --swatch-accent: #ffb04d;
}
.theme-chip[data-theme-name="black"] .theme-swatch {
  --swatch-bg: #000000;
  --swatch-accent: #ffb04d;
}
.theme-chip[data-theme-name="light"] .theme-swatch {
  --swatch-bg: #e2e4ea;
  --swatch-accent: #9c5400;
}
.theme-chip[data-theme-name="indigo"] .theme-swatch {
  --swatch-bg: #675ba0;
  --swatch-accent: #7f6ae7;
}
.theme-chip[data-theme-name="fuchsia"] .theme-swatch {
  --swatch-bg: #af748c;
  --swatch-accent: #e8739a;
}
.theme-chip[data-theme-name="glacier"] .theme-swatch {
  --swatch-bg: #999db6;
  --swatch-accent: #769be5;
}
.theme-chip[data-theme-name="kiwi"] .theme-swatch {
  --swatch-bg: #6ee126;
  --swatch-accent: #65db4d;
}
.theme-chip[data-theme-name="dmg"] .theme-swatch {
  --swatch-bg: #c2bcb9;
  --swatch-accent: #9cc954;
}
.theme-chip[data-theme-name="atomic-purple"] .theme-swatch {
  --swatch-bg: #9769a9;
  --swatch-accent: #c36ee7;
}
.theme-chip[data-theme-name="daiei"] .theme-swatch {
  --swatch-bg: #c76637;
  --swatch-accent: #eb7c33;
}
.theme-chip[data-theme-name="famicom"] .theme-swatch {
  --swatch-bg: #b99c68;
  --swatch-accent: #e0635c;
}

.modal-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

.modal-row-label {
  min-width: 92px;
  font-weight: 500;
}

.modal-row-status {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-faint);
}

/* "Reset save file" — a two-step confirm button that arms red before it wipes
   the current game's save (matches the settings-reset button's confirm look). */
.saves-reset-btn {
  flex: 0 0 auto;
  white-space: nowrap;
}

.saves-reset-btn.armed {
  color: var(--accent-ink);
  background: linear-gradient(180deg, var(--danger-hi), var(--danger));
  border-color: rgba(var(--danger-rgb), 0.6);
  font-weight: 600;
}

/* Manage ROMs and Saves modal — one row per stored game, two actions each */
.roms-manage-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  overflow-y: auto;
}

/* Fit the whole modal within the screen with the ROM list as the ONLY scroll
   region, so the header and the Drive sign-in stay visible. Previously the
   list had its own 60vh scroller AND the modal scrolled, so on a phone you
   scrolled the list, then the modal itself scrolled to reveal Drive. Now it's
   a flex column against the shared .modal max-height: the list flexes into the
   leftover space (min-height:0 lets it shrink below its content and scroll),
   while everything above and the Drive section below hold their place. */
#roms-modal .modal {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

#roms-modal .roms-manage-list {
  flex: 1 1 auto;
  min-height: 0;
}

.roms-manage-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px 12px;
  padding: 8px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.roms-manage-name {
  flex: 1 1 140px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
}

.roms-manage-actions {
  flex: 0 0 auto;
  display: flex;
  gap: 8px;
}

.roms-manage-btn {
  flex: 0 0 auto;
}

.roms-manage-btn.armed {
  color: var(--accent-ink);
  background: linear-gradient(180deg, var(--danger-hi), var(--danger));
  border-color: rgba(var(--danger-rgb), 0.6);
  font-weight: 600;
}

.roms-manage-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

.modal-actions {
  display: flex;
  gap: 9px;
  justify-content: flex-end;
  margin-top: 20px;
}

.modal-close {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: 6px;
  color: var(--text-faint);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}

.modal-close:hover {
  color: var(--text);
  background: rgba(var(--hi-rgb), 0.06);
}

/* Keyboard bindings */
.kb-select {
  padding: 6px 10px;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-sm);
  font-size: 13px;
  cursor: pointer;
}

#kb-bindings {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px 14px;
  margin-bottom: 4px;
}

.kb-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.kb-row span {
  min-width: 46px;
  font-size: 13px;
  color: var(--text-dim);
}

.kb-btn {
  flex: 1 1 auto;
  min-width: 0;
  padding: 7px 8px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color 0.12s, background 0.12s;
}

.kb-btn:hover {
  background: var(--surface-3);
}

.kb-btn.active {
  color: var(--accent);
  border-color: var(--accent);
  background: rgba(var(--accent-rgb), 0.08);
  box-shadow: 0 0 0 3px var(--accent-glow);
}

/* Shortcuts reference list (desktop-only: lives inside #kb-section) */
#kb-shortcuts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px 14px;
  margin-bottom: 14px;
}

.kb-shortcut {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: 13px;
  color: var(--text-dim);
}

.kb-shortcut kbd {
  padding: 2px 7px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

/*
 * ==========================================================
 * On-screen gamepad (touch)
 * ==========================================================
 */

#controls {
  flex: 0 0 auto;
  display: none;
  padding: 14px calc(16px + var(--safe-r)) calc(16px + var(--safe-b))
    calc(16px + var(--safe-l));
  /* Two optional pinstripes (--panel-stripe, transparent by default) run the
     full width just through the bottom of the d-pad / A-B cluster — the
     Famicom faceplate's double line. Lengths from the bottom clear the
     Select/Start row (34px) + bottom padding + gap. No border-top: the strip
     sits directly on the stage. */
  background:
    linear-gradient(var(--panel-stripe), var(--panel-stripe)) 0
      calc(100% - var(--safe-b) - 112px) / 100% 4px no-repeat,
    linear-gradient(var(--panel-stripe), var(--panel-stripe)) 0
      calc(100% - var(--safe-b) - 100px) / 100% 4px no-repeat,
    linear-gradient(180deg, var(--control-strip-top), var(--control-strip-bottom));
  gap: 14px;
  grid-template-rows: auto 1fr auto;
}

@media (pointer: coarse) {
  body.running #controls {
    display: grid;
  }
  /* "Hide touch controls with a controller" (default on): a connected gamepad
     supersedes the on-screen buttons. Higher specificity than every
     body.running #controls display rule, including the landscape blocks. */
  body.running.gamepad-hides-touch #controls {
    display: none;
  }
}

/* Tactile key base */
.pad-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--pad-label);
  font-size: 15px;
  font-weight: 600;
  background: linear-gradient(180deg, var(--pad-top), var(--pad-bottom));
  border: 1px solid var(--pad-border);
  box-shadow: inset 0 1px 0 var(--inset-hi), 0 2px 5px rgba(0, 0, 0, 0.4);
  transition: none;
  -webkit-tap-highlight-color: transparent;
}

.pad-btn.pressed,
.pad-btn.arm-active {
  color: var(--accent);
  background: linear-gradient(180deg, var(--accent-tint-top), var(--pad-pressed-bottom));
  border-color: rgba(var(--accent-rgb), 0.55);
  box-shadow: inset 0 2px 7px var(--inset-lo), 0 0 16px -3px var(--accent-glow);
}

/* L / R shoulders */
#lr {
  display: flex;
  gap: 14px;
}

.pad-shoulder {
  flex: 1 1 0;
  height: 46px;
  border-radius: 10px 10px 6px 6px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* Main cluster */
#main-controls {
  container-type: size;
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* Explicit height so d-pad/face buttons (sized via container units) have
     vertical room; the grid's 1fr row alone would collapse in an auto-height
     control strip. */
  height: min(54vw, 290px);
}

/* D-pad. Sizes are kept below half the row width so a central gutter always
   remains between the d-pad and the A/B cluster (no button collisions). */
#dpad {
  --dpad-size: min(46cqi, 240px, 100cqb);
  width: var(--dpad-size);
  aspect-ratio: 1 / 1;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  grid-template-rows: 1fr 1fr 1fr;
}

/* The d-pad keys can carry their own border (e.g. the Famicom's red outline
   tracing the cross), independent of the other pads. Defaults follow the
   shared pad border. */
#dpad > .pad-btn {
  border-color: var(--dpad-border, var(--pad-border));
  border-width: var(--dpad-border-w, 1px);
}

#dpad > #upleft {
  grid-area: 1 / 1;
}
#dpad > #up {
  grid-area: 1 / 2;
  border-radius: 9px 9px 0 0;
  border-bottom: none;
}
#dpad > #upright {
  grid-area: 1 / 3;
}
#dpad > #left {
  grid-area: 2 / 1;
  border-radius: 9px 0 0 9px;
  border-right: none;
}
#dpad > #dpad-center {
  grid-area: 2 / 2;
  background: linear-gradient(180deg, var(--pad-top), var(--pad-bottom));
  /* The horizontal bar's edge lines where it crosses the vertical bar. These
     are interior — the cross's outer outline is fully drawn by the four arm
     cells — so a theme with a bold d-pad outline (Famicom red) sets
     --dpad-bar-line: transparent to keep the cross unsegmented. */
  border-top: var(--dpad-border-w, 1px) solid
    var(--dpad-bar-line, var(--dpad-border, var(--pad-border)));
  border-bottom: var(--dpad-border-w, 1px) solid
    var(--dpad-bar-line, var(--dpad-border, var(--pad-border)));
  position: relative;
}
#dpad > #dpad-center::after {
  content: "";
  position: absolute;
  inset: 22%;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.25);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
}
#dpad > #right {
  grid-area: 2 / 3;
  border-radius: 0 9px 9px 0;
  border-left: none;
}
#dpad > #downleft {
  grid-area: 3 / 1;
}
#dpad > #down {
  grid-area: 3 / 2;
  border-radius: 0 0 9px 9px;
  border-top: none;
}
#dpad > #downright {
  grid-area: 3 / 3;
}

/*
 * Joystick (optional replacement for the d-pad, body.joystick-controls).
 * The #joystick element is the TOUCH REGION — the whole left half of the
 * control row, so floating mode has generous room to spawn the stick under
 * the thumb. The visible base ring is --dpad-size (sharing the d-pad's
 * sizing rules: the large-controls / landscape overrides list #joystick
 * next to #dpad) and sits centered in the region. Base and knob reuse the
 * .pad-btn vocabulary so the landscape-translucent, opaque-controls and
 * light-theme rules apply unchanged; JS moves them with element.style
 * transforms only (never layout).
 */
#joystick {
  --dpad-size: min(46cqi, 240px, 100cqb);
  position: relative;
  width: 50cqi;
  height: 100%;
  display: none;
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
}

body.joystick-controls #dpad {
  display: none;
}

body.joystick-controls #joystick {
  display: block;
}

#joystick-base {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--dpad-size);
  height: var(--dpad-size);
  margin: calc(var(--dpad-size) / -2) 0 0 calc(var(--dpad-size) / -2);
  border-radius: 50%;
}

/* Recessed well in the middle of the base, same trick as #dpad-center::after */
#joystick-base::after {
  content: "";
  position: absolute;
  inset: 18%;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.25);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
}

#joystick-knob {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44%;
  height: 44%;
  margin: -22% 0 0 -22%;
  border-radius: 50%;
  z-index: 1;
}

/* Directional feedback: a short accent arc on the rim, rotated by JS to the
   quantized direction currently being sent (0deg = up). */
#joystick-rim {
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  border: 3px solid transparent;
  border-top-color: rgba(var(--accent-rgb), 0.85);
  opacity: 0;
  transition: opacity 0.1s;
  filter: drop-shadow(0 0 5px var(--accent-glow));
  pointer-events: none;
}

#joystick.active #joystick-rim {
  opacity: 1;
}

/* Snap-home animation on release (class added just for the return trip so
   live tracking stays transition-free) */
#joystick-base.homing,
#joystick-knob.homing {
  transition: transform 0.16s ease-out;
}

/* A / B face buttons */
#ab {
  --btn-size: min(calc(46cqi / 2.7), 84px, calc(100cqb / 2.7));
  width: calc(var(--btn-size) * 2.2);
  height: min(46cqi, 240px, 100cqb);
  position: relative;
}

.pad-face {
  width: var(--btn-size);
  height: var(--btn-size);
  border-radius: 50%;
  position: absolute;
  font-size: calc(var(--btn-size) * 0.32);
  /* A/B carry their own device color (e.g. DMG magenta, Famicom red);
     defaults to the shared pad look via --btn-ab-* → --pad-*. The pressed
     state keeps winning through .pad-btn.pressed's higher specificity. */
  color: var(--btn-ab-label);
  background: linear-gradient(180deg, var(--btn-ab-top), var(--btn-ab-bottom));
  border-color: var(--btn-ab-border);
  border-width: var(--btn-ab-border-w);
}

#a {
  top: 14%;
  right: 0;
}

#b {
  bottom: 14%;
  left: 0;
}

/* Select / Start */
#select-start {
  display: flex;
  justify-content: center;
  gap: 18px;
}

.pad-pill {
  flex: 0 1 150px;
  height: 34px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  /* Select/Start can differ from the d-pad (e.g. DMG's grey pills); defaults
     to the shared pad look via --pill-* → --pad-*. */
  color: var(--pill-label);
  background: linear-gradient(180deg, var(--pill-top), var(--pill-bottom));
}

/*
 * Large controls mode: maximise the d-pad for big fingers. The d-pad grows
 * past half the width while A/B, L/R and Select/Start give up room.
 */
body.large-controls #main-controls {
  height: min(64vw, 330px);
}

body.large-controls #dpad,
body.large-controls #joystick {
  --dpad-size: min(62cqi, 330px, 100cqb);
}

body.large-controls #ab {
  --btn-size: min(calc(42cqi / 2.8), 74px, calc(100cqb / 2.8));
  width: calc(var(--btn-size) * 2.05);
}

body.large-controls .pad-shoulder {
  height: 38px;
}

body.large-controls .pad-pill {
  height: 28px;
  font-size: 10px;
  flex-basis: 120px;
}

/*
 * Landscape touch devices (phones AND tablets): flank the screen with
 * controls instead of stacking. The top bar hosts the screen; controls
 * become side rails via fixed pads. Phone-only refinements (topbar
 * slide-away, translucent pads) stay behind the max-height: 500px blocks
 * below; the tablet tier (min-height: 501px, after those) re-sizes and
 * re-anchors the rails for big screens.
 */
@media (pointer: coarse) and (orientation: landscape) {
  body.running #controls {
    position: fixed;
    inset: 0;
    top: var(--bar-h);
    z-index: 20;
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr auto;
    align-items: center;
    padding: 10px calc(14px + var(--safe-r)) calc(10px + var(--safe-b))
      calc(14px + var(--safe-l));
    background: none;
    border: none;
    pointer-events: none;
  }
  #controls > * {
    pointer-events: auto;
  }
  #lr {
    grid-column: 1 / 3;
    grid-row: 1 / 2;
    align-self: start;
    justify-content: space-between;
  }
  /* The joystick touch region is deliberately huge (the whole left half of
     the row) and, being positioned, paints after these rows in tree order —
     without a raise it eats touches aimed at the L shoulder (and, on
     tablets, the absolutely-positioned shoulder/pill stacks). Buttons win. */
  #lr,
  #select-start {
    position: relative;
    z-index: 1;
  }
  .pad-shoulder {
    flex: 0 0 104px;
    height: 44px;
  }
  #main-controls {
    grid-column: 1 / 3;
    grid-row: 1 / 3;
    height: 100%;
    align-items: flex-end;
    padding-bottom: 8px;
  }
  #dpad,
  #joystick {
    --dpad-size: min(32cqi, 158px, 76cqb);
  }
  /* The joystick region spans the full row height so floating mode has room,
     but with the row bottom-aligned (align-items: flex-end) the d-pad rests
     low while a base centered at top:50% would float up to mid-height — the
     reported "joystick sits too high". Bottom-anchor the base so its resting
     center matches the bottom-aligned d-pad's center. Same story on the x
     axis: the region is the whole left half of the row, so a base centered
     at left:50% rests ~25% of the screen width in — far from where the d-pad
     (a flex child hugging the row's left edge) sits. Left-anchor it so the
     resting center matches the d-pad's landscape center. JS re-measures
     joyHome/joyBounds from the rendered rect, so floating spawn + clamp
     follow the new resting position automatically. */
  #joystick-base {
    top: auto;
    bottom: 0;
    left: 0;
    margin-top: 0;
    margin-left: 0;
  }
  #ab {
    --btn-size: min(calc(32cqi / 2.6), 62px, calc(76cqb / 2.6));
    height: min(32cqi, 180px, 76cqb);
  }
  #select-start {
    grid-column: 1 / 3;
    grid-row: 2 / 3;
    justify-self: center;
    gap: 14px;
  }
  /*
   * Large-controls in landscape: the portrait large-mode rules would blow the
   * d-pad up to ~330px (most of a phone's height) while shrinking A/B and the
   * shoulders — incoherent for the side-rail layout. Instead, "large" here
   * restores the pre-2026-07 landscape default sizes, one comfortable step up
   * from the shrunken default above.
   */
  body.large-controls #main-controls {
    height: 100%;
  }
  body.large-controls #dpad,
  body.large-controls #joystick {
    --dpad-size: min(40cqi, 210px, 92cqb);
  }
  body.large-controls #ab {
    --btn-size: min(calc(40cqi / 2.6), 82px, calc(92cqb / 2.6));
    width: calc(var(--btn-size) * 2.2);
    height: min(46cqi, 240px, 92cqb);
  }
  body.large-controls .pad-shoulder {
    flex: 0 0 120px;
    height: 46px;
  }
}

/*
 * GB/GBC (body.gb-mode, set on ROM load; body.link-gb for the 2P pair): the
 * system has no shoulder buttons, so the L/R row is dead weight — drop it AND
 * its grid track (a hidden item leaves a 0px track whose two gaps linger) and
 * hand the height back to the stage. In landscape the rails overlay the frame
 * (explicitly placed, so removing #lr shifts nothing) and just declutter.
 */
body.gb-mode #lr,
body.link-gb #lr {
  display: none;
}
body.gb-mode #controls,
body.link-gb #controls {
  grid-template-rows: 1fr auto;
}

/*
 * Short-portrait compression (4-inch class: iPhone SE 1st gen / 5s, 320x568).
 * The control strip's fixed rows otherwise squeeze the stage below what an
 * edge-to-edge 3:2 canvas needs (~213px at 320 wide), letterboxing the game.
 * Compress the chrome rows and gutters — NOT the d-pad/face buttons — to hand
 * ~30px back to the stage. Taller phones (667px-class and up) already reach
 * full width and keep the roomier default spacing.
 */
@media (pointer: coarse) and (orientation: portrait) and (max-height: 620px) {
  #controls {
    padding-top: 10px;
    padding-bottom: calc(12px + var(--safe-b));
    gap: 10px;
  }
  .pad-shoulder {
    height: 36px;
  }
  .pad-pill {
    height: 28px;
  }
}

/*
 * Container-query fallback (iOS 15 / pre-Safari-16). Container units (cqi/cqb)
 * parse fine inside custom properties on old WebKit — custom props accept any
 * token stream — so the cq declarations above still win the cascade there and
 * then fail at var() substitution, collapsing the d-pad and face buttons to
 * intrinsic size. A plain "fallback line above" therefore can't work; this
 * @supports-not block (skipped entirely by cq-capable browsers) re-declares
 * the sizes in viewport units, approximating the container's geometry:
 * #main-controls spans the viewport width minus strip padding (cqi ≈ vw) and
 * its height is capped well above what the px caps below allow, so dropping
 * the 100cqb terms is safe. The fixed-size tablet tier (min-height: 501px,
 * later in source) still overrides these on old iPads.
 */
@supports not (width: 1cqi) {
  #dpad,
  #joystick {
    --dpad-size: min(46vw, 240px);
  }
  #joystick {
    width: 50%; /* = 50cqi: % of width resolves against the same container */
  }
  #ab {
    --btn-size: min(calc(46vw / 2.7), 84px);
    height: min(46vw, 240px);
  }
  body.large-controls #dpad,
  body.large-controls #joystick {
    --dpad-size: min(62vw, 330px);
  }
  body.large-controls #ab {
    --btn-size: min(calc(42vw / 2.8), 74px);
  }
  @media (pointer: coarse) and (orientation: landscape) {
    #dpad,
    #joystick {
      --dpad-size: min(32vw, 158px, 76vh);
    }
    #ab {
      --btn-size: min(calc(32vw / 2.6), 62px);
      height: min(32vw, 180px, 76vh);
    }
    body.large-controls #dpad,
    body.large-controls #joystick {
      --dpad-size: min(40vw, 210px, 92vh);
    }
    body.large-controls #ab {
      --btn-size: min(calc(40vw / 2.6), 82px);
      height: min(46vw, 240px, 92vh);
    }
  }
}

/*
 * Mobile home-screen compaction. Placed after the base home rules so these
 * win on source order (equal specificity) — the logo + wordmark must never be
 * clipped behind the load box on short phones.
 */
@media (max-width: 560px) {
  #home {
    padding: 16px;
  }
  #home-inner {
    gap: 18px;
  }
  #home-logo {
    width: 58px;
    margin-bottom: 4px;
  }
  #home-brand h1 {
    font-size: 24px;
  }
  #home-tagline {
    font-size: 12px;
  }
  /* One full-width column on phones so game names aren't truncated */
  #home-recent {
    grid-template-columns: 1fr;
  }
}

/*
 * ==========================================================
 * 2P link mode
 * ==========================================================
 */

/* Two side-by-side screens (stacked in portrait), one per player */
#link-stage {
  display: none;
}

body.running.link-mode #canvas {
  display: none;
}

body.running.link-mode #link-stage {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 16px;
  width: 100%;
  height: 100%;
  padding: 12px;
}

/* Online rollback: one game (this player's), reusing link-canvas-0. */
body.running.rollback-mode #canvas { display: none; }
body.running.rollback-mode #link-stage {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: 12px;
}
body.rollback-mode #link-stage .link-pane:nth-child(2),
body.rollback-mode #link-stage .link-label { display: none; }
body.running.rollback-mode .link-pane canvas {
  width: min(100%, calc((100dvh - var(--bar-h) - 24px) * 1.5));
}
/* GB/GBC online rollback: single 160x144 (10:9) screen. */
body.running.rollback-mode.link-gb .link-pane canvas {
  width: min(100%, calc((100dvh - var(--bar-h) - 24px) * 1.111));
}

/* "Disconnect Link Cable" button — squared to match the other top-bar
   .icon-btn controls, occupying the space the hidden speed/state controls left
   so it's clear why they're gone. Shown only in rollback mode. */
#rb-disconnect {
  display: none;
  align-items: center;
  gap: 7px;
  height: 36px;
  padding: 0 14px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  background: rgba(var(--accent-rgb), 0.14); /* pre-Safari-16.2 fallback */
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  cursor: pointer;
}
#rb-disconnect svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
#rb-disconnect:hover {
  background: rgba(var(--accent-rgb), 0.26); /* pre-Safari-16.2 fallback */
  background: color-mix(in srgb, var(--accent) 26%, transparent);
}
body.running.rollback-mode #rb-disconnect { display: inline-flex; }
/* Narrow screens: drop the icon and " Link Cable" — just "Disconnect", which is
   clear and compact. */
@media (max-width: 480px) {
  #rb-disconnect svg,
  #rb-disconnect .rb-dc-suffix { display: none; }
  #rb-disconnect { padding: 0 12px; gap: 0; }
}

.link-pane {
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.link-pane canvas {
  width: min(100%, calc((100dvh - var(--bar-h) - 72px) * 1.5));
  max-width: 100%;
  aspect-ratio: 3 / 2;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  background: var(--screen-off);
  border-radius: 4px;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 18px 50px rgba(0, 0, 0, 0.6),
    0 0 60px -20px var(--accent-glow);
}

/* GB/GBC linked pair: 160x144 panels are 10:9, not the GBA's 3:2. */
body.link-gb .link-pane canvas {
  aspect-ratio: 10 / 9;
  width: min(100%, calc((100dvh - var(--bar-h) - 72px) * 1.111));
}

/* The screen the keyboard currently drives (click either to switch). The
   unfocused screen dims slightly so it's obvious which one has input. */
.link-pane:not(.focused) canvas {
  opacity: 0.72;
}
.link-pane.focused canvas {
  box-shadow: 0 0 0 2px var(--accent), 0 18px 50px rgba(0, 0, 0, 0.6),
    0 0 60px -16px var(--accent-glow);
}
.link-pane.focused .link-label {
  color: var(--accent);
}

.link-label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  text-transform: uppercase;
}

/* Portrait / narrow: stack the two screens */
@media (max-aspect-ratio: 5/4) {
  body.running.link-mode #link-stage {
    flex-direction: column;
    gap: 10px;
  }
  .link-pane canvas {
    width: min(100%, calc((50dvh - var(--bar-h)) * 1.5));
  }
  body.link-gb .link-pane canvas {
    width: min(100%, calc((50dvh - var(--bar-h)) * 1.111));
  }
}

/* Anything that would desync the linked pair is hidden in link mode:
   rewind and the speed toggles change one clock, save states and .sav
   import/export target a single core, and the screenshot path captures
   the (hidden) single-core canvas. Pause/reset stay: they act on both.
   Exception: in online rollback mode #speed-2x-btn stays visible — its 2x is
   relayed to the peer (RB_SPEED) so both cores fast-forward in lockstep.
   Unbounded #fast-forward has no such sync, so it stays hidden everywhere. */
body.link-mode #rewind,
body.link-mode #speed-2x-btn,
body.link-mode #fast-forward,
body.link-mode #save-state,
body.link-mode #load-state,
body.link-mode #manage-saves,
body.link-mode #screenshot,
body.link-mode #save-sep,
body.rollback-mode #rewind,
body.rollback-mode #fast-forward,
body.rollback-mode #reset,
body.rollback-mode #save-state,
body.rollback-mode #load-state,
body.rollback-mode #manage-saves,
body.rollback-mode #screenshot,
body.rollback-mode #save-sep {
  display: none !important;
}

/*
 * ==========================================================
 * Narrow phones: segmented playback bar
 * ==========================================================
 * The five playback buttons fuse into one segmented control
 * ([reset|rewind|pause|2x|ffw]) so the fps readout and the mute button
 * always fit beside them (375pt-wide phones cramp with separate buttons).
 */
@media (max-width: 480px) {
  /* Tighten the bar itself so all controls + the fps readout fit at 375pt */
  #topbar {
    gap: 6px;
    padding-left: calc(8px + var(--safe-l));
    padding-right: calc(8px + var(--safe-r));
  }
  body.running #playback-controls {
    gap: 0;
    /* Chrome tokens, not the dark surface set: on shell-coloured top bars the
       segmented cluster must stay frosted like the individual icon buttons,
       or its (chrome-ink) glyphs go dark-on-dark. */
    border: 1px solid var(--chrome-btn-border);
    border-radius: var(--radius-sm);
    background: linear-gradient(180deg, var(--chrome-btn-top), var(--chrome-btn-bottom));
    box-shadow: inset 0 1px 0 var(--inset-hi), 0 1px 2px rgba(0, 0, 0, 0.35);
    overflow: hidden;
  }
  #playback-controls .icon-btn {
    width: 30px;
    height: 34px;
    border: none;
    border-radius: 0;
    background: none;
    box-shadow: none;
  }
  /* Small separator between segments */
  #playback-controls .icon-btn + .icon-btn {
    border-left: 1px solid var(--chrome-btn-border);
  }
  #playback-controls .icon-btn svg {
    width: 17px;
    height: 17px;
  }
  #playback-controls .icon-btn.active {
    background: linear-gradient(180deg, var(--accent-tint-top), var(--btn-active-bottom));
  }
  #playback-controls .icon-btn:active {
    transform: none;
    box-shadow: inset 0 2px 5px var(--inset-lo);
  }
}

/*
 * ==========================================================
 * Mobile landscape: maximise the screen
 * ==========================================================
 * The top bar slides off-screen (a handle at the top edge pulls it back),
 * the stage takes the full height, and the overlaid touch controls hug the
 * screen edges, semi-transparent so they don't hide the game.
 */

/* Handle base style (only displayed by the landscape block below) */
#topbar-handle {
  display: none;
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  z-index: 41;
  width: 46px;
  height: 19px;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--text-dim);
  background: rgba(var(--surface-1-rgb), 0.55);
  border: 1px solid var(--border-2);
  border-top: none;
  border-radius: 0 0 9px 9px;
  cursor: pointer;
  transition: top 0.2s ease;
}

#topbar-handle svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 0.2s ease;
}

@media (pointer: coarse) and (orientation: landscape) and (max-height: 500px) {
  /* Top bar off-screen by default; the handle slides it in/out */
  body.running #topbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 40;
    transform: translateY(-100%);
    transition: transform 0.2s ease;
  }
  body.running.topbar-open #topbar {
    transform: translateY(0);
    box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55);
  }
  body.running #topbar-handle {
    display: flex;
  }
  body.running.topbar-open #topbar-handle {
    top: var(--bar-h);
  }
  body.running.topbar-open #topbar-handle svg {
    transform: rotate(180deg);
  }
  /* With the bar gone the stage owns the full height */
  body.running #canvas {
    width: min(100%, calc(100dvh * var(--game-ar, 1.5)));
  }
  body.running #controls {
    top: 0;
  }
  /* Hug the screen edges */
  body.running #controls {
    padding: 6px calc(6px + var(--safe-r)) calc(6px + var(--safe-b))
      calc(6px + var(--safe-l));
  }
  /* Fully transparent controls, just a faint outline and glyph: they
     overlap the display area, so they must not obscure it */
  .pad-btn,
  #dpad > #dpad-center {
    background: none;
    border-color: rgba(var(--hi-rgb), 0.28);
    box-shadow: none;
    color: rgba(var(--text-rgb), 0.5);
  }
  #dpad > #dpad-center::after,
  #joystick-base::after {
    background: none;
    box-shadow: none;
  }
  .pad-btn.pressed,
  .pad-btn.arm-active {
    color: var(--accent);
    background: none;
    border-color: rgba(var(--accent-rgb), 0.7);
    box-shadow: 0 0 14px -4px var(--accent-glow);
  }
  /* Opaque-controls opt-out: restore the solid portrait look for the buttons
     that the transparency rules above dimmed. Higher specificity than the
     bare-selector rules above, and later in source order, so it wins. */
  body.opaque-controls .pad-btn {
    background: linear-gradient(180deg, var(--surface-3), var(--surface-1));
    border-color: var(--border-2);
    box-shadow: inset 0 1px 0 var(--inset-hi), 0 2px 5px rgba(0, 0, 0, 0.4);
    color: var(--text-dim);
  }
  body.opaque-controls #dpad > #dpad-center {
    background: linear-gradient(180deg, var(--surface-3), var(--surface-1));
    border-color: var(--border-2);
    box-shadow: none;
    color: var(--text-dim);
  }
  body.opaque-controls #dpad > #dpad-center::after,
  body.opaque-controls #joystick-base::after {
    background: rgba(0, 0, 0, 0.25);
    box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.5);
  }
  body.opaque-controls .pad-btn.pressed,
  body.opaque-controls .pad-btn.arm-active {
    color: var(--accent);
    background: linear-gradient(180deg, var(--accent-tint-top), var(--pad-pressed-bottom));
    border-color: rgba(var(--accent-rgb), 0.55);
    box-shadow: inset 0 2px 7px var(--inset-lo), 0 0 16px -3px var(--accent-glow);
  }
  /* Select / Start shrink to small, label-less circles (the text isn't
     needed at this size; position tells them apart) */
  .pad-pill {
    flex: 0 0 38px;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    font-size: 0;
  }
  /* Large-controls keeps the circular landscape pills, at the old size (the
     portrait large-mode pill rule would otherwise re-widen them to 120x28
     labelled bars). */
  body.large-controls .pad-pill {
    flex: 0 0 42px;
    width: 42px;
    height: 42px;
    border-radius: 50%;
    font-size: 0;
  }
  #select-start {
    gap: 12px;
  }
}

/* Light theme, landscape translucent pads: the white outlines above are
   nearly invisible on the light stage — flip outline/glyph dark. Kept after
   (and re-asserting) the pressed rules because these selectors outrank them;
   the opaque-controls opt-out is excluded (its solid look themes correctly). */
@media (pointer: coarse) and (orientation: landscape) and (max-height: 500px) {
  :root[data-theme="light"] body:not(.opaque-controls) .pad-btn,
  :root[data-theme="light"] body:not(.opaque-controls) #dpad > #dpad-center {
    border-color: rgba(24, 32, 52, 0.4);
    color: rgba(var(--text-rgb), 0.6);
  }
  :root[data-theme="light"] body:not(.opaque-controls) .pad-btn.pressed,
  :root[data-theme="light"] body:not(.opaque-controls) .pad-btn.arm-active {
    color: var(--accent);
    border-color: rgba(var(--accent-rgb), 0.7);
  }
}

/*
 * ==========================================================
 * Tablet touch layouts
 * ==========================================================
 * Big touchscreens break both phone assumptions: the portrait strip's
 * flex-stretched shoulders grow to half the screen width, and the
 * landscape side rails only existed under max-height: 500px, so tablets
 * fell back to the stacked phone layout with the game squeezed small.
 * The rule here is reachability: a tablet is gripped by its side edges,
 * so every control pins to the bottom side corners at a FIXED physical
 * size (px caps, not viewport units) — as the screen grows the buttons
 * stay where thumbs are, and only the frame gains. Frequency orders the
 * stack: face buttons under the thumb, shoulders just above, the rare
 * Select/Start above those.
 */

/* Landscape tablets: the shared landscape grid above already pins the
   d-pad / A-B clusters to the bottom side corners; this tier keeps the
   top bar and the opaque tactile look (the slide-away bar and translucent
   pads are phone-cramp compromises), sizes the pads GBA-like for big
   screens, and stacks shoulder + pill above each cluster instead of
   leaving them at the far top corners / dead center. */
@media (pointer: coarse) and (orientation: landscape) and (min-height: 501px) {
  body.running {
    /* Width reserved at each side for the control rail: pad size + outer
       padding + a margin so the frame never sits under the buttons. */
    --rail-w: 224px;
  }
  body.running.large-controls {
    --rail-w: 274px;
  }
  /* The frame yields to the rails (never the other way around): reserve
     the rail width as stage padding. The base #canvas percentage rule
     resolves against the stage CONTENT box, and updateCanvasScaling
     subtracts padding from its contain-fit measurements, so both the
     pre-JS fallback and the JS-pinned size respect the rails. */
  body.running #stage {
    /* The 60px bottom band keeps the frame clear of the Select/Start
       pills (bottom-inboard of the clusters) on aspect ratios where the
       contain-fit would otherwise reach the stage's bottom edge. */
    padding: 0 var(--rail-w) 60px;
  }
  /* A connected gamepad hides the touch rails ("hide touch controls with a
     controller") — give the reserved rail width back to the frame. */
  body.running.gamepad-hides-touch #stage {
    padding: 0;
  }
  #dpad,
  #joystick {
    /* ~4 cm at iPad density — physical-handheld d-pad territory */
    --dpad-size: 190px;
  }
  #ab {
    --btn-size: 73px;
    width: calc(var(--btn-size) * 2.2);
    height: 190px;
  }
  /* Shoulders sit directly above their cluster at the screen edges (the
     top corners are out of the thumb arc on a tablet). Absolutely
     positioned against the fixed #controls overlay; the spanning flex row
     itself must not eat stage taps. */
  #lr {
    position: absolute;
    left: calc(14px + var(--safe-l));
    right: calc(14px + var(--safe-r));
    bottom: calc(10px + var(--safe-b) + var(--rail-btn-h, 190px) + 22px);
    grid-area: auto;
    justify-content: space-between;
    pointer-events: none;
  }
  .pad-shoulder {
    flex: 0 0 150px;
    height: 46px;
    pointer-events: auto;
  }
  /* Select / Start sit inboard of their cluster on the bottom row — the
     GBA's own bottom placement, and a shorter thumb sweep than topping
     the rail stack. The insets clear the cluster widths (d-pad left,
     A/B right); the stage's bottom band above keeps the frame off them. */
  #select-start {
    position: absolute;
    left: calc(26px + var(--safe-l) + var(--rail-cluster-l, 190px));
    right: calc(26px + var(--safe-r) + var(--rail-cluster-r, 161px));
    bottom: calc(18px + var(--safe-b));
    grid-area: auto;
    /* The shared landscape block's justify-self: center would shrink-wrap
       and re-center this absolutely-positioned row (grid self-alignment
       applies to abs-pos children); stretch it back edge to edge. */
    justify-self: stretch;
    justify-content: space-between;
    pointer-events: none;
  }
  .pad-pill {
    /* 0 1: allowed to shrink so the two pills never collide mid-screen
       on the narrowest tablet-tier viewports. */
    flex: 0 1 150px;
    pointer-events: auto;
  }
  body.large-controls #dpad,
  body.large-controls #joystick {
    --dpad-size: 240px;
  }
  body.large-controls #ab {
    --btn-size: 92px;
    width: calc(var(--btn-size) * 2.2);
    height: 240px;
  }
  body.large-controls #lr {
    --rail-btn-h: 240px;
  }
  body.large-controls #select-start {
    /* Cluster widths grow in large mode: 240px d-pad, 92px * 2.2 A/B. */
    --rail-cluster-l: 240px;
    --rail-cluster-r: 203px;
  }
  body.large-controls .pad-shoulder {
    flex: 0 0 170px;
    height: 46px;
  }
  body.large-controls .pad-pill {
    flex: 0 0 150px;
    height: 34px;
    font-size: 11px;
  }
}

/* Portrait tablets: the stacked strip already pushes d-pad / A-B to the
   side edges, but the flex-stretched shoulders balloon to half the screen
   width and Select/Start sit in the unreachable dead center. Shoulders
   pin to the strip's edges at fixed widths; Select/Start sit inboard of
   the clusters on the bottom row, matching the landscape rails. */
@media (pointer: coarse) and (orientation: portrait) and (min-width: 700px) {
  #lr {
    justify-content: space-between;
  }
  .pad-shoulder {
    flex: 0 0 200px;
  }
  /* Absolutely positioned against the strip; the collapsed grid row gives
     its height back to the stage. Insets clear the cluster widths (16px
     strip padding + cluster + 12px gap); bottom aligns the pills with the
     cluster bottoms ((main-controls height - cluster height) / 2 above
     the row's bottom edge, itself 14px row gap above the 16px padding). */
  #controls {
    position: relative;
  }
  #select-start {
    position: absolute;
    left: calc(28px + var(--safe-l) + var(--port-cluster-l, 240px));
    right: calc(28px + var(--safe-r) + var(--port-cluster-r, 185px));
    bottom: calc(var(--safe-b) + var(--port-pill-b, 55px));
    justify-content: space-between;
    pointer-events: none;
  }
  .pad-pill {
    /* 0 1: allowed to shrink so the pills never collide near the 700px
       tier threshold. */
    flex: 0 1 150px;
    pointer-events: auto;
  }
  body.large-controls #select-start {
    /* Large mode: 330px d-pad fills the taller row (no centering gap);
       A/B narrows to 74px * 2.05. */
    --port-cluster-l: 330px;
    --port-cluster-r: 152px;
    --port-pill-b: 44px;
  }
}

/*
 * ==========================================================
 * Online link mode (phase 3b)
 * ==========================================================
 */

/* Room-code modal */
.net-join-row {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}

/* The code field fills the row; the spinner overlays its right edge while
   connecting, so no separate waiting line is needed. */
.net-input-wrap {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
}

#net-code-input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-align: center;
  text-transform: uppercase;
  color: var(--text);
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
}

#net-code-input:focus {
  outline: none;
  border-color: var(--accent);
}

/* Connecting: the field is locked (read-only) and dimmed, with the spinner at
   its right — the read-only cursor + spinner + "Cancel" button convey waiting. */
#net-code-input:read-only {
  color: var(--text-dim);
  border-color: var(--accent);
  padding-right: 32px;
  cursor: default;
}

/* Only errors land here now (the wait is shown in-field), so it takes no space
   until it has something to say — no empty gap under the button. */
.net-status {
  margin: 0;
  font-size: 13px;
  color: var(--text-dim);
  text-align: center;
}

.net-status:not(:empty) {
  margin-top: 12px;
}

.net-status.net-error {
  color: var(--danger);
}

/* Small amber spinner pinned inside the right of the code field while connecting. */
.net-spinner {
  position: absolute;
  top: 50%;
  right: 11px;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 2px solid rgba(var(--accent-rgb), 0.25);
  border-top-color: var(--accent);
  animation: net-spin 0.7s linear infinite;
  pointer-events: none;
}

.net-spinner[hidden] {
  display: none;
}

@keyframes net-spin {
  from { transform: translateY(-50%) rotate(0); }
  to { transform: translateY(-50%) rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .net-spinner {
    animation-duration: 2s;
  }
}

/* ---- Manual code exchange (signaling server unreachable) ---- */

#net-manual-view[hidden] { display: none; }

.net-manual-label {
  margin: 14px 0 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dim);
}

.net-manual-code {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text);
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
}
.net-manual-code:focus { outline: none; border-color: var(--accent); }
/* The "Your code" box is disabled (Copy moves it); keep it legible, not grey-out. */
input.net-manual-code:disabled {
  color: var(--text-dim);
  -webkit-text-fill-color: var(--text-dim);
  opacity: 1;
}

/* "waiting for peer" badge: subtle chip over the top-right of the stage,
   shown only during sustained stalls of the emulated clock */
#net-stall {
  position: absolute;
  top: 10px;
  right: 12px;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--accent);
  background: var(--net-stall-bg);
  border: 1px solid rgba(var(--accent-rgb), 0.35);
  border-radius: 999px;
  pointer-events: none;
}

#net-stall[hidden] {
  display: none;
}

/* Anything that would desync (or reset) one side of the pair is hidden
   while linked online. Unlike local 2P, reset also hides (it can't reach
   the remote core), while screenshot and save-file export are fine (this
   side's canvas and .sav are the only ones here). */
body.net-mode #rewind,
body.net-mode #speed-2x-btn,
body.net-mode #fast-forward,
body.net-mode #reset,
body.net-mode #save-state,
body.net-mode #load-state,
/* Inside Manage Saves, only save-file EXPORT stays: states and save import
   would desync (or reset) this side of the pair */
body.net-mode #saves-modal .modal-toggle-row:has(#export-state),
body.net-mode #saves-modal .modal-toggle-row:has(#import-state),
body.net-mode #saves-modal .modal-toggle-row:has(#load-save),
body.net-mode #saves-modal .modal-subhead.no-rule,
/* Resetting the live game's save mid-session would desync the pair. */
body.net-mode #saves-modal .modal-toggle-row:has(#reset-save-slot) {
  display: none !important;
}

/*
 * ==========================================================
 * Google Drive backup (Manage ROMs and Saves modal)
 * ==========================================================
 */

.gdrive-status {
  font-size: 12.5px;
  color: var(--text-dim);
  margin: 0 0 10px;
  overflow-wrap: anywhere;
}

.gdrive-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 10px;
}

.gdrive-progress {
  font-size: 12px;
  color: var(--text-faint);
  margin: 0 0 10px;
  min-height: 15px;
  overflow-wrap: anywhere;
}

.gdrive-restore-name {
  flex: 1 1 140px;
  min-width: 0;
  font-size: 13px;
}

.gdrive-restore-title {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gdrive-restore-sub {
  display: block;
  margin-top: 1px;
  font-size: 11.5px;
  color: var(--text-faint);
}

/* ───────────────────────── Google Drive sync ───────────────────────── */
/* Sync status indicator — occupies the retired status-LED slot. Muted on
   purpose: the accent belongs to the enhanced-audio indicator, and this should
   read as ambient state, not an alert. */
#sync-indicator {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 12px;
  color: var(--text-faint);
  cursor: pointer;
  white-space: nowrap;
}
#sync-indicator[hidden] {
  display: none;
}
#sync-indicator svg {
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.1;
  stroke-linecap: round;
  stroke-linejoin: round;
}
#sync-indicator:hover {
  color: var(--text-dim);
}
.sync-spin {
  transform-origin: center;
  animation: sync-spin 1s linear infinite;
}
@keyframes sync-spin {
  to {
    transform: rotate(360deg);
  }
}
/* The status row is normally running-only; sync must be able to reveal it. */
body.sync-shown #status {
  display: inline-flex;
}
/* Phones: icon only — tapping it toasts the description. The fps unit goes
   too: with sync + enhanced-audio both showing, "48 fps" overflows the bar at
   375pt, and the bare number reads fine. */
@media (max-width: 560px) {
  #sync-indicator .sync-label,
  .fps-unit {
    display: none;
  }
}

/* The grid's Sync link doubles as a progress readout while a sync runs. */
#home-sync {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
/* display:inline-flex above would otherwise defeat the [hidden] attribute
   used to keep this out of the bar when signed out. */
#home-sync[hidden] {
  display: none;
}
#home-sync svg {
  width: 11px;
  height: 11px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
#home-sync[disabled] {
  cursor: default;
  text-decoration: none;
  color: var(--text-faint);
}

/* Manage ROMs sort toggle */
.roms-sort-row {
  display: flex;
  justify-content: flex-end;
  margin: -4px 0 10px;
}
.roms-sort-row[hidden] {
  display: none;
}

/* Tile system chip — one identity slot per tile (replaced the cartridge glyph
   and the duplicate corner badge). Box art takes the same slot when present. */
.sys-chip {
  flex: 0 0 auto;
  width: 34px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.03em;
  border-radius: 5px;
}

/* A game that lives on Drive but isn't downloaded to this device yet. */
.home-tile-cloud .home-tile-launch {
  border-style: dashed;
  opacity: 0.72;
}
.home-tile-cloud .home-tile-launch:hover {
  opacity: 1;
}
/* Its own tap target (a real button): download-without-launch. Padding widens
   the hit area within the launch button's reserved right padding; the glyph
   stays visually where it was. */
.home-tile-dl {
  position: absolute;
  top: 50%;
  right: 2px;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 6px;
  color: var(--text-faint);
  background: none;
  border: 1px solid transparent;
  border-radius: 6px;
  cursor: pointer;
  transition: color 0.12s, background 0.12s, border-color 0.12s;
}
.home-tile-dl:hover {
  color: var(--accent);
  background: rgba(var(--hi-rgb), 0.08);
  border-color: var(--border-2);
}
.home-tile-dl:active {
  transform: translateY(-50%) scale(0.92);
}
.home-tile-dl.is-busy {
  cursor: default;
  color: var(--text-faint);
  background: none;
  border-color: transparent;
}
.home-tile-dl svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* "Games removed on another device" modal */
.tomb-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 44vh;
  overflow-y: auto;
  margin: 4px -6px 16px;
  padding: 2px 6px;
}
.tomb-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.tomb-name {
  flex: 1 1 auto;
  min-width: 0;
}
.tomb-title {
  display: block;
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tomb-actions {
  display: flex;
  gap: 8px;
}
.tomb-actions .button {
  flex: 1 1 auto;
  justify-content: center;
}

/* ─────────────────────────── Cheats modal ─────────────────────────── */
.cheats-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 14px;
}

.cheat-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
}

.cheat-row input[type="checkbox"] {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
}

.cheat-row-info {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1 1 auto;
}

.cheat-row-name {
  font-size: 13px;
  color: var(--text);
}

.cheat-row-code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 11px;
  color: var(--text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cheat-del {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  line-height: 1;
  font-size: 16px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--text-faint);
  cursor: pointer;
}
.cheat-del:hover {
  background: rgba(var(--danger-rgb), 0.16);
  color: var(--danger);
}

.cheat-input,
.cheat-textarea {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 8px;
  padding: 8px 10px;
  background: var(--surface-1);
  border: 1px solid var(--border-2);
  border-radius: 8px;
  color: var(--text);
  font-size: 13px;
}

.cheat-textarea {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  resize: vertical;
  min-height: 56px;
}

.cheat-input:focus,
.cheat-textarea:focus {
  outline: none;
  border-color: var(--accent);
}

.cheat-error {
  font-size: 12px;
  color: var(--danger);
  margin: 2px 0 10px;
  white-space: pre-line;
}
.cheat-error[hidden] { display: none; }
